hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/mkproj.c
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-10-13 01:02:42 +0200
committerhachem <im@hachem.wtf>2026-10-13 01:02:42 +0200
commit346d8bc24babeed3a112895ec34aab0074b4a78b (patch)
tree2c476ce32c2b1939fcd7dcc747315b7378eaa02e /mkproj.c
parent0c4716968a6ad336324af15df158db66db3f651f (diff)
feat: refactor defaultsHEADmain
Diffstat (limited to 'mkproj.c')
-rw-r--r--mkproj.c1120
1 files changed, 950 insertions, 170 deletions
diff --git a/mkproj.c b/mkproj.c
index 96d7119..f5cdf91 100644
--- a/mkproj.c
+++ b/mkproj.c
@@ -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);