From bdd84a6926e051d3d1f49f760165d192a8d441ae Mon Sep 17 00:00:00 2001 From: minelogy-dev <72928567+minelogy-dev@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:32:21 +0000 Subject: [PATCH 1/5] fix: repair compiler compatibility and sanitizer failures --- build.c | 25 ++++++++++++++++---- lib/src/args.c | 23 ++++++++++++------- lib/src/build.c | 24 ++------------------ lib/src/compiler/gcc.c | 7 +++--- lib/src/os/unix.c | 44 ++++++++++++++++++++++++++---------- lib/src/strv.c | 6 ++--- src/forge.c | 14 ++++++++---- tests/edge/test_ed_config.c | 6 +++-- tests/edge/test_ed_runtime.c | 13 ++++++++++- tests/edge/test_ed_shell.c | 38 +++++++++++++++++++++++++++++++ tests/sub/test_args.c | 6 ++--- tests/test_archiver.c | 16 ++++++++++--- tests/test_compiler.c | 5 ++-- tests/test_linker.c | 8 ++++++- tests/test_null.c | 14 ++++++++---- tests/test_type.c | 6 +++-- 16 files changed, 178 insertions(+), 77 deletions(-) create mode 100644 tests/edge/test_ed_shell.c diff --git a/build.c b/build.c index a037708..80a30ac 100644 --- a/build.c +++ b/build.c @@ -2,6 +2,23 @@ /* Copyright (C) 2026 The Forge development team */ /* Additional permission under GPLv3+ §7 applies; see LICENSE. */ +/* Keep targets and tests on the compiler selected in build.conf. */ +#ifndef CONF_compiler +#define CONF_compiler GCC +#endif +#ifndef CONF_sanitize +#define CONF_sanitize 0 +#endif +#define TOOLCHAIN set_toolchain(CONF_compiler) +#define LANGUAGE \ + { \ + set_language(C); \ + set_standard("c23"); \ + add_define("_DEFAULT_SOURCE", "1"); \ + if (CONF_sanitize) \ + add(TARGET_OPTIONS, 4, "-g", "-fsanitize=address,undefined", \ + "-fno-sanitize-recover=all", "-fno-omit-frame-pointer"); \ + } #include /* The version has a single source of truth: the version key in @@ -24,7 +41,7 @@ function(build) { parse_args(builtin_args); target("forge") { - set_toolchain(GCC); + set_toolchain(CONF_compiler); add_sources("src/forge.c"); add_sources("src/cli.c"); add_sources("src/parser.c"); @@ -45,7 +62,7 @@ add_define("FORGE_VERSION", "\"" FORGE_VERSION "\""); return -1; } target("main") { - set_toolchain(GCC); + set_toolchain(CONF_compiler); set_type(FORGE_STATIC_LIB); /* A static library must NOT carry -flto: its objects are linked by whichever compiler builds the consumer (e.g. clang, whose driver @@ -63,7 +80,7 @@ add_define("FORGE_VERSION", "\"" FORGE_VERSION "\""); return -1; } target("forge") { - set_toolchain(GCC); + set_toolchain(CONF_compiler); set_type(FORGE_SHARED_LIB); add_sources_r("lib/src"); add_include_path("lib/include"); @@ -377,4 +394,4 @@ function(gen_deb) { fprintf(stderr, "forge: gen_deb is only supported on Linux\n"); return -1; #endif -} \ No newline at end of file +} diff --git a/lib/src/args.c b/lib/src/args.c index dabda5f..cceb860 100644 --- a/lib/src/args.c +++ b/lib/src/args.c @@ -67,7 +67,6 @@ int forge_parse_args(int argc, char **argv, opt_t *opts, int opts_count) { if (argv[i][1] == '-') { // long if (argv[i][2] != '\0') { char *eq = strchr(argv[i], '='); - size_t name_len = eq ? (size_t)(eq - argv[i]) : strlen(argv[i]); char *value = eq ? eq + 1 : NULL; if (eq) *eq = '\0'; @@ -99,13 +98,17 @@ int forge_parse_args(int argc, char **argv, opt_t *opts, int opts_count) { opt_t *target = &opts[match_idx]; if (target->argv == NULL) { - target->argv = malloc(8 * sizeof(size_t)); - memset(target->argv, -1, sizeof(size_t) * 8); + target->argv = malloc(8 * sizeof(*target->argv)); + if (!target->argv) { + fprintf(stderr, "%s: malloc failed\n", argv[0]); + exit(EXIT_FAILURE); + } + memset(target->argv, -1, sizeof(*target->argv) * 8); target->argv[7] = NULL; } if (target->argv[target->argc] == NULL) { char **ptr = - realloc(target->argv, (target->argc * 2 + 2) * sizeof(size_t)); + realloc(target->argv, (target->argc * 2 + 2) * sizeof(*target->argv)); if (ptr == NULL) { fprintf(stderr, "%s: realloc failed\n", argv[0]); exit(EXIT_FAILURE); @@ -142,12 +145,16 @@ int forge_parse_args(int argc, char **argv, opt_t *opts, int opts_count) { opt_t *target = &opts[(unsigned char)argv[i][1]]; if (target->takes_arg) { if(target->argv == NULL) { - target->argv = malloc(8 * sizeof(size_t)); - memset(target->argv, -1, sizeof(size_t) * 8); + target->argv = malloc(8 * sizeof(*target->argv)); + if (!target->argv) { + fprintf(stderr, "%s: malloc failed\n", argv[0]); + exit(EXIT_FAILURE); + } + memset(target->argv, -1, sizeof(*target->argv) * 8); target->argv[7] = NULL; } if(target->argv[target->argc] == NULL) { - char **ptr = realloc(target->argv, (target->argc * 2 + 2) * sizeof(size_t)); + char **ptr = realloc(target->argv, (target->argc * 2 + 2) * sizeof(*target->argv)); if(ptr == NULL) { fprintf(stderr, "%s: realloc failed\n", argv[0]); exit(EXIT_FAILURE); @@ -182,4 +189,4 @@ int forge_parse_args(int argc, char **argv, opt_t *opts, int opts_count) { } } return i; -} \ No newline at end of file +} diff --git a/lib/src/build.c b/lib/src/build.c index 763f174..05baa87 100644 --- a/lib/src/build.c +++ b/lib/src/build.c @@ -1045,26 +1045,6 @@ static char *sub_dir(const target_t *t, const char *sub) { return forge_strdup(sub); } -/* Target path under (when set) or - //, with mirrored/flattened naming */ -static char *obj_path_for(const target_t *t, const char *file, const char *ext, - const char *fallback) { - char *rel = os_path_file_rel(file, ext); - if (!rel) - return NULL; - char *base = (t->output_intermediate_path && *t->output_intermediate_path) - ? forge_strdup(t->output_intermediate_path) - : sub_dir(t, fallback); - if (!base) { - free(rel); - return NULL; - } - char *out = os_path_join(base, rel); - free(base); - free(rel); - return out; -} - /* Ensure the parent directory of path exists */ static void ensure_parent_dir(const char *path) { if (!path) @@ -1852,8 +1832,8 @@ static int needs_rebuild(const char *obj, const char *src, const char *cmd) { break; } } - for (char **p = deps; *p; p++) - free(*p); + for (int i = 0; i < dc; i++) + free(deps[i]); free(deps); } diff --git a/lib/src/compiler/gcc.c b/lib/src/compiler/gcc.c index 5399e47..a118343 100644 --- a/lib/src/compiler/gcc.c +++ b/lib/src/compiler/gcc.c @@ -17,7 +17,7 @@ * optimization -> -D -> -I (target+libs) -> per-file cflags -> * visibility/lto/pic flags -> -c|-S -> source file -> -o obj -> * -MMD -MF obj.d -> trailing options; - * - Link branch (FORGE_OBJ, EXE/SHARED): cc -> -flto(=N when + * - Link branch (FORGE_OBJ, EXE/SHARED): cc -> -flto(=N for GCC when * os_jobs>1) -> -shared -> obj list -> -L (incl. libs) -> -l -> -o -> * export symbol (os_export_flag) -> trailing linker options. * FORGE_STATIC_LIB belongs to the archiver; NULL is returned here. @@ -27,6 +27,7 @@ * MSVC uses compiler/msvc.c. */ #include "forge_compiler.h" +#include "forge_def.h" #include "forge_linker.h" #include "forge_os.h" #include "internal.h" @@ -178,7 +179,7 @@ static strv_t *gcc_link(const forge_context_t ctx) { failed = strv_append(&args, forge_default_cc(t)) != 0; if (!failed && la->lto_enabled == ENABLE) { - if (os_jobs > 1) { + if (os_jobs > 1 && t->toolchain.compiler != CLANG) { char flto[32]; snprintf(flto, sizeof(flto), "-flto=%d", os_jobs); failed = strv_append(&args, flto) != 0; @@ -236,4 +237,4 @@ strv_t *gcc_compiler(const forge_context_t context) { if (context.source_type == FORGE_SOURCE) return gcc_compile(context); return gcc_link(context); -} \ No newline at end of file +} diff --git a/lib/src/os/unix.c b/lib/src/os/unix.c index bfa209e..c686389 100644 --- a/lib/src/os/unix.c +++ b/lib/src/os/unix.c @@ -748,17 +748,27 @@ typedef struct { /* append data; -1 on failure (the old buffer is kept; the caller does the uniform cleanup) */ static int shell_buf_append(shell_buf *b, const char *data, size_t n) { + if (b->len == SIZE_MAX || n > SIZE_MAX - b->len - 1) + return -1; if (b->len + n + 1 > b->cap) { size_t ncap = b->cap ? b->cap : 256; - while (ncap < b->len + n + 1) + while (ncap < b->len + n + 1) { + if (ncap > SIZE_MAX / 2) { + ncap = b->len + n + 1; + break; + } ncap *= 2; + } char *nb = realloc(b->buf, ncap); if (!nb) return -1; b->buf = nb; b->cap = ncap; } - memcpy(b->buf + b->len, data, n); + if (!b->buf) + return -1; + if (n) + memcpy(b->buf + b->len, data, n); b->len += n; return 0; } @@ -782,8 +792,16 @@ char *os_shell(char **argv) { char *shell_argv[] = {(char *)"sh", (char *)"-c", line, NULL}; int errpipe[2] = {-1, -1}, outpipe[2] = {-1, -1}; - if (pipe(errpipe) != 0 || pipe(outpipe) != 0) - goto fail; + if (pipe(errpipe) != 0) { + free(line); + return NULL; + } + if (pipe(outpipe) != 0) { + close(errpipe[0]); + close(errpipe[1]); + free(line); + return NULL; + } pid_t pid = fork(); if (pid < 0) @@ -853,6 +871,12 @@ char *os_shell(char **argv) { } } + /* poll may fail before EOF; release both remaining read ends. */ + if (err_open) + close(errpipe[0]); + if (out_open) + close(outpipe[0]); + /* reap the child (pipe EOF before waitpid: no deadlock risk in the same process) */ while (waitpid(pid, NULL, 0) < 0 && errno == EINTR) {} @@ -873,14 +897,10 @@ char *os_shell(char **argv) { return res; fail: - if (errpipe[0] >= 0) { - close(errpipe[0]); - close(errpipe[1]); - } - if (outpipe[0] >= 0) { - close(outpipe[0]); - close(outpipe[1]); - } + close(errpipe[0]); + close(errpipe[1]); + close(outpipe[0]); + close(outpipe[1]); free(line); return NULL; } diff --git a/lib/src/strv.c b/lib/src/strv.c index 79fc50a..c53d3be 100644 --- a/lib/src/strv.c +++ b/lib/src/strv.c @@ -76,7 +76,7 @@ char* strv_get(strv_t* strv, int idx) { } int strv_find(strv_t* strv, const char* target) { if(strv == NULL || target == NULL) return -1; - for(size_t i = 0; i < strv->count; i++) { + for(int i = 0; i < strv->count; i++) { if(strcmp(strv->strs[i], target) == 0) return (int)i; } @@ -96,7 +96,7 @@ int strv_replace_first(strv_t* strv, const char* target, const char* replacement int strv_replace_all(strv_t* strv, const char* target, const char* replacement) { if(strv == NULL || target == NULL || replacement == NULL) return -1; int num = 0; - for(size_t i = 0; i < strv->count; i++) { + for(int i = 0; i < strv->count; i++) { if(strcmp(strv->strs[i], target) == 0) { char* ptr = forge_strdup(replacement); if(ptr == NULL) return -1; @@ -143,4 +143,4 @@ int strv_copy(strv_t *dest, const strv_t *src) { *dest = *v; free(v); return 0; -} \ No newline at end of file +} diff --git a/src/forge.c b/src/forge.c index f5aaee1..b346dc2 100644 --- a/src/forge.c +++ b/src/forge.c @@ -45,7 +45,6 @@ int forge_parse_args(int argc, char *argv[], opt_t *opts, int opts_count) { if (argv[i][1] == '-') { // long if (argv[i][2] != '\0') { char *eq = strchr(argv[i], '='); - size_t name_len = eq ? (size_t)(eq - argv[i]) : strlen(argv[i]); char *value = eq ? eq + 1 : NULL; if (eq) *eq = '\0'; @@ -261,7 +260,7 @@ static char *resolve_lib(int compiler) { } } - char *dirs[7]; + char *dirs[8]; int n = 0; if (prefix && *prefix) { if (compiler == MSVC) @@ -281,6 +280,8 @@ static char *resolve_lib(int compiler) { dirs[n++] = forge_strdup("/usr/lib"); for (int i = 0; i < n; i++) { + if (!dirs[i]) + continue; for (int k = 0; names[k]; k++) { char *p = path_join(dirs[i], names[k]); if (!p) @@ -294,13 +295,12 @@ static char *resolve_lib(int compiler) { free(p); } } - free(exe_dir); fprintf(stderr, "%s: library", program_name); for (int k = 0; names[k]; k++) fprintf(stderr, " %s", names[k]); fprintf(stderr, " not found in"); for (int i = 0; i < n; i++) - fprintf(stderr, " %s", dirs[i]); + fprintf(stderr, " %s", dirs[i] ? dirs[i] : "(null)"); fprintf(stderr, " (set FORGE_PREFIX or install to /usr/local/lib)\n"); for (int i = 0; i < n; i++) free(dirs[i]); @@ -674,6 +674,10 @@ int forge_main(char *target) { int compiler = UNKNOWN; char *compiler_path = nullptr; char *buf = path_join(target, "build.conf"); + if (!buf) { + fprintf(stderr, "%s: out of memory\n", program_name); + return -1; + } FILE *config = fopen(buf, "r"); if (config) { fseek(config, 0, SEEK_END); @@ -757,4 +761,4 @@ int forge_main(char *target) { free(compiler_path); free(buf); return ret; -} \ No newline at end of file +} diff --git a/tests/edge/test_ed_config.c b/tests/edge/test_ed_config.c index c3dd354..d817392 100644 --- a/tests/edge/test_ed_config.c +++ b/tests/edge/test_ed_config.c @@ -80,7 +80,9 @@ int main(void) { TEST(edge_write(dir, "build.conf", "") == 0); st = edge_forge(dir, out, sizeof(out)); TEST(st == 0); - TEST(os_path_is_file(os_path_join(dir, "make"))); + char *make_path = os_path_join(dir, "make"); + TEST(make_path && os_path_is_file(make_path)); + free(make_path); free(dir); /* 3. Unknown keys / lines without '=' / comments -> tolerated, @@ -192,4 +194,4 @@ int main(void) { PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/edge/test_ed_runtime.c b/tests/edge/test_ed_runtime.c index 762e7f8..f9650e2 100644 --- a/tests/edge/test_ed_runtime.c +++ b/tests/edge/test_ed_runtime.c @@ -117,6 +117,17 @@ int main(void) { TEST(out_has(out, "-c src/main.c")); free(dir); + /* Empty .d means no header dependencies; repeated incremental + builds must neither dereference NULL nor rebuild the source. */ + TEST(init_real("rt_dempty") == 0); + dir = os_path_abs("test/edge_rt_dempty"); + TEST(edge_forge(dir, out, sizeof(out)) == 0); + TEST(edge_make(dir, "make", out, sizeof(out)) == 0); + TEST(edge_write(dir, "opt/app/src/main.o.d", " \n\t") == 0); + TEST(edge_make(dir, "make", out, sizeof(out)) == 0); + TEST(!out_has(out, "-c src/main.c")); + free(dir); + /* 5. Corrupt .d -> conservative rebuild, success */ TEST(init_real("rt_dbad") == 0); dir = os_path_abs("test/edge_rt_dbad"); @@ -158,4 +169,4 @@ int main(void) { PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/edge/test_ed_shell.c b/tests/edge/test_ed_shell.c new file mode 100644 index 0000000..00fa80b --- /dev/null +++ b/tests/edge/test_ed_shell.c @@ -0,0 +1,38 @@ +// Source lib/src/os/line.c +// Source lib/src/os/path.c +// Source lib/src/os/unix.c +// Include lib/include +#include +#include + +#include + +/* Exercise the empty buffer, separate pipes and buffer growth paths. */ +int main(void) { + TEST(os_shell(NULL) == NULL); + + char *empty[] = {"true", NULL}; + char *out = os_shell(empty); + TEST(out && strcmp(out, "") == 0); + free(out); + + char *both[] = {"sh", "-c", "printf out; printf err >&2", NULL}; + out = os_shell(both); + TEST(out && strcmp(out, "errout") == 0); + free(out); + + char *large[] = {"seq", "1", "4096", NULL}; + out = os_shell(large); + TEST(out && strlen(out) > 8192); + TEST(strncmp(out, "1\n2\n", 4) == 0); + TEST(strstr(out, "4095\n4096\n") != NULL); + size_t lines = 0; + for (const char *p = out; *p; p++) + if (*p == '\n') + lines++; + TEST(lines == 4096); + free(out); + + PASS(); + return 0; +} diff --git a/tests/sub/test_args.c b/tests/sub/test_args.c index c4d47ec..cfa4d5a 100644 --- a/tests/sub/test_args.c +++ b/tests/sub/test_args.c @@ -2,7 +2,7 @@ // Source lib/src/strv.c // Source lib/src/args.c // Include lib/include -// Option -O2 -flto=8 +// Option -O2 -flto #include #include @@ -20,8 +20,8 @@ int main(void) { target_t *t = new_target(NULL); TEST(t != NULL); TEST(t->name == NULL); - free(t); + free_target_contents(t); PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/test_archiver.c b/tests/test_archiver.c index 5735a7c..4227119 100644 --- a/tests/test_archiver.c +++ b/tests/test_archiver.c @@ -23,8 +23,18 @@ static void rt(const forge_context_t ctx, const char *tool, const char *flags) { strv_t *seq = tool[0] == 'a' ? ar_archiver(ctx) : llvm_ar_archiver(ctx); TEST(seq != NULL); TEST(seq[0].strs != NULL); - TEST(strcmp(seq[0].strs[0], tool) == 0); - TEST(strcmp(seq[0].strs[1], flags) == 0); + TEST(seq[0].count == 3); + TEST(strcmp(seq[0].strs[0], "rm") == 0); + TEST(strcmp(seq[0].strs[1], "-f") == 0); + TEST(strcmp(seq[0].strs[2], "out.a") == 0); + TEST(seq[1].strs != NULL); + TEST(seq[1].count == 5); + TEST(strcmp(seq[1].strs[0], tool) == 0); + TEST(strcmp(seq[1].strs[1], flags) == 0); + TEST(strcmp(seq[1].strs[2], "out.a") == 0); + TEST(strcmp(seq[1].strs[3], "a.o") == 0); + TEST(strcmp(seq[1].strs[4], "b.o") == 0); + TEST(seq[2].strs == NULL); for (int i = 0; seq[i].strs; i++) strv_destroy(&seq[i]); free(seq); @@ -65,4 +75,4 @@ int main(void) { free_target_contents(t); PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/test_compiler.c b/tests/test_compiler.c index 2f9a992..6fe27c9 100644 --- a/tests/test_compiler.c +++ b/tests/test_compiler.c @@ -113,7 +113,6 @@ int main(void) { int o3 = find_arg(&seq[0], "-O3", -1); TEST(io >= 0 && o3 > io); free_seq(seq); - strv_destroy(&ctx.files[0].cflags); /* 3. Dispatcher: compiler() routes on toolchain.compiler */ seq = compiler(ctx); @@ -129,11 +128,11 @@ int main(void) { generation failure path reports an explicit error instead of emitting a deceptively usable command */ t->toolchain.compiler = MSVC; - memset(&ctx.files[0].cflags, 0, sizeof(strv_t)); seq = compiler(ctx); TEST(seq == NULL); + strv_destroy(&ctx.files[0].cflags); free_target_contents(t); PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/test_linker.c b/tests/test_linker.c index 835ac6c..15e090e 100644 --- a/tests/test_linker.c +++ b/tests/test_linker.c @@ -65,12 +65,14 @@ int main(void) { forge_context_t ctx; mk_ctx(&ctx, t, FORGE_SHARED_LIB); + os_set_jobs(2); /* 1. Default GCC link leg: gcc -> -shared -> objs -> -o output; the export flag (os_export_flag) must be included */ strv_t *seq = gcc_linker(ctx); TEST(seq != NULL && seq[0].strs != NULL); TEST(strcmp(seq[0].strs[0], "gcc") == 0); + TEST(find_arg(&seq[0], "-flto=2") >= 0); TEST(find_arg(&seq[0], "-shared") >= 0); TEST(find_arg(&seq[0], "a.o") >= 0 && find_arg(&seq[0], "b.o") >= 0); TEST(find_arg(&seq[0], "-rdynamic") >= 0); /* os_export_flag (Linux) */ @@ -98,6 +100,8 @@ int main(void) { t->toolchain.compiler = CLANG; seq = linker(ctx); TEST(seq != NULL && strcmp(seq[0].strs[0], "clang") == 0); + TEST(find_arg(&seq[0], "-flto") >= 0); + TEST(find_arg(&seq[0], "-flto=2") < 0); TEST(find_arg(&seq[0], "-shared") >= 0); free_seq(seq); @@ -108,7 +112,9 @@ int main(void) { TEST(seq != NULL && strcmp(seq[0].strs[0], "gcc") == 0); free_seq(seq); + for (int i = 0; i < ctx.files_count; i++) + strv_destroy(&ctx.files[i].cflags); free_target_contents(t); PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/test_null.c b/tests/test_null.c index 8b74eca..a2421f4 100644 --- a/tests/test_null.c +++ b/tests/test_null.c @@ -24,15 +24,17 @@ 2026-09-13). */ int main(void) { - strv_t v; + strv_t v = {0}; char *r = NULL; /* ---- strv family ---- */ - TEST(strv_new() != NULL); + strv_t *allocated = strv_new(); + TEST(allocated != NULL); + strv_free(allocated); strv_free(NULL); /* must be a no-op */ TEST(strv_init(NULL) == -1); strv_destroy(NULL); /* must be a no-op */ - strv_destroy(&v); /* an uninitialized stack strv must also be safe */ + strv_destroy(&v); /* a zero-initialized strv must also be safe */ TEST(strv_append(NULL, "x") == 1); TEST(strv_init(&v) == 0); TEST(strv_append(&v, NULL) == 1); @@ -62,7 +64,9 @@ int main(void) { r = os_path_join("a", NULL); TEST(r != NULL && strcmp(r, "a") == 0); free(r); - TEST(strcmp(os_path_basename(NULL), "") == 0); /* empty string, not NULL */ + r = os_path_basename(NULL); + TEST(r && strcmp(r, "") == 0); /* owned empty string, not NULL */ + free(r); TEST(os_path_is_absolute(NULL) == 0); TEST(os_path_is_under(NULL, "b") == 0); TEST(os_path_is_under("a", NULL) == 1); /* frozen observed value: NULL acts as an empty root */ @@ -82,4 +86,4 @@ int main(void) { strv_destroy(&v); PASS(); return 0; -} \ No newline at end of file +} diff --git a/tests/test_type.c b/tests/test_type.c index 6d58250..6074ef0 100644 --- a/tests/test_type.c +++ b/tests/test_type.c @@ -15,7 +15,9 @@ #include int main(void) { - TEST(new_target(NULL) != NULL); + target_t *unnamed = new_target(NULL); + TEST(unnamed != NULL); + free_target_contents(unnamed); target_t *t = new_target("abc"); TEST(t != NULL); @@ -59,4 +61,4 @@ int main(void) { free_target_contents(t); PASS(); return 0; -} \ No newline at end of file +} From 758f34eaffd61d07857362f0925a4d6000dc679e Mon Sep 17 00:00:00 2001 From: minelogy-dev <72928567+minelogy-dev@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:32:21 +0000 Subject: [PATCH 2/5] ci: add compiler, sanitizer, security and quality checks --- .github/dependabot.yml | 7 ++ .github/scripts/check-c.sh | 20 ++++ .github/scripts/configure.sh | 13 +++ .github/scripts/install-tools.sh | 20 ++++ .github/workflows/ci.yml | 153 +++++++++++++++++++++++++++++++ .github/workflows/codeql.yml | 51 +++++++++++ README.md | 5 +- README.zh-CN.md | 5 +- docs/CI.md | 93 +++++++++++++++++++ 9 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/scripts/check-c.sh create mode 100644 .github/scripts/configure.sh create mode 100644 .github/scripts/install-tools.sh create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 docs/CI.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8004983 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/scripts/check-c.sh b/.github/scripts/check-c.sh new file mode 100644 index 0000000..fdd06b1 --- /dev/null +++ b/.github/scripts/check-c.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Compile every production translation unit; warnings are CI failures. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +compiler=${CC:-gcc} +flags=(-std=c23 -O1 -Wall -Wextra -Werror -Iinclude -Isrc -Ilib/include -Ilib/src) +case "${1:-warnings}" in + warnings) ;; + analyze) flags+=(-fanalyzer -fno-analyzer-state-merge) ;; + *) echo "Usage: bash .github/scripts/check-c.sh [warnings|analyze]" >&2; exit 2 ;; +esac +mkdir -p build/ci +object_dir=$(mktemp -d "$PWD/build/ci/check-c.XXXXXX") +trap 'rm -rf "$object_dir"' EXIT +mapfile -d '' sources < <(git ls-files -z -- 'src/*.c' 'lib/src/*.c') +(( ${#sources[@]} > 0 )) +for source in "${sources[@]}"; do + echo "Checking $source" + "$compiler" "${flags[@]}" -c "$source" -o "$object_dir/${source//\//_}.o" +done diff --git a/.github/scripts/configure.sh b/.github/scripts/configure.sh new file mode 100644 index 0000000..569796e --- /dev/null +++ b/.github/scripts/configure.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Configure the local build entry point for CI or an equivalent local run. +set -euo pipefail +cd "$(git rev-parse --show-toplevel)" +compiler_path=$(command -v "${CC:-gcc}") +case "$(basename "$compiler_path")" in + gcc*) backend=GCC ;; + clang*) backend=CLANG ;; + *) echo "CC must select GCC or Clang" >&2; exit 2 ;; +esac +sed -i '/^compiler=/d; /^compiler_path=/d; /^sanitize=/d' build.conf +printf '\ncompiler=%s\ncompiler_path=%s\nsanitize=1\n' "$backend" "$compiler_path" >> build.conf +"$compiler_path" --version diff --git a/.github/scripts/install-tools.sh b/.github/scripts/install-tools.sh new file mode 100644 index 0000000..2ddb4ce --- /dev/null +++ b/.github/scripts/install-tools.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Pinned CLI releases, verified before executing; no GitHub secrets required. +set -euo pipefail +destination=${1:?Pass an installation directory} +mkdir -p "$destination" +destination=$(cd "$destination" && pwd) +download_dir=$(mktemp -d) +trap 'rm -rf "$download_dir"' EXIT +install_release() { + local url=$1 checksum=$2 executable=$3 + curl --fail --location --silent --show-error --retry 3 "$url" -o "$download_dir/archive.tar.gz" + printf '%s %s\n' "$checksum" "$download_dir/archive.tar.gz" | sha256sum --check + tar -xzf "$download_dir/archive.tar.gz" -C "$destination" "$executable" +} +install_release \ + https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \ + 023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757 actionlint +install_release \ + https://github.com/gitleaks/gitleaks/releases/download/v8.24.2/gitleaks_8.24.2_linux_x64.tar.gz \ + fa0500f6b7e41d28791ebc680f5dd9899cd42b58629218a5f041efa899151a8e gitleaks diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b5c817a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,153 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +defaults: + run: + shell: bash + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + ASAN_OPTIONS: detect_leaks=1:halt_on_error=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + +jobs: + build-test: + name: Build and test (${{ matrix.cc }}) + runs-on: ubuntu-24.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + cc: [gcc, clang] + env: + CC: ${{ matrix.cc }} + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install both supported toolchains + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y gcc-14 clang llvm binutils pkg-config + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 + - name: Configure the selected compiler + run: bash .github/scripts/configure.sh + - name: Reject compiler warnings + run: bash .github/scripts/check-c.sh + - name: Bootstrap the real build + run: sh bootstrap.sh --no-test + - name: Run all unit and scenario tests with sanitizers + run: ./make test -j 2 2>&1 | tee build/ci-tests.log + - name: Build and run the example + working-directory: examples/demo + run: | + ../../build/output/forge . + ./make -j 2 + ./output/app | tee demo.log + grep -Fx 'hello from forge demo' demo.log + - name: Validate installation and Debian package + run: | + ./make install "$RUNNER_TEMP/forge-install" + "$RUNNER_TEMP/forge-install/bin/forge" --version + ./make gen_deb + dpkg-deb --info build/deb/forge_*.deb + dpkg-deb --contents build/deb/forge_*.deb + - name: Keep diagnostics after a failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: diagnostics-${{ matrix.cc }} + path: | + build/ci-tests.log + build/**/*.d + build/**/*.meta + test/edge_*/build.conf + test/edge_*/build.c + if-no-files-found: ignore + retention-days: 7 + + static-analysis: + name: GCC static analysis + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install analyzer dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y gcc-14 + - name: Check production code for unsafe memory and resource use + env: + CC: gcc-14 + run: | + mkdir -p build + bash .github/scripts/check-c.sh analyze 2>&1 | tee build/ci-analyzer.log + - name: Keep analyzer diagnostics after a failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: analyzer-diagnostics + path: build/ci-analyzer.log + if-no-files-found: ignore + retention-days: 7 + + quality: + name: Workflow and secret checks + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install ShellCheck and pinned security tools + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y shellcheck + bash .github/scripts/install-tools.sh "$RUNNER_TEMP/ci-tools" + - name: Lint workflows and helper scripts + run: | + "$RUNNER_TEMP/ci-tools/actionlint" -color + shellcheck .github/scripts/*.sh + - name: Scan the complete Git history for credentials + run: '"$RUNNER_TEMP/ci-tools/gitleaks" git --redact --verbose' + - name: Reject whitespace errors in the proposed changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: | + if test -n "$BASE_SHA" && git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then + git diff --check "$BASE_SHA" "$HEAD_SHA" + else + git show --format= --check "$HEAD_SHA" + fi + + ci: + name: CI + if: always() + needs: [build-test, static-analysis, quality] + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Require every check to pass + env: + BUILD_TEST: ${{ needs.build-test.result }} + STATIC_ANALYSIS: ${{ needs.static-analysis.result }} + QUALITY: ${{ needs.quality.result }} + run: | + test "$BUILD_TEST" = success + test "$STATIC_ANALYSIS" = success + test "$QUALITY" = success diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d1f05b7 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,51 @@ +name: CodeQL + +on: + push: + branches: [main] + pull_request: + schedule: + - cron: '17 3 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: CodeQL (C/C++) + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + security-events: write + actions: read + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + persist-credentials: false + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install --no-install-recommends -y gcc-14 binutils + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 + - name: Configure the build compiler + env: + CC: gcc + run: bash .github/scripts/configure.sh + - name: Initialize security and maintainability queries + uses: github/codeql-action/init@793b778da9c9275a0f276522b27cb5fc14bba279 # v4 + with: + languages: c-cpp + build-mode: manual + queries: security-and-quality + - name: Build production code for analysis + run: sh bootstrap.sh --no-test + - name: Publish findings to code scanning and the pull request + uses: github/codeql-action/analyze@793b778da9c9275a0f276522b27cb5fc14bba279 # v4 + with: + category: /language:c-cpp diff --git a/README.md b/README.md index 17772dd..4a2dd0e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ itself. ## Quick Start +Pull requests run compiler, sanitizer, static-analysis and secret checks. +See [Continuous integration](docs/CI.md) for coverage and merge-protection setup. + ```sh ./bootstrap.sh # bootstrap from zero: host forge + runtime library + full build + tests ./make # incremental build (re-running the ./make produced by "forge .") @@ -225,4 +228,4 @@ requests. - IDE project generation These are not bugs - they are the current boundaries. Issues and -feature requests are welcome to help set priorities. \ No newline at end of file +feature requests are welcome to help set priorities. diff --git a/README.zh-CN.md b/README.zh-CN.md index 9c789a4..9d860ca 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -11,6 +11,9 @@ ## 快速开始 +PR 会自动执行编译、内存检查、静态分析和密钥扫描。 +检查范围与分支保护设置见[持续集成说明](docs/CI.md)。 + ```sh ./bootstrap.sh # 从零自举:宿主 forge + 运行时库 + 全量构建 + 测试 ./make # 增量构建(等同 forge . 生成的 make 再次运行) @@ -172,4 +175,4 @@ Forge 当通用库嵌进无关应用--附加许可只针对构建脚本及其构 - 交叉编译矩阵 - IDE 工程生成 -这些不是 bug,是当前边界。欢迎提 issue 讨论优先级。 \ No newline at end of file +这些不是 bug,是当前边界。欢迎提 issue 讨论优先级。 diff --git a/docs/CI.md b/docs/CI.md new file mode 100644 index 0000000..9b726c2 --- /dev/null +++ b/docs/CI.md @@ -0,0 +1,93 @@ +# Continuous integration + +Changes to `main`, pull requests, and manual runs use GitHub Actions. +The workflows have read-only repository access, except for the CodeQL job's +permission to publish code-scanning results. Pull-request code never runs via +`pull_request_target`, and checkout credentials are not persisted. + +## Checks + +| Check | What it verifies | +| --- | --- | +| Build and test (gcc / clang) | A real build on Ubuntu 24.04 with GCC 14 and Clang 18, the complete test suite, ASan, UBSan and leak detection | +| Compiler warnings | Every production C translation unit compiles with `-Wall -Wextra -Werror` | +| GCC static analysis | `-fanalyzer` checks production translation units; diagnostics fail the job | +| Workflow and secret checks | Actionlint, ShellCheck, whitespace errors and Gitleaks over the complete Git history, with redacted output | +| CodeQL (C/C++) | Manual build tracing with `security-and-quality` queries, findings in the Security tab and on pull requests | +| CI | A stable required check that fails if any build, analyzer or quality job fails or is cancelled | + +CodeQL also runs every Monday at 11:17 Asia/Shanghai (03:17 UTC). +Its successful job status means the analysis completed; it does **not** mean +there are no alerts. Use a code-scanning merge rule to enforce alert severity. + +GCC analysis uses `-O1 -fno-analyzer-state-merge`: GCC 14's default state merging +produces spurious allocation/cleanup diagnostics in this code. No diagnostic +categories are suppressed. An analyzer failure should be investigated, not +worked around with `continue-on-error`. + +All external Actions use immutable commit SHAs. Dependabot proposes weekly +GitHub Actions updates. The Actionlint and Gitleaks CLIs have pinned versions +and SHA-256 checksums in `.github/scripts/install-tools.sh`; update both values +together when upgrading them. + +## Enable merge protection + +After the first successful run, create a branch ruleset for `main`: + +1. Require pull requests and the `CI` and `CodeQL (C/C++)` status checks. +2. Require branches to be up to date before merging. +3. Add “Require code scanning results”, select CodeQL, and block high/critical + security alerts and error-level quality alerts. Tighten these thresholds + after triaging existing findings. +4. Use the checked-in advanced CodeQL workflow rather than enabling a duplicate + default-setup scan. + +Workflow files alone do not enable branch protection. These repository settings +require a one-time maintainer configuration. Changes to CI permissions, checks +and dependency pins still deserve human review. + +## Reproduce locally + +Install Linux build dependencies and a C23 compiler (GCC 14+ or Clang 18+). +Save local `build.conf` changes before running `configure.sh`: it updates the +compiler selection and enables Forge test instrumentation in that file. + +```sh +CC=gcc bash .github/scripts/configure.sh # or CC=clang +CC=gcc bash .github/scripts/check-c.sh +CC=gcc bash .github/scripts/check-c.sh analyze +``` + +Use GCC for `analyze`. Runtime checks set: + +```sh +export ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 +export UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 +``` + +Then run: + +```sh +sh bootstrap.sh --no-test +./make test -j 2 +``` + +`sanitize=1` in `build.conf` enables sanitizer instrumentation for test +translation units and their linked source dependencies; production artifacts +remain optimized builds. Test targets also use the compiler chosen in +`build.conf`. + +The build jobs additionally execute the demo, install into a temporary prefix, +and inspect the generated Debian package. They never install project artifacts +into system directories. + +## Review and failures + +Open the failed check for compiler diagnostics, sanitizer stack traces or +secret-scan locations. Build failures retain dependency and command metadata +for seven days; the Actions log contains the full build/test output. +CodeQL alerts include the affected lines and data flow where available. + +Automation covers mechanical regressions and many common memory-safety bugs. +Human review should concentrate on requirements, API contracts, ownership, +architecture and the findings reported by these tools. From 819a6bdb168d01a3dc65365d0d081b294bdd3999 Mon Sep 17 00:00:00 2001 From: minelogy-dev <72928567+minelogy-dev@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:49:29 +0000 Subject: [PATCH 3/5] fix: support Clang 18 in host option table --- src/forge.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forge.c b/src/forge.c index b346dc2..76bfc7c 100644 --- a/src/forge.c +++ b/src/forge.c @@ -27,7 +27,7 @@ typedef struct { char *argv; } opt_t; -constexpr int opts_count = 256; +enum { opts_count = 256 }; opt_t opts[opts_count] = { ['V'] = {"version", 0, 0, NULL}, ['h'] = {"help", 0, 0, NULL}, From c51550d12b05a4312de46c5e6f35faa9b2f09a33 Mon Sep 17 00:00:00 2001 From: minelogy-dev <72928567+minelogy-dev@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:33:42 +0000 Subject: [PATCH 4/5] ci: pin Clang 19 for C23 support Co-authored-by: Codex --- .github/workflows/ci.yml | 11 +++++++++-- docs/CI.md | 4 ++-- src/forge.c | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5c817a..8f9393c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - cc: [gcc, clang] + cc: [gcc-14, clang-19] env: CC: ${{ matrix.cc }} steps: @@ -39,8 +39,15 @@ jobs: - name: Install both supported toolchains run: | sudo apt-get update - sudo apt-get install --no-install-recommends -y gcc-14 clang llvm binutils pkg-config + sudo apt-get install --no-install-recommends -y gcc-14 clang-19 llvm-19 libclang-rt-19-dev binutils pkg-config sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 + sudo update-alternatives --set gcc /usr/bin/gcc-14 + sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-19 190 + sudo update-alternatives --set clang /usr/bin/clang-19 + - name: Verify compiler versions used by generated builds + run: | + test "$(gcc -dumpversion | cut -d. -f1)" = 14 + test "$(clang -dumpversion | cut -d. -f1)" = 19 - name: Configure the selected compiler run: bash .github/scripts/configure.sh - name: Reject compiler warnings diff --git a/docs/CI.md b/docs/CI.md index 9b726c2..21c94e5 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -9,7 +9,7 @@ permission to publish code-scanning results. Pull-request code never runs via | Check | What it verifies | | --- | --- | -| Build and test (gcc / clang) | A real build on Ubuntu 24.04 with GCC 14 and Clang 18, the complete test suite, ASan, UBSan and leak detection | +| Build and test (gcc-14 / clang-19) | A real build on Ubuntu 24.04 with GCC 14 and Clang 19, the complete test suite, ASan, UBSan and leak detection | | Compiler warnings | Every production C translation unit compiles with `-Wall -Wextra -Werror` | | GCC static analysis | `-fanalyzer` checks production translation units; diagnostics fail the job | | Workflow and secret checks | Actionlint, ShellCheck, whitespace errors and Gitleaks over the complete Git history, with redacted output | @@ -48,7 +48,7 @@ and dependency pins still deserve human review. ## Reproduce locally -Install Linux build dependencies and a C23 compiler (GCC 14+ or Clang 18+). +Install Linux build dependencies and a C23 compiler (GCC 14+ or Clang 19+). Save local `build.conf` changes before running `configure.sh`: it updates the compiler selection and enables Forge test instrumentation in that file. diff --git a/src/forge.c b/src/forge.c index 76bfc7c..b346dc2 100644 --- a/src/forge.c +++ b/src/forge.c @@ -27,7 +27,7 @@ typedef struct { char *argv; } opt_t; -enum { opts_count = 256 }; +constexpr int opts_count = 256; opt_t opts[opts_count] = { ['V'] = {"version", 0, 0, NULL}, ['h'] = {"help", 0, 0, NULL}, From 97d93c4dd2602ef129a42b2215d2e6d85cb4ef31 Mon Sep 17 00:00:00 2001 From: minelogy-dev <72928567+minelogy-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:04:33 +0000 Subject: [PATCH 5/5] fix: make test configuration macro a single statement Wrap LANGUAGE in do-while (0) so callers can use it safely with a trailing semicolon in conditional statements. Co-authored-by: Codex --- build.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.c b/build.c index 80a30ac..08631a0 100644 --- a/build.c +++ b/build.c @@ -11,14 +11,14 @@ #endif #define TOOLCHAIN set_toolchain(CONF_compiler) #define LANGUAGE \ - { \ + do { \ set_language(C); \ set_standard("c23"); \ add_define("_DEFAULT_SOURCE", "1"); \ if (CONF_sanitize) \ add(TARGET_OPTIONS, 4, "-g", "-fsanitize=address,undefined", \ "-fno-sanitize-recover=all", "-fno-omit-frame-pointer"); \ - } + } while (0) #include /* The version has a single source of truth: the version key in