diff options
| author | hachem <im@hachem.wtf> | 2026-10-13 01:02:42 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-10-13 01:02:42 +0200 |
| commit | 346d8bc24babeed3a112895ec34aab0074b4a78b (patch) | |
| tree | 2c476ce32c2b1939fcd7dcc747315b7378eaa02e /mkproj.c | |
| parent | 0c4716968a6ad336324af15df158db66db3f651f (diff) | |
Diffstat (limited to 'mkproj.c')
| -rw-r--r-- | mkproj.c | 1120 |
1 files changed, 950 insertions, 170 deletions
@@ -5,7 +5,7 @@ * stop writing the same fucking build files every time you start a project. * * Author: hachem <im@hachem.wtf> - * Version: 0.3.0 + * Version: 0.4.0 */ #include <ctype.h> @@ -21,7 +21,7 @@ #include <unistd.h> #define mkproj_program "mkproj" -#define mkproj_version "0.3.0" +#define mkproj_version "0.4.0" #define mkproj_tagline "dumb project generator" enum Language @@ -30,6 +30,8 @@ enum Language language_cpp, language_objc, language_nasm, + language_swift, + language_crust, language_count }; @@ -46,7 +48,8 @@ enum Build_System enum Kind { kind_exe, - kind_lib + kind_lib, + kind_shared }; enum License @@ -82,6 +85,8 @@ struct Build_Info struct Config { const char* name; + const char* dir; + const char* src_dir; enum Language language; const char* standard; const char* compiler; @@ -92,9 +97,13 @@ struct Config const char* bin_int_dir; int use_git; int use_intermediate; - int use_clangd; int use_flat; int use_docker; + int use_root; + int use_style; + int use_ci; + int use_single; + int use_validation; }; static const char* const c_standards[] = @@ -129,7 +138,7 @@ static const struct Language_Info languages[language_count] = .extension = "c", .standards = c_standards, .standard_count = sizeof c_standards / sizeof c_standards[0], - .default_standard = "c17", + .default_standard = "c2y", }, [language_cpp] = { @@ -139,7 +148,7 @@ static const struct Language_Info languages[language_count] = .extension = "cpp", .standards = cpp_standards, .standard_count = sizeof cpp_standards / sizeof cpp_standards[0], - .default_standard = "c++23", + .default_standard = "c++26", }, [language_objc] = { @@ -149,7 +158,7 @@ static const struct Language_Info languages[language_count] = .extension = "m", .standards = c_standards, .standard_count = sizeof c_standards / sizeof c_standards[0], - .default_standard = "c17", + .default_standard = "c2y", }, [language_nasm] = { @@ -161,8 +170,36 @@ static const struct Language_Info languages[language_count] = .standard_count = 0, .default_standard = NULL, }, + [language_swift] = + { + .key = "swift", + .name = "Swift", + .compiler = "swiftc", + .extension = "swift", + .standards = NULL, + .standard_count = 0, + .default_standard = NULL, + }, + [language_crust] = + { + .key = "crust", + .name = "Rust", + .compiler = "rustc", + .extension = "rs", + .standards = NULL, + .standard_count = 0, + .default_standard = NULL, + }, }; +/* the clang-driven languages: they share -std, the warning set, per-object + * make rules and clang-format. nasm, swift and crust each build their own way + * instead. */ +static int is_c_family(enum Language language) +{ + return language == language_c || language == language_cpp || language == language_objc; +} + static const struct Build_Info build_systems[build_system_count] = { [build_make] = @@ -223,30 +260,172 @@ static void print_usage(FILE* stream) "stop writing the same fucking build files every time you start a project.\n" "\n" "usage: %s [-l lang] [-s std] [-b system] [-k kind] [--cc cc] [--license name]\n" - " [--no-git] [--intermediate] [--flat] [--docker] [--bin dir]\n" - " [--bin-int dir] [--no-clangd] <name>\n" + " [--git] [--no-root] [--single] [--no-validation] [--docker]\n" + " [--style] [--ci] [--src dir] [--bin dir] [--bin-int dir] <name>\n" + " %s --completions bash|zsh|fish\n" " %s [-v | -h]\n" "\n" + "<name> is the project directory to create, or . to generate into the\n" + "current directory (the project name is taken from its basename).\n" + "\n" + "defaults: a flat C project built with make, binary at the project root,\n" + "the latest language standard, and objects under bin/.\n" + "\n" "options:\n" - " -l, --lang LANG language: c, cpp, objc, nasm (default c)\n" - " -s, --std STD language standard (default per language)\n" + " -l, --lang LANG language: c, cpp, objc, nasm, swift, crust (default c)\n" + " -s, --std STD language standard (default: latest for the language)\n" " -b, --build SYSTEM build system: make, cmake, meson, premake, xmake (default make)\n" " -k, --kind KIND exe or lib (default exe)\n" " --cc CC compiler (default clang/clang++); make, premake, xmake\n" " --license NAME add a LICENSE: mit, isc, bsd-2, bsd-3, unlicense\n" - " --no-git don't initialize a git repository\n" - " --intermediate use a separate intermediate object directory\n" - " --flat flat layout: sources at the root, no src/ directory\n" + " --git initialize a git repository and write a .gitignore\n" + " --src DIR put sources in DIR/ instead of at the project root\n" + " --no-root put the binary in the build dir, not the project root\n" + " --single single-file project: compile directly, no object dir (make, c family)\n" + " --no-validation drop the make check target and the sanitizer flags\n" " --docker add a Dockerfile to build in a container (c, cpp, nasm)\n" + " --style add .clang-format + .editorconfig from the style guide\n" + " --ci add a GitHub Actions workflow that builds the project\n" " --bin DIR build directory (default per build system)\n" - " --bin-int DIR separate object dir for make/premake; implies --intermediate\n" - " --no-clangd don't generate compile_flags.txt for clangd\n" + " --bin-int DIR put objects in a separate dir (make/premake)\n" " -v, --version print version and bail\n" " -h, --help print this and bail\n" "\n" "by hachem <im@hachem.wtf>\n", mkproj_program, mkproj_version, mkproj_tagline, - mkproj_program, mkproj_program); + mkproj_program, mkproj_program, mkproj_program); +} + +/* space-separated language / build-system / license keys, straight from the + * tables so the completions never drift from what the parser accepts. */ +static void collect_langs(char* buf, size_t size) +{ + size_t off = 0; + + for (enum Language l = 0; l < language_count; l++) + off += (size_t) snprintf(buf + off, size - off, "%s%s", off != 0 ? " " : "", languages[l].key); +} + +static void collect_builds(char* buf, size_t size) +{ + size_t off = 0; + + for (enum Build_System b = 0; b < build_system_count; b++) + off += (size_t) snprintf(buf + off, size - off, "%s%s", off != 0 ? " " : "", build_systems[b].key); +} + +static void collect_licenses(char* buf, size_t size) +{ + size_t off = 0; + + for (enum License l = license_mit; l < license_count; l++) + off += (size_t) snprintf(buf + off, size - off, "%s%s", off != 0 ? " " : "", licenses[l].key); +} + +/* the plain (no-argument) flags, shared by every shell's option list. */ +#define mkproj_flags \ + "-l --lang -s --std -b --build -k --kind --cc --license " \ + "--git --src --no-root --single --no-validation --docker --style --ci " \ + "--bin --bin-int -v --version -h --help" + +static void print_completions_bash(void) +{ + char langs[128], builds[128], lics[128]; + collect_langs(langs, sizeof langs); + collect_builds(builds, sizeof builds); + collect_licenses(lics, sizeof lics); + + (void) printf( + "_mkproj()\n" + "{\n" + " local cur prev\n" + " cur=\"${COMP_WORDS[COMP_CWORD]}\"\n" + " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n" + "\n" + " case \"$prev\" in\n" + " -l|--lang) COMPREPLY=( $(compgen -W \"%s\" -- \"$cur\") ); return ;;\n" + " -b|--build) COMPREPLY=( $(compgen -W \"%s\" -- \"$cur\") ); return ;;\n" + " -k|--kind) COMPREPLY=( $(compgen -W \"exe lib\" -- \"$cur\") ); return ;;\n" + " --license) COMPREPLY=( $(compgen -W \"%s\" -- \"$cur\") ); return ;;\n" + " --completions) COMPREPLY=( $(compgen -W \"bash zsh fish\" -- \"$cur\") ); return ;;\n" + " --cc|--bin|--bin-int|--src|-s|--std) return ;;\n" + " esac\n" + "\n" + " if [[ \"$cur\" == -* ]]; then\n" + " COMPREPLY=( $(compgen -W \"%s\" -- \"$cur\") )\n" + " else\n" + " COMPREPLY=( $(compgen -f -- \"$cur\") )\n" + " fi\n" + "}\n" + "complete -F _mkproj mkproj\n", + langs, builds, lics, mkproj_flags); +} + +static void print_completions_zsh(void) +{ + char langs[128], builds[128], lics[128]; + collect_langs(langs, sizeof langs); + collect_builds(builds, sizeof builds); + collect_licenses(lics, sizeof lics); + + (void) printf( + "#compdef mkproj\n" + "\n" + "_mkproj()\n" + "{\n" + " _arguments -s \\\n" + " '(-l --lang)'{-l,--lang}'[source language]:language:(%s)' \\\n" + " '(-s --std)'{-s,--std}'[language standard]:standard:' \\\n" + " '(-b --build)'{-b,--build}'[build system]:system:(%s)' \\\n" + " '(-k --kind)'{-k,--kind}'[exe or lib]:kind:(exe lib)' \\\n" + " '--cc[compiler]:compiler:' \\\n" + " '--license[add a LICENSE]:license:(%s)' \\\n" + " '--src[source directory]:dir:_files -/' \\\n" + " '--bin[build directory]:dir:_files -/' \\\n" + " '--bin-int[object directory]:dir:_files -/' \\\n" + " '--git[init a git repo and write .gitignore]' \\\n" + " '--no-root[binary in the build dir]' \\\n" + " '--single[single-file project, no object dir]' \\\n" + " '--no-validation[no check target or sanitizers]' \\\n" + " '--docker[add a Dockerfile]' \\\n" + " '--style[add clang-format + editorconfig]' \\\n" + " '--ci[add a GitHub Actions workflow]' \\\n" + " '(-v --version)'{-v,--version}'[print version]' \\\n" + " '(-h --help)'{-h,--help}'[print help]' \\\n" + " '1:name:'\n" + "}\n" + "\n" + "_mkproj \"$@\"\n", + langs, builds, lics); +} + +static void print_completions_fish(void) +{ + char langs[128], builds[128], lics[128]; + collect_langs(langs, sizeof langs); + collect_builds(builds, sizeof builds); + collect_licenses(lics, sizeof lics); + + (void) printf( + "complete -c mkproj -s l -l lang -x -a '%s' -d 'source language'\n" + "complete -c mkproj -s s -l std -x -d 'language standard'\n" + "complete -c mkproj -s b -l build -x -a '%s' -d 'build system'\n" + "complete -c mkproj -s k -l kind -x -a 'exe lib' -d 'exe or lib'\n" + "complete -c mkproj -l cc -x -d 'compiler'\n" + "complete -c mkproj -l license -x -a '%s' -d 'add a LICENSE'\n" + "complete -c mkproj -l src -x -a '(__fish_complete_directories)' -d 'source directory'\n" + "complete -c mkproj -l bin -x -a '(__fish_complete_directories)' -d 'build directory'\n" + "complete -c mkproj -l bin-int -x -a '(__fish_complete_directories)' -d 'object directory'\n" + "complete -c mkproj -l git -d 'init a git repo and write .gitignore'\n" + "complete -c mkproj -l no-root -d 'binary in the build dir'\n" + "complete -c mkproj -l single -d 'single-file project, no object dir'\n" + "complete -c mkproj -l no-validation -d 'no check target or sanitizers'\n" + "complete -c mkproj -l docker -d 'add a Dockerfile'\n" + "complete -c mkproj -l style -d 'add clang-format + editorconfig'\n" + "complete -c mkproj -l ci -d 'add a GitHub Actions workflow'\n" + "complete -c mkproj -s v -l version -d 'print version'\n" + "complete -c mkproj -s h -l help -d 'print help'\n", + langs, builds, lics); } static int find_language(const char* key, enum Language* out) @@ -373,6 +552,36 @@ static const char* toolset_of(const char* compiler) return "clang"; } +/* the build dir ends up in a "rm -rf" clean rule, so it must stay inside the + * project: reject empty, absolute and ~ paths, any .. component, and paths + * that resolve to the project root itself (., ./, ./., ...). */ +static int bin_dir_safe(const char* dir) +{ + if (dir[0] == '\0' || dir[0] == '/' || dir[0] == '~') + return 0; + + int has_real = 0; + + for (const char* p = dir; *p != '\0'; ) + { + const char* start = p; + while (*p != '\0' && *p != '/') + p++; + + size_t len = (size_t) (p - start); + + if (len == 2 && start[0] == '.' && start[1] == '.') + return 0; + if (len != 0 && !(len == 1 && start[0] == '.')) + has_real = 1; + + if (*p == '/') + p++; + } + + return has_real; +} + /* * parse argv into config. returns 0 on success, a negative value when the * program should exit successfully (help/version), or a positive value on a @@ -388,6 +597,7 @@ static int parse_args(int argc, char** argv, struct Config* config) const char* license_arg = getenv("MKPROJ_LICENSE"); const char* bin_arg = NULL; const char* bin_int_arg = NULL; + const char* src_arg = NULL; const char* name = NULL; for (int i = 1; i < argc; i++) @@ -406,33 +616,70 @@ static int parse_args(int argc, char** argv, struct Config* config) return -1; } - if (strcmp(arg, "--no-git") == 0) + if (strcmp(arg, "--completions") == 0) { - config->use_git = 0; + if (i + 1 >= argc) + { + error("missing argument for", arg); + return 1; + } + + const char* shell = argv[++i]; + + if (strcmp(shell, "bash") == 0) + print_completions_bash(); + else if (strcmp(shell, "zsh") == 0) + print_completions_zsh(); + else if (strcmp(shell, "fish") == 0) + print_completions_fish(); + else + { + error("unknown shell (want bash, zsh or fish)", shell); + return 1; + } + + return -1; + } + + if (strcmp(arg, "--git") == 0) + { + config->use_git = 1; continue; } - if (strcmp(arg, "--intermediate") == 0) + if (strcmp(arg, "--no-root") == 0) { - config->use_intermediate = 1; + config->use_root = 0; continue; } - if (strcmp(arg, "--no-clangd") == 0) + if (strcmp(arg, "--docker") == 0) { - config->use_clangd = 0; + config->use_docker = 1; continue; } - if (strcmp(arg, "--flat") == 0) + if (strcmp(arg, "--style") == 0) { - config->use_flat = 1; + config->use_style = 1; continue; } - if (strcmp(arg, "--docker") == 0) + if (strcmp(arg, "--ci") == 0) { - config->use_docker = 1; + config->use_ci = 1; + continue; + } + + if (strcmp(arg, "--single") == 0) + { + config->use_single = 1; + continue; + } + + if (strcmp(arg, "--no-validation") == 0) + { + config->use_validation = 0; continue; } @@ -495,6 +742,14 @@ static int parse_args(int argc, char** argv, struct Config* config) continue; } + matched = opt_value(arg, "--src", "--src", argc, argv, &i, &src_arg); + if (matched != 0) + { + if (matched < 0) { error("missing argument for", arg); return 1; } + config->use_flat = 0; + continue; + } + if (arg[0] == '-' && arg[1] != '\0') { error("unknown option", arg); @@ -516,6 +771,30 @@ static int parse_args(int argc, char** argv, struct Config* config) return 1; } + /* "." means generate into the current directory, taking the project name + * from its basename instead of creating a subdirectory. */ + static char here_name[PATH_MAX]; + + if (strcmp(name, ".") == 0) + { + char cwd[PATH_MAX]; + + if (getcwd(cwd, sizeof cwd) == NULL) + { + error("cannot determine the current directory", NULL); + return 1; + } + + const char* base = strrchr(cwd, '/'); + (void) snprintf(here_name, sizeof here_name, "%s", base != NULL ? base + 1 : cwd); + name = here_name; + config->dir = "."; + } + else + { + config->dir = name; + } + const char* name_error = validate_name(name); if (name_error != NULL) @@ -525,6 +804,7 @@ static int parse_args(int argc, char** argv, struct Config* config) } config->name = name; + config->src_dir = src_arg != NULL ? src_arg : "src"; if (lang_arg != NULL && !find_language(lang_arg, &config->language)) { @@ -538,11 +818,11 @@ static int parse_args(int argc, char** argv, struct Config* config) return 1; } - if (config->language == language_nasm) + if (!is_c_family(config->language)) { if (std_arg != NULL) { - error("nasm has no language standard", std_arg); + error("this language has no language standard", languages[config->language].name); return 1; } @@ -571,9 +851,11 @@ static int parse_args(int argc, char** argv, struct Config* config) config->kind = kind_exe; else if (strcmp(kind_arg, "lib") == 0) config->kind = kind_lib; + else if (strcmp(kind_arg, "shared") == 0) + config->kind = kind_shared; else { - error("unknown kind (want exe or lib)", kind_arg); + error("unknown kind (want exe, lib or shared)", kind_arg); return 1; } } @@ -588,19 +870,26 @@ static int parse_args(int argc, char** argv, struct Config* config) && config->build != build_make && config->build != build_premake) { - error("--intermediate/--bin-int only apply to make and premake", build_systems[config->build].key); + error("--bin-int only applies to make and premake", build_systems[config->build].key); return 1; } - if (config->language == language_nasm && config->build != build_make) + if (config->use_intermediate + && (config->language == language_swift || config->language == language_crust)) + { + error("--bin-int does not apply to", languages[config->language].name); + return 1; + } + + if (!is_c_family(config->language) && config->build != build_make) { - error("nasm projects only support make", build_systems[config->build].key); + error("this language only supports make", languages[config->language].name); return 1; } - if (config->language == language_nasm && config->kind == kind_lib) + if (!is_c_family(config->language) && config->kind != kind_exe) { - error("nasm projects only support executables", NULL); + error("this language only supports executables", languages[config->language].name); return 1; } @@ -622,6 +911,48 @@ static int parse_args(int argc, char** argv, struct Config* config) return 1; } + if (config->use_docker && config->kind != kind_exe) + { + error("--docker needs an executable, not a library", NULL); + return 1; + } + + if (config->use_style && !is_c_family(config->language)) + { + error("--style (clang-format) supports only c, cpp and objc", languages[config->language].name); + return 1; + } + + if (config->use_single && config->build != build_make) + { + error("--single only applies to make", build_systems[config->build].key); + return 1; + } + + if (config->use_single && !is_c_family(config->language)) + { + error("--single supports only c, cpp and objc", languages[config->language].name); + return 1; + } + + if (config->use_single && config->kind != kind_exe) + { + error("--single needs an executable, not a library", NULL); + return 1; + } + + if (bin_arg != NULL && !bin_dir_safe(bin_arg)) + { + error("--bin must be a path inside the project, not '.', '..' or absolute", bin_arg); + return 1; + } + + if (bin_int_arg != NULL && !bin_dir_safe(bin_int_arg)) + { + error("--bin-int must be a path inside the project, not '.', '..' or absolute", bin_int_arg); + return 1; + } + config->bin_dir = bin_arg != NULL ? bin_arg : build_systems[config->build].default_bin; config->bin_int_dir = bin_int_arg != NULL ? bin_int_arg : build_systems[config->build].default_bin_int; @@ -718,14 +1049,25 @@ static const char* main_source(const struct Config* config) "msg: db \"type shi\", 10, 0\n"; } + if (config->language == language_swift) + return "print(\"type shi\")\n"; + + if (config->language == language_crust) + return + "fn main()\n" + "{\n" + " println!(\"type shi\");\n" + "}\n"; + if (config->language == language_objc) return "#import <Foundation/Foundation.h>\n" "\n" "int main(void)\n" "{\n" - " @autoreleasepool {\n" - " (void) NSLog(@\"type shi\");\n" + " @autoreleasepool\n" + " {\n" + " (void)NSLog(@\"type shi\");\n" " }\n" "}\n"; @@ -765,7 +1107,7 @@ static const char* main_source(const struct Config* config) "\n" "int main(void)\n" "{\n" - " (void) puts(\"type shi\");\n" + " (void)puts(\"type shi\");\n" "}\n"; } @@ -835,21 +1177,6 @@ static size_t base_flags(const struct Config* config, const char* flags[]) return count; } -/* clangd reads compile_flags.txt (one flag per line) to know how to parse the - * project, so the editor works before any build has run. */ -static void write_compile_flags(FILE* out, const struct Config* config) -{ - const char* warnings[10]; - size_t count = base_warnings(config, warnings); - - (void) fprintf(out, "-std=%s\n", config->standard); - - for (size_t i = 0; i < count; i++) - (void) fprintf(out, "%s\n", warnings[i]); - - (void) fprintf(out, "%s\n", config->use_flat ? "-I." : "-Isrc"); -} - static const char* cmake_language(enum Language language) { if (language == language_cpp) @@ -867,13 +1194,25 @@ static void write_makefile_nasm(FILE* out, const struct Config* config) { const char* obj_dir = config->use_intermediate ? config->bin_int_dir : config->bin_dir; - char clean[2 * PATH_MAX + 16]; + char clean[3 * PATH_MAX + 16]; if (config->use_intermediate) (void) snprintf(clean, sizeof clean, "rm -rf %s %s", config->bin_dir, config->bin_int_dir); else (void) snprintf(clean, sizeof clean, "rm -rf %s", config->bin_dir); + if (config->use_root) + { + size_t n = strlen(clean); + (void) snprintf(clean + n, sizeof clean - n, " %s", config->name); + } + + char target[2 * PATH_MAX]; + if (config->use_root) + (void) snprintf(target, sizeof target, "%s", config->name); + else + (void) snprintf(target, sizeof target, "%s/%s", config->bin_dir, config->name); + const char* format = config->use_docker ? "elf64" : "macho64"; const char* ld = config->use_docker ? "ld" : "cc"; const char* ldflags = config->use_docker ? "" : "-arch x86_64"; @@ -892,13 +1231,14 @@ static void write_makefile_nasm(FILE* out, const struct Config* config) "ASFLAGS += -g -F dwarf\n" "endif\n" "\n" - "TARGET := %s/%s\n", - ld, format, ldflags, config->bin_dir, config->name); + "TARGET := %s\n", + ld, format, ldflags, target); if (config->use_flat) (void) fprintf(out, "SRC := $(wildcard *.asm)\nOBJ := $(SRC:%%=%s/%%.o)\n", obj_dir); else - (void) fprintf(out, "SRC := $(shell find src -name '*.asm')\nOBJ := $(SRC:src/%%=%s/%%.o)\n", obj_dir); + (void) fprintf(out, "SRC := $(shell find %s -name '*.asm')\nOBJ := $(SRC:%s/%%=%s/%%.o)\n", + config->src_dir, config->src_dir, obj_dir); (void) fprintf( out, @@ -913,17 +1253,151 @@ static void write_makefile_nasm(FILE* out, const struct Config* config) if (config->use_flat) (void) fprintf(out, "%s/%%.asm.o: %%.asm\n", obj_dir); else - (void) fprintf(out, "%s/%%.asm.o: src/%%.asm\n", obj_dir); + (void) fprintf(out, "%s/%%.asm.o: %s/%%.asm\n", obj_dir, config->src_dir); (void) fprintf( out, "\t@mkdir -p $(@D)\n" "\t$(AS) $(ASFLAGS) $< -o $@\n" "\n" + "run: $(TARGET)\n" + "\t./$(TARGET)\n" + "\n" + "clean:\n" + "\t%s\n" + "\n" + ".PHONY: all run clean\n", + clean); +} + +/* swift compiles the whole module in one shot: no per-file objects, no -std, + * no warning flags. asan is available through swiftc's own -sanitize. */ +static void write_makefile_swift(FILE* out, const struct Config* config) +{ + char target[2 * PATH_MAX]; + if (config->use_root) + (void) snprintf(target, sizeof target, "%s", config->name); + else + (void) snprintf(target, sizeof target, "%s/%s", config->bin_dir, config->name); + + char clean[2 * PATH_MAX + 16]; + if (config->use_root) + (void) snprintf(clean, sizeof clean, "rm -rf %s %s", config->bin_dir, config->name); + else + (void) snprintf(clean, sizeof clean, "rm -rf %s", config->bin_dir); + + (void) fprintf(out, "SWIFTC := %s\n\nDEBUG ?= 1\n", config->compiler); + + if (config->use_validation) + (void) fprintf(out, "SANITIZE ?= 1\n"); + + (void) fprintf( + out, + "\n" + "SWIFTFLAGS :=\n" + "\n" + "ifeq ($(DEBUG),1)\n" + "SWIFTFLAGS += -g -Onone\n" + "else\n" + "SWIFTFLAGS += -O\n" + "endif\n"); + + if (config->use_validation) + (void) fprintf( + out, + "\n" + "ifeq ($(SANITIZE),1)\n" + "SWIFTFLAGS += -sanitize=address\n" + "endif\n"); + + (void) fprintf(out, "\nTARGET := %s\n", target); + + if (config->use_flat) + (void) fprintf(out, "SRC := $(wildcard *.swift)\n"); + else + (void) fprintf(out, "SRC := $(shell find %s -name '*.swift')\n", config->src_dir); + + (void) fprintf( + out, + "\n" + "all: $(TARGET)\n" + "\n" + "$(TARGET): $(SRC)\n" + "\t@mkdir -p $(@D)\n" + "\t$(SWIFTC) $(SWIFTFLAGS) $^ -o $@\n" + "\n" + "run: $(TARGET)\n" + "\t./$(TARGET)\n" + "\n" "clean:\n" "\t%s\n" "\n" - ".PHONY: all clean\n", + ".PHONY: all run clean\n", + clean); +} + +/* crust is rust driven straight through rustc from the crate root, no cargo. + * rustc pulls in the other modules itself, so only the crate root is compiled; + * $(SRC) exists just to retrigger the build when any source changes. */ +static void write_makefile_crust(FILE* out, const struct Config* config) +{ + char target[2 * PATH_MAX]; + if (config->use_root) + (void) snprintf(target, sizeof target, "%s", config->name); + else + (void) snprintf(target, sizeof target, "%s/%s", config->bin_dir, config->name); + + char clean[2 * PATH_MAX + 16]; + if (config->use_root) + (void) snprintf(clean, sizeof clean, "rm -rf %s %s", config->bin_dir, config->name); + else + (void) snprintf(clean, sizeof clean, "rm -rf %s", config->bin_dir); + + char crate[PATH_MAX]; + if (config->use_flat) + (void) snprintf(crate, sizeof crate, "main.rs"); + else + (void) snprintf(crate, sizeof crate, "%s/main.rs", config->src_dir); + + (void) fprintf( + out, + "RUSTC := %s\n" + "\n" + "DEBUG ?= 1\n" + "\n" + "RUSTFLAGS :=\n" + "\n" + "ifeq ($(DEBUG),1)\n" + "RUSTFLAGS += -g\n" + "else\n" + "RUSTFLAGS += -O\n" + "endif\n" + "\n" + "TARGET := %s\n" + "CRATE := %s\n", + config->compiler, target, crate); + + if (config->use_flat) + (void) fprintf(out, "SRC := $(wildcard *.rs)\n"); + else + (void) fprintf(out, "SRC := $(shell find %s -name '*.rs')\n", config->src_dir); + + (void) fprintf( + out, + "\n" + "all: $(TARGET)\n" + "\n" + "$(TARGET): $(SRC)\n" + "\t@mkdir -p $(@D)\n" + "\t$(RUSTC) $(RUSTFLAGS) $(CRATE) -o $@\n" + "\n" + "run: $(TARGET)\n" + "\t./$(TARGET)\n" + "\n" + "clean:\n" + "\t%s\n" + "\n" + ".PHONY: all run clean\n", clean); } @@ -935,23 +1409,60 @@ static void write_makefile(FILE* out, const struct Config* config) return; } + if (config->language == language_swift) + { + write_makefile_swift(out, config); + return; + } + + if (config->language == language_crust) + { + write_makefile_crust(out, config); + return; + } + const struct Language_Info* lang = &languages[config->language]; const char* ext = lang->extension; const char* obj_dir = config->use_intermediate ? config->bin_int_dir : config->bin_dir; - char clean[2 * PATH_MAX + 16]; + char target_name[PATH_MAX]; + if (config->kind == kind_lib) + (void) snprintf(target_name, sizeof target_name, "lib%s.a", config->name); + else if (config->kind == kind_shared) + (void) snprintf(target_name, sizeof target_name, "lib%s.$(SOEXT)", config->name); + else + (void) snprintf(target_name, sizeof target_name, "%s", config->name); - if (config->use_intermediate) + char target[2 * PATH_MAX]; + if (config->use_single || config->use_root) + (void) snprintf(target, sizeof target, "%s", target_name); + else + (void) snprintf(target, sizeof target, "%s/%s", config->bin_dir, target_name); + + char clean[3 * PATH_MAX + 16]; + + if (config->use_single) + (void) snprintf(clean, sizeof clean, "rm -f %s", target_name); + else if (config->use_intermediate) (void) snprintf(clean, sizeof clean, "rm -rf %s %s", config->bin_dir, config->bin_int_dir); else (void) snprintf(clean, sizeof clean, "rm -rf %s", config->bin_dir); + if (!config->use_single && config->use_root) + { + size_t n = strlen(clean); + (void) snprintf(clean + n, sizeof clean - n, " %s", target_name); + } + const char* flags[16]; size_t count = 0; + char include[PATH_MAX + 2]; + (void) snprintf(include, sizeof include, "-I%s", config->use_flat ? "." : config->src_dir); + flags[count++] = "-std=$(STD)"; count += base_warnings(config, flags + count); - flags[count++] = config->use_flat ? "-I." : "-Isrc"; + flags[count++] = include; int width = 0; @@ -959,7 +1470,14 @@ static void write_makefile(FILE* out, const struct Config* config) if ((int) strlen(flags[i]) > width) width = (int) strlen(flags[i]); - (void) fprintf(out, "CC := %s\nSTD := %s\n\nDEBUG ?= 1\nSANITIZE ?= 1\n\nCFLAGS := ", config->compiler, config->standard); + int sanitize = config->use_validation && config->kind == kind_exe; + + (void) fprintf(out, "CC := %s\nSTD := %s\n\nDEBUG ?= 1\n", config->compiler, config->standard); + + if (sanitize) + (void) fprintf(out, "SANITIZE ?= 1\n"); + + (void) fprintf(out, "\nCFLAGS := "); for (size_t i = 0; i < count; i++) { @@ -979,60 +1497,110 @@ static void write_makefile(FILE* out, const struct Config* config) "CFLAGS += -g -fno-omit-frame-pointer\n" "else\n" "CFLAGS += -O2\n" - "endif\n" - "\n" - "ifeq ($(SANITIZE),1)\n" - "CFLAGS += -fsanitize=address,undefined\n" - "LDFLAGS += -fsanitize=address,undefined\n" "endif\n"); + if (sanitize) + (void) fprintf( + out, + "\n" + "ifeq ($(SANITIZE),1)\n" + "CFLAGS += -fsanitize=address,undefined\n" + "LDFLAGS += -fsanitize=address,undefined\n" + "endif\n"); + if (config->language == language_objc && config->kind == kind_exe) (void) fprintf(out, "\nLDFLAGS += -framework Foundation\n"); - if (config->kind == kind_lib) - (void) fprintf(out, "\nTARGET := %s/lib%s.a\n", config->bin_dir, config->name); - else - (void) fprintf(out, "\nTARGET := %s/%s\n", config->bin_dir, config->name); + if (config->kind == kind_shared) + (void) fprintf( + out, + "\n" + "CFLAGS += -fPIC\n" + "\n" + "UNAME := $(shell uname)\n" + "ifeq ($(UNAME),Darwin)\n" + "SOEXT := dylib\n" + "SOFLAGS := -dynamiclib\n" + "else\n" + "SOEXT := so\n" + "SOFLAGS := -shared\n" + "endif\n"); - if (config->use_flat) - (void) fprintf(out, "SRC := $(wildcard *.%s)\nOBJ := $(SRC:%%=%s/%%.o)\n", ext, obj_dir); - else - (void) fprintf(out, "SRC := $(shell find src -name '*.%s')\nOBJ := $(SRC:src/%%=%s/%%.o)\n", ext, obj_dir); + (void) fprintf(out, "\nTARGET := %s\n", target); - (void) fprintf( - out, - "\n" - "all: $(TARGET)\n" - "\n" - "$(TARGET): $(OBJ)\n" - "\t@mkdir -p $(@D)\n"); + if (config->use_single) + { + if (config->use_flat) + (void) fprintf(out, "SRC := $(wildcard *.%s)\n", ext); + else + (void) fprintf(out, "SRC := $(shell find %s -name '*.%s')\n", config->src_dir, ext); - if (config->kind == kind_lib) - (void) fprintf(out, "\t$(AR) rcs $@ $^\n\n"); + (void) fprintf( + out, + "\n" + "all: $(TARGET)\n" + "\n" + "$(TARGET): $(SRC)\n" + "\t$(CC) $(CFLAGS) $(SRC) -o $@ $(LDFLAGS)\n" + "\n"); + } else - (void) fprintf(out, "\t$(CC) $^ -o $@ $(LDFLAGS)\n\n"); + { + if (config->use_flat) + (void) fprintf(out, "SRC := $(wildcard *.%s)\nOBJ := $(SRC:%%=%s/%%.o)\n", ext, obj_dir); + else + (void) fprintf(out, "SRC := $(shell find %s -name '*.%s')\nOBJ := $(SRC:%s/%%=%s/%%.o)\n", + config->src_dir, ext, config->src_dir, obj_dir); - if (config->use_flat) - (void) fprintf(out, "%s/%%.%s.o: %%.%s\n", obj_dir, ext, ext); - else - (void) fprintf(out, "%s/%%.%s.o: src/%%.%s\n", obj_dir, ext, ext); + (void) fprintf( + out, + "\n" + "all: $(TARGET)\n" + "\n" + "$(TARGET): $(OBJ)\n" + "\t@mkdir -p $(@D)\n"); + + if (config->kind == kind_lib) + (void) fprintf(out, "\t$(AR) rcs $@ $^\n\n"); + else if (config->kind == kind_shared) + (void) fprintf(out, "\t$(CC) $(SOFLAGS) $^ -o $@ $(LDFLAGS)\n\n"); + else + (void) fprintf(out, "\t$(CC) $^ -o $@ $(LDFLAGS)\n\n"); + + if (config->use_flat) + (void) fprintf(out, "%s/%%.%s.o: %%.%s\n", obj_dir, ext, ext); + else + (void) fprintf(out, "%s/%%.%s.o: %s/%%.%s\n", obj_dir, ext, config->src_dir, ext); + + (void) fprintf( + out, + "\t@mkdir -p $(@D)\n" + "\t$(CC) $(CFLAGS) -c $< -o $@\n" + "\n"); + } + + if (config->use_validation) + (void) fprintf( + out, + "check:\n" + "\tbear -- make clean all\n" + "\tcppcheck --enable=warning,style,performance,portability \\\n" + "\t\t --project=compile_commands.json \\\n" + "\t\t --error-exitcode=1\n" + "\n"); + + if (config->kind == kind_exe) + (void) fprintf(out, "run: $(TARGET)\n\t./$(TARGET)\n\n"); (void) fprintf( out, - "\t@mkdir -p $(@D)\n" - "\t$(CC) $(CFLAGS) -c $< -o $@\n" - "\n" - "check:\n" - "\tbear -- make clean all\n" - "\tcppcheck --enable=warning,style,performance,portability \\\n" - "\t\t --project=compile_commands.json \\\n" - "\t\t --error-exitcode=1\n" - "\n" "clean:\n" "\t%s\n" "\n" - ".PHONY: all check clean\n", - clean); + ".PHONY: all%s clean%s\n", + clean, + config->use_validation ? " check" : "", + config->kind == kind_exe ? " run" : ""); } static void write_cmake(FILE* out, const struct Config* config) @@ -1042,23 +1610,29 @@ static void write_cmake(FILE* out, const struct Config* config) const char* flags[10]; size_t count = base_flags(config, flags); + int sanitize = config->kind == kind_exe; + (void) fprintf( out, "cmake_minimum_required(VERSION 3.16)\n" "\n" "project(%s %s)\n" - "\n" - "option(SANITIZE \"build with asan + ubsan\" ON)\n" "\n", name, cmake_language(config->language)); + if (sanitize) + (void) fprintf(out, "option(SANITIZE \"build with asan + ubsan\" ON)\n\n"); + if (config->use_flat) (void) fprintf(out, "file(GLOB SOURCES CONFIGURE_DEPENDS *.%s)\n", ext); else - (void) fprintf(out, "file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS src/*.%s)\n", ext); + (void) fprintf(out, "file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS %s/*.%s)\n", + config->src_dir, ext); if (config->kind == kind_lib) (void) fprintf(out, "\nadd_library(%s STATIC ${SOURCES})\n", name); + else if (config->kind == kind_shared) + (void) fprintf(out, "\nadd_library(%s SHARED ${SOURCES})\n", name); else (void) fprintf(out, "\nadd_executable(%s ${SOURCES})\n", name); @@ -1068,7 +1642,7 @@ static void write_cmake(FILE* out, const struct Config* config) "target_include_directories(%s PRIVATE %s)\n" "\n" "target_compile_options(%s PRIVATE\n", - name, config->use_flat ? "." : "src", name); + name, config->use_flat ? "." : config->src_dir, name); for (size_t i = 0; i < count; i++) (void) fprintf(out, " %s\n", flags[i]); @@ -1082,14 +1656,15 @@ static void write_cmake(FILE* out, const struct Config* config) "target_link_libraries(%s PRIVATE \"-framework Foundation\")\n", name); - (void) fprintf( - out, - "\n" - "if(SANITIZE)\n" - " target_compile_options(%s PRIVATE -fsanitize=address,undefined)\n" - " target_link_options(%s PRIVATE -fsanitize=address,undefined)\n" - "endif()\n", - name, name); + if (sanitize) + (void) fprintf( + out, + "\n" + "if(SANITIZE)\n" + " target_compile_options(%s PRIVATE -fsanitize=address,undefined)\n" + " target_link_options(%s PRIVATE -fsanitize=address,undefined)\n" + "endif()\n", + name, name); } static const char* meson_language(enum Language language) @@ -1111,15 +1686,18 @@ static void write_meson(FILE* out, const struct Config* config) const char* flags[10]; size_t count = base_flags(config, flags); - (void) fprintf( - out, - "project('%s', '%s',\n" - " default_options: [\n" - " 'b_sanitize=address,undefined',\n" - " 'b_lundef=false',\n" - " ])\n" - "\n", - name, lang); + if (config->kind == kind_exe) + (void) fprintf( + out, + "project('%s', '%s',\n" + " default_options: [\n" + " 'b_sanitize=address,undefined',\n" + " 'b_lundef=false',\n" + " ])\n" + "\n", + name, lang); + else + (void) fprintf(out, "project('%s', '%s')\n\n", name, lang); if (config->use_flat) (void) fprintf( @@ -1133,10 +1711,10 @@ static void write_meson(FILE* out, const struct Config* config) (void) fprintf( out, "sources = run_command(\n" - " 'find', 'src', '-name', '*.%s',\n" + " 'find', '%s', '-name', '*.%s',\n" " check: true).stdout().strip().split('\\n')\n" "\n", - ext); + config->src_dir, ext); (void) fprintf(out, "add_project_arguments(\n"); @@ -1153,13 +1731,15 @@ static void write_meson(FILE* out, const struct Config* config) int link_foundation = config->language == language_objc && config->kind == kind_exe; if (!config->use_flat) - (void) fprintf(out, "inc = include_directories('src')\n\n"); + (void) fprintf(out, "inc = include_directories('%s')\n\n", config->src_dir); if (link_foundation) (void) fprintf(out, "foundation = dependency('appleframeworks', modules: ['Foundation'])\n\n"); if (config->kind == kind_lib) (void) fprintf(out, "static_library('%s', sources", name); + else if (config->kind == kind_shared) + (void) fprintf(out, "shared_library('%s', sources", name); else (void) fprintf(out, "executable('%s', sources", name); @@ -1188,6 +1768,10 @@ static void write_premake(FILE* out, const struct Config* config) const char* flags[10]; size_t count = base_flags(config, flags); + const char* pkind = config->kind == kind_lib ? "StaticLib" + : config->kind == kind_shared ? "SharedLib" + : "ConsoleApp"; + (void) fprintf( out, "workspace \"%s\"\n" @@ -1202,14 +1786,15 @@ static void write_premake(FILE* out, const struct Config* config) " targetname \"%s\"\n" " objdir \"%s\"\n" "\n", - name, name, config->kind == kind_lib ? "StaticLib" : "ConsoleApp", + name, name, pkind, premake_language(config->language), - toolset_of(config->compiler), config->bin_dir, name, obj_dir); + toolset_of(config->compiler), config->use_root ? "." : config->bin_dir, name, obj_dir); if (config->use_flat) (void) fprintf(out, " files { \"*.%s\" }\n includedirs { \".\" }\n", ext); else - (void) fprintf(out, " files { \"src/**.%s\" }\n includedirs { \"src\" }\n", ext); + (void) fprintf(out, " files { \"%s/**.%s\" }\n includedirs { \"%s\" }\n", + config->src_dir, ext, config->src_dir); (void) fprintf(out, "\n buildoptions {\n"); @@ -1260,25 +1845,29 @@ static void write_xmake(FILE* out, const struct Config* config) "target(\"%s\")\n" " set_kind(\"%s\")\n", config->bin_dir, toolset_of(config->compiler), name, - config->kind == kind_lib ? "static" : "binary"); + config->kind == kind_lib ? "static" : config->kind == kind_shared ? "shared" : "binary"); if (config->use_flat) (void) fprintf(out, " add_files(\"*.%s\")\n add_includedirs(\".\")\n", ext); else - (void) fprintf(out, " add_files(\"src/**.%s\")\n add_includedirs(\"src\")\n", ext); + (void) fprintf(out, " add_files(\"%s/**.%s\")\n add_includedirs(\"%s\")\n", + config->src_dir, ext, config->src_dir); (void) fprintf(out, "\n %s(\n", flag_fn); for (size_t i = 0; i < count; i++) (void) fprintf(out, " \"%s\",\n", flags[i]); - (void) fprintf( - out, - " \"-std=%s\",\n" - " \"-fsanitize=address,undefined\")\n" - "\n" - " add_ldflags(\"-fsanitize=address,undefined\")\n", - config->standard); + if (config->kind == kind_exe) + (void) fprintf( + out, + " \"-std=%s\",\n" + " \"-fsanitize=address,undefined\")\n" + "\n" + " add_ldflags(\"-fsanitize=address,undefined\")\n", + config->standard); + else + (void) fprintf(out, " \"-std=%s\")\n", config->standard); if (config->language == language_objc && config->kind == kind_exe) (void) fputs("\n add_frameworks(\"Foundation\")\n", out); @@ -1430,10 +2019,97 @@ static void write_license(FILE* out, const struct Config* config) } } +/* a minimal GitHub Actions workflow that builds the project. objc, swift and + * nasm run on macOS; everything else on Linux. build only: the C 'make check' + * needs bear and cppcheck, which we do not drag into CI. */ +static void write_ci(FILE* out, const struct Config* config) +{ + int apple = config->language == language_objc + || config->language == language_swift + || config->language == language_nasm; + const char* runner = apple ? "macos-latest" : "ubuntu-latest"; + + (void) fprintf( + out, + "name: ci\n" + "\n" + "on:\n" + " push:\n" + " pull_request:\n" + "\n" + "jobs:\n" + " build:\n" + " runs-on: %s\n" + " steps:\n" + " - uses: actions/checkout@v4\n", + runner); + + /* extra toolchain the runner does not already ship. */ + if (config->language == language_nasm) + (void) fputs(" - run: brew install nasm\n", out); + + switch (config->build) + { + case build_meson: + if (apple) + (void) fputs(" - run: brew install meson ninja\n", out); + else + (void) fputs(" - run: sudo apt-get update && sudo apt-get install -y meson ninja-build\n", out); + break; + + case build_premake: + (void) fputs(" - uses: abel0b/setup-premake@v2.4\n", out); + break; + + case build_xmake: + (void) fputs(" - uses: xmake-io/github-action-setup-xmake@v1\n", out); + break; + + default: + break; + } + + switch (config->build) + { + case build_cmake: + (void) fprintf(out, " - run: cmake -B %s && cmake --build %s\n", + config->bin_dir, config->bin_dir); + break; + + case build_meson: + (void) fprintf(out, " - run: meson setup %s && meson compile -C %s\n", + config->bin_dir, config->bin_dir); + break; + + case build_premake: + (void) fputs(" - run: premake5 gmake2 && make\n", out); + break; + + case build_xmake: + (void) fputs(" - run: xmake -y\n", out); + break; + + default: + (void) fputs(" - run: make\n", out); + break; + } +} + /* a single-stage Dockerfile that installs the toolchain, builds a release * (no sanitizers), and runs the binary. c/cpp with make, cmake or meson. */ static void write_dockerfile(FILE* out, const struct Config* config) { + /* make and premake honour --root and drop the binary at the project root; + * cmake and meson always keep it inside their build directory. */ + int at_root = config->use_root + && (config->build == build_make || config->build == build_premake); + char run_path[2 * PATH_MAX]; + + if (at_root) + (void) snprintf(run_path, sizeof run_path, "./%s", config->name); + else + (void) snprintf(run_path, sizeof run_path, "./%s/%s", config->bin_dir, config->name); + if (config->language == language_nasm) { (void) fprintf( @@ -1449,8 +2125,8 @@ static void write_dockerfile(FILE* out, const struct Config* config) "\n" "RUN make\n" "\n" - "CMD [\"./%s/%s\"]\n", - config->bin_dir, config->name); + "CMD [\"%s\"]\n", + run_path); return; } @@ -1498,8 +2174,8 @@ static void write_dockerfile(FILE* out, const struct Config* config) "\n" "RUN %s\n" "\n" - "CMD [\"./%s/%s\"]\n", - packages, build, config->bin_dir, config->name); + "CMD [\"%s\"]\n", + packages, build, run_path); } static void write_dockerignore(FILE* out, const struct Config* config) @@ -1510,11 +2186,63 @@ static void write_dockerignore(FILE* out, const struct Config* config) (void) fprintf(out, "%s/\n", config->bin_int_dir); } +/* clang-format matching the house style guide (hachem.wtf/blabbing/style-guide). */ +static void write_clang_format(FILE* out, const struct Config* config) +{ + (void) fprintf(out, "Language: %s\n", config->language == language_objc ? "ObjC" : "Cpp"); + + (void) fputs( + "BreakBeforeBraces: Allman\n" + "PointerAlignment: Left\n" + "AllowShortFunctionsOnASingleLine: None\n" + "AllowShortIfStatementsOnASingleLine: Never\n" + "AllowShortLoopsOnASingleLine: false\n" + "AllowShortBlocksOnASingleLine: Never\n" + "SpaceBeforeParens: ControlStatements\n" + "IndentCaseLabels: false\n" + "IndentWidth: 4\n" + "ContinuationIndentWidth: 8\n" + "ColumnLimit: 100\n" + "UseTab: Never\n" + "AlignConsecutiveDeclarations: false\n", + out); +} + +static void write_editorconfig(FILE* out, const struct Config* config) +{ + (void) config; + + (void) fputs( + "root = true\n" + "\n" + "[*]\n" + "charset = utf-8\n" + "end_of_line = lf\n" + "insert_final_newline = true\n" + "trim_trailing_whitespace = true\n" + "\n" + "[*.{c,h,cpp,hpp,cc,m}]\n" + "indent_style = space\n" + "indent_size = 4\n" + "max_line_length = 100\n", + out); +} + static void write_gitignore(FILE* out, const struct Config* config) { (void) fprintf(out, "%s/\n", config->bin_dir); - if (config->use_intermediate && + if (config->use_root) + { + if (config->kind == kind_lib) + (void) fprintf(out, "/lib%s.a\n", config->name); + else if (config->kind == kind_shared) + (void) fprintf(out, "/lib%s.so\n/lib%s.dylib\n", config->name, config->name); + else + (void) fprintf(out, "/%s\n", config->name); + } + + if (config->use_intermediate && (config->build == build_make || config->build == build_premake) && strcmp(config->bin_int_dir, config->bin_dir) != 0) @@ -1526,11 +2254,8 @@ static void write_gitignore(FILE* out, const struct Config* config) if (config->build == build_xmake) (void) fputs(".xmake/\n", out); - if (config->use_clangd && config->language != language_nasm) - (void) fputs("\n" - ".clangd\n" - "compile_commands.json\n", - out); + if (is_c_family(config->language)) + (void) fputs("\ncompile_commands.json\n", out); } /* open path, hand the stream to writer, close. returns 0 on success. */ @@ -1568,6 +2293,17 @@ static int make_dir(const char* path) return -1; } +/* like make_dir but fine with the directory already being there; for nested + * output dirs such as .github/workflows that we do not own outright. */ +static int ensure_dir(const char* path) +{ + if (mkdir(path, 0755) == 0 || errno == EEXIST) + return 0; + + error("cannot create directory", path); + return -1; +} + static void print_next_steps(const struct Config* config) { if (config->standard != NULL) @@ -1582,7 +2318,8 @@ static void print_next_steps(const struct Config* config) languages[config->language].name, build_systems[config->build].name); - (void) printf(" cd %s\n", config->name); + if (strcmp(config->dir, ".") != 0) + (void) printf(" cd %s\n", config->name); if (config->use_docker) { @@ -1596,7 +2333,9 @@ static void print_next_steps(const struct Config* config) { case build_make: (void) printf(" make\n"); - if (config->language != language_nasm) + if (config->kind == kind_exe) + (void) printf(" make run\n"); + else if (is_c_family(config->language)) (void) printf(" make check\n"); break; @@ -1645,24 +2384,33 @@ static int generate_project(const struct Config* config) } const char* ext = languages[config->language].extension; + const char* dir = config->dir; + int in_place = strcmp(dir, ".") == 0; char srcdir[PATH_MAX]; - if (make_dir(config->name) != 0) + /* generating in place: refuse to clobber an existing build file. */ + if (in_place && access(build_file, F_OK) == 0) + { + error("refusing to overwrite existing", build_file); + return -1; + } + + if (!in_place && make_dir(dir) != 0) return -1; if (config->use_flat) { - (void) snprintf(srcdir, sizeof srcdir, "%s", config->name); + (void) snprintf(srcdir, sizeof srcdir, "%s", dir); } else { - (void) snprintf(srcdir, sizeof srcdir, "%s/src", config->name); + (void) snprintf(srcdir, sizeof srcdir, "%s/%s", dir, config->src_dir); if (make_dir(srcdir) != 0) return -1; } - if (config->kind == kind_lib) + if (config->kind != kind_exe) { (void) snprintf(path, sizeof path, "%s/%s.h", srcdir, config->name); @@ -1682,27 +2430,22 @@ static int generate_project(const struct Config* config) return -1; } - (void) snprintf(path, sizeof path, "%s/%s", config->name, build_file); + (void) snprintf(path, sizeof path, "%s/%s", dir, build_file); if (emit(path, config, write_build) != 0) return -1; - (void) snprintf(path, sizeof path, "%s/.gitignore", config->name); - - if (emit(path, config, write_gitignore) != 0) - return -1; - - if (config->use_clangd && config->language != language_nasm) + if (config->use_git) { - (void) snprintf(path, sizeof path, "%s/compile_flags.txt", config->name); + (void) snprintf(path, sizeof path, "%s/.gitignore", dir); - if (emit(path, config, write_compile_flags) != 0) + if (emit(path, config, write_gitignore) != 0) return -1; } if (config->license != license_none) { - (void) snprintf(path, sizeof path, "%s/LICENSE", config->name); + (void) snprintf(path, sizeof path, "%s/LICENSE", dir); if (emit(path, config, write_license) != 0) return -1; @@ -1710,24 +2453,55 @@ static int generate_project(const struct Config* config) if (config->use_docker) { - (void) snprintf(path, sizeof path, "%s/Dockerfile", config->name); + (void) snprintf(path, sizeof path, "%s/Dockerfile", dir); if (emit(path, config, write_dockerfile) != 0) return -1; - (void) snprintf(path, sizeof path, "%s/.dockerignore", config->name); + (void) snprintf(path, sizeof path, "%s/.dockerignore", dir); if (emit(path, config, write_dockerignore) != 0) return -1; } - if (config->use_git && git_init(config->name) != 0) + if (config->use_style) + { + (void) snprintf(path, sizeof path, "%s/.clang-format", dir); + + if (emit(path, config, write_clang_format) != 0) + return -1; + + (void) snprintf(path, sizeof path, "%s/.editorconfig", dir); + + if (emit(path, config, write_editorconfig) != 0) + return -1; + } + + if (config->use_ci) + { + (void) snprintf(path, sizeof path, "%s/.github", dir); + + if (ensure_dir(path) != 0) + return -1; + + (void) snprintf(path, sizeof path, "%s/.github/workflows", dir); + + if (ensure_dir(path) != 0) + return -1; + + (void) snprintf(path, sizeof path, "%s/.github/workflows/ci.yml", dir); + + if (emit(path, config, write_ci) != 0) + return -1; + } + + if (config->use_git && git_init(dir) != 0) (void) fprintf(stderr, "%s: warning: git is not installed\n", mkproj_program); print_next_steps(config); { - char* const argv[] = { "tree", "-a", "--dirsfirst", "-I", ".git", (char*) config->name, NULL }; + char* const argv[] = { "tree", "-a", "--dirsfirst", "-I", ".git", (char*) dir, NULL }; if (run(argv, 0) == 127) (void) puts(" (tree is not installed)"); @@ -1741,6 +2515,8 @@ int main(int argc, char** argv) struct Config config = { .name = NULL, + .dir = NULL, + .src_dir = NULL, .language = language_c, .standard = NULL, .compiler = NULL, @@ -1749,11 +2525,15 @@ int main(int argc, char** argv) .license = license_none, .bin_dir = NULL, .bin_int_dir = NULL, - .use_git = 1, + .use_git = 0, .use_intermediate = 0, - .use_clangd = 1, - .use_flat = 0, + .use_flat = 1, + .use_single = 0, + .use_validation = 1, .use_docker = 0, + .use_root = 1, + .use_style = 0, + .use_ci = 0, }; int status = parse_args(argc, argv, &config); |
