Give build/run/check/test/doc a -file flag.

A package has canonically always been a directory, but odin allowing you to build a single-file package confused newcomers who didn't understand why they could then not access variables and procedures from another file in the same directory.

This change disallows building single-file packages by default, requiring the `-file` flag to acknowledge you understand the nuance.

`-help` for these commands also clarifies the difference.

```
W:\Odin>odin build -help
odin is a tool for managing Odin source code
Usage:
        odin build [arguments]

        build   Compile directory of .odin files as an executable.
                One must contain the program's entry point, all must be in the same package.
                Use `-file` to build a single file instead.
                Examples:
                        odin build .                    # Build package in current directory
                        odin build <dir>                # Build package in <dir>
                        odin build filename.odin -file  # Build single-file package, must contain entry point.

        Flags

        -file
                Tells `odin build` to treat the given file as a self-contained package.
                This means that `<dir>/a.odin` won't have access to `<dir>/b.odin`'s contents.
```

```
W:\Odin>odin run examples\demo\demo.odin
ERROR: `odin run` takes a package as its first argument.
Did you mean `odin run examples\demo\demo.odin -file`?
The `-file` flag tells it to treat a file as a self-contained package.
```
This commit is contained in:
Jeroen van Rijn
2022-04-05 20:26:18 +02:00
parent 48012ec73c
commit ad0a413b40
3 changed files with 141 additions and 85 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ build_odin() {
} }
run_demo() { run_demo() {
./odin run examples/demo/demo.odin ./odin run examples/demo/demo.odin -file
} }
case $OS in case $OS in
+22 -1
View File
@@ -598,7 +598,6 @@ bool allow_check_foreign_filepath(void) {
return true; return true;
} }
// TODO(bill): OS dependent versions for the BuildContext // TODO(bill): OS dependent versions for the BuildContext
// join_path // join_path
// is_dir // is_dir
@@ -606,6 +605,28 @@ bool allow_check_foreign_filepath(void) {
// is_abs_path // is_abs_path
// has_subdir // has_subdir
enum TargetFileValidity : u8 {
TargetFileValidity_Invalid,
TargetFileValidity_Writable_File,
TargetFileValidity_No_Write_Permission,
TargetFileValidity_Directory,
TargetTargetFileValidity_COUNT,
};
TargetFileValidity set_output_filename(void) {
// Assembles the output filename from build_context information.
// Returns `true` if it doesn't exist or is a file.
// Returns `false` if a directory or write-protected file.
return TargetFileValidity_Writable_File;
}
String const WIN32_SEPARATOR_STRING = {cast(u8 *)"\\", 1}; String const WIN32_SEPARATOR_STRING = {cast(u8 *)"\\", 1};
String const NIX_SEPARATOR_STRING = {cast(u8 *)"/", 1}; String const NIX_SEPARATOR_STRING = {cast(u8 *)"/", 1};
+57 -22
View File
@@ -585,14 +585,14 @@ void usage(String argv0) {
print_usage_line(0, "Usage:"); print_usage_line(0, "Usage:");
print_usage_line(1, "%.*s command [arguments]", LIT(argv0)); print_usage_line(1, "%.*s command [arguments]", LIT(argv0));
print_usage_line(0, "Commands:"); print_usage_line(0, "Commands:");
print_usage_line(1, "build compile .odin file, or directory of .odin files, as an executable."); print_usage_line(1, "build compile directory of .odin files, as an executable.");
print_usage_line(1, " one must contain the program's entry point, all must be in the same package."); print_usage_line(1, " one must contain the program's entry point, all must be in the same package.");
print_usage_line(1, "run same as 'build', but also then runs the newly compiled executable."); print_usage_line(1, "run same as 'build', but also then runs the newly compiled executable.");
print_usage_line(1, "check parse, and type check an .odin file, or directory of .odin files"); print_usage_line(1, "check parse, and type check a directory of .odin files");
print_usage_line(1, "query parse, type check, and output a .json file containing information about the program"); print_usage_line(1, "query parse, type check, and output a .json file containing information about the program");
print_usage_line(1, "strip-semicolon parse, type check, and remove unneeded semicolons from the entire program"); print_usage_line(1, "strip-semicolon parse, type check, and remove unneeded semicolons from the entire program");
print_usage_line(1, "test build ands runs procedures with the attribute @(test) in the initial package"); print_usage_line(1, "test build ands runs procedures with the attribute @(test) in the initial package");
print_usage_line(1, "doc generate documentation .odin file, or directory of .odin files"); print_usage_line(1, "doc generate documentation on a directory of .odin files");
print_usage_line(1, "version print version"); print_usage_line(1, "version print version");
print_usage_line(1, "report print information useful to reporting a bug"); print_usage_line(1, "report print information useful to reporting a bug");
print_usage_line(0, ""); print_usage_line(0, "");
@@ -604,6 +604,7 @@ enum BuildFlagKind {
BuildFlag_Invalid, BuildFlag_Invalid,
BuildFlag_Help, BuildFlag_Help,
BuildFlag_SingleFile,
BuildFlag_OutFile, BuildFlag_OutFile,
BuildFlag_OptimizationLevel, BuildFlag_OptimizationLevel,
@@ -763,6 +764,7 @@ ExactValue build_param_to_exact_value(String name, String param) {
bool parse_build_flags(Array<String> args) { bool parse_build_flags(Array<String> args) {
auto build_flags = array_make<BuildFlag>(heap_allocator(), 0, BuildFlag_COUNT); auto build_flags = array_make<BuildFlag>(heap_allocator(), 0, BuildFlag_COUNT);
add_flag(&build_flags, BuildFlag_Help, str_lit("help"), BuildFlagParam_None, Command_all); add_flag(&build_flags, BuildFlag_Help, str_lit("help"), BuildFlagParam_None, Command_all);
add_flag(&build_flags, BuildFlag_SingleFile, str_lit("file"), BuildFlagParam_None, Command__does_build | Command__does_check);
add_flag(&build_flags, BuildFlag_OutFile, str_lit("out"), BuildFlagParam_String, Command__does_build &~ Command_test); add_flag(&build_flags, BuildFlag_OutFile, str_lit("out"), BuildFlagParam_String, Command__does_build &~ Command_test);
add_flag(&build_flags, BuildFlag_OptimizationLevel, str_lit("opt"), BuildFlagParam_Integer, Command__does_build); add_flag(&build_flags, BuildFlag_OptimizationLevel, str_lit("opt"), BuildFlagParam_Integer, Command__does_build);
add_flag(&build_flags, BuildFlag_OptimizationMode, str_lit("o"), BuildFlagParam_String, Command__does_build); add_flag(&build_flags, BuildFlag_OptimizationMode, str_lit("o"), BuildFlagParam_String, Command__does_build);
@@ -1879,32 +1881,46 @@ void remove_temp_files(lbGenerator *gen) {
void print_show_help(String const arg0, String const &command) { void print_show_help(String const arg0, String const &command) {
print_usage_line(0, "%.*s is a tool for managing Odin source code", LIT(arg0)); print_usage_line(0, "%.*s is a tool for managing Odin source code", LIT(arg0));
print_usage_line(0, "Usage"); print_usage_line(0, "Usage:");
print_usage_line(1, "%.*s %.*s [arguments]", LIT(arg0), LIT(command)); print_usage_line(1, "%.*s %.*s [arguments]", LIT(arg0), LIT(command));
print_usage_line(0, ""); print_usage_line(0, "");
if (command == "build") { if (command == "build") {
print_usage_line(1, "build compile .odin file, or directory of .odin files, as an executable."); print_usage_line(1, "build Compile directory of .odin files as an executable.");
print_usage_line(1, " one must contain the program's entry point, all must be in the same package."); print_usage_line(2, "One must contain the program's entry point, all must be in the same package.");
} else if (command == "run") { print_usage_line(2, "Use `-file` to build a single file instead.");
print_usage_line(1, "run same as 'build', but also then runs the newly compiled executable.");
print_usage_line(1, " append an empty flag and then the args, '-- <args>', to specify args for the output.");
} else if (command == "check") {
print_usage_line(1, "check parse and type check .odin file(s)");
} else if (command == "test") {
print_usage_line(1, "test build ands runs procedures with the attribute @(test) in the initial package");
} else if (command == "query") {
print_usage_line(1, "query [experimental] parse, type check, and output a .json file containing information about the program");
} else if (command == "doc") {
print_usage_line(1, "doc generate documentation from a .odin file, or directory of .odin files");
print_usage_line(2, "Examples:"); print_usage_line(2, "Examples:");
print_usage_line(3, "odin doc core/path"); print_usage_line(3, "odin build . # Build package in current directory");
print_usage_line(3, "odin doc core/path core/path/filepath"); print_usage_line(3, "odin build <dir> # Build package in <dir>");
print_usage_line(3, "odin build filename.odin -file # Build single-file package, must contain entry point.");
} else if (command == "run") {
print_usage_line(1, "run Same as 'build', but also then runs the newly compiled executable.");
print_usage_line(2, "Append an empty flag and then the args, '-- <args>', to specify args for the output.");
print_usage_line(2, "Examples:");
print_usage_line(3, "odin run . # Build and run package in current directory");
print_usage_line(3, "odin run <dir> # Build and run package in <dir>");
print_usage_line(3, "odin run filename.odin -file # Build and run single-file package, must contain entry point.");
} else if (command == "check") {
print_usage_line(1, "check Parse and type check directory of .odin files");
print_usage_line(2, "Examples:");
print_usage_line(3, "odin check . # Type check package in current directory");
print_usage_line(3, "odin check <dir> # Type check package in <dir>");
print_usage_line(3, "odin check filename.odin -file # Type check single-file package, must contain entry point.");
} else if (command == "test") {
print_usage_line(1, "test Build ands runs procedures with the attribute @(test) in the initial package");
} else if (command == "query") {
print_usage_line(1, "query [experimental] Parse, type check, and output a .json file containing information about the program");
} else if (command == "doc") {
print_usage_line(1, "doc generate documentation from a directory of .odin files");
print_usage_line(2, "Examples:");
print_usage_line(3, "odin doc . # Generate documentation on package in current directory");
print_usage_line(3, "odin doc <dir> # Generate documentation on package in <dir>");
print_usage_line(3, "odin doc filename.odin -file # Generate documentation on single-file package.");
} else if (command == "version") { } else if (command == "version") {
print_usage_line(1, "version print version"); print_usage_line(1, "version print version");
} else if (command == "strip-semicolon") { } else if (command == "strip-semicolon") {
print_usage_line(1, "strip-semicolon"); print_usage_line(1, "strip-semicolon");
print_usage_line(2, "parse and type check .odin file(s) and then remove unneeded semicolons from the entire project"); print_usage_line(2, "Parse and type check .odin file(s) and then remove unneeded semicolons from the entire project");
} }
bool doc = command == "doc"; bool doc = command == "doc";
@@ -1919,6 +1935,13 @@ void print_show_help(String const arg0, String const &command) {
print_usage_line(1, "Flags"); print_usage_line(1, "Flags");
print_usage_line(0, ""); print_usage_line(0, "");
if (check) {
print_usage_line(1, "-file");
print_usage_line(2, "Tells `%.*s %.*s` to treat the given file as a self-contained package.", LIT(arg0), LIT(command));
print_usage_line(2, "This means that `<dir>/a.odin` won't have access to `<dir>/b.odin`'s contents.");
print_usage_line(0, "");
}
if (doc) { if (doc) {
print_usage_line(1, "-short"); print_usage_line(1, "-short");
print_usage_line(2, "Show shortened documentation for the packages"); print_usage_line(2, "Show shortened documentation for the packages");
@@ -2487,8 +2510,6 @@ int strip_semicolons(Parser *parser) {
return cast(int)failed; return cast(int)failed;
} }
int main(int arg_count, char const **arg_ptr) { int main(int arg_count, char const **arg_ptr) {
if (arg_count < 2) { if (arg_count < 2) {
usage(make_string_c(arg_ptr[0])); usage(make_string_c(arg_ptr[0]));
@@ -2557,6 +2578,7 @@ int main(int arg_count, char const **arg_ptr) {
args = array_slice(args, 0, last_non_run_arg); args = array_slice(args, 0, last_non_run_arg);
run_args_string = string_join_and_quote(heap_allocator(), run_args); run_args_string = string_join_and_quote(heap_allocator(), run_args);
init_filename = args[2]; init_filename = args[2];
run_output = true; run_output = true;
@@ -2649,6 +2671,19 @@ int main(int arg_count, char const **arg_ptr) {
build_context.show_help = true; build_context.show_help = true;
} }
if (init_filename.len > 0 && !build_context.show_help) {
// The command must be build, run, test, check, or another that takes a directory or filename.
if (!path_is_directory(init_filename)) {
// Input package is a filename. We allow this only if `-file` was given, otherwise we exit with an error message.
if (!(args.count > 3 && args[3] == "-file")) {
gb_printf_err("ERROR: `%.*s %.*s` takes a package as its first argument.\n", LIT(args[0]), LIT(command));
gb_printf_err("Did you mean `%.*s %.*s %.*s -file`?\n", LIT(args[0]), LIT(command), LIT(init_filename));
gb_printf_err("The `-file` flag tells it to treat a file as a self-contained package.\n");
return 1;
}
}
}
build_context.command = command; build_context.command = command;
if (!parse_build_flags(args)) { if (!parse_build_flags(args)) {