commit a66723352d1c160257286be2be3040eebf0d1eb2
parent cbb7b541f827b20f0a97e30525e762e4bbdbb2c2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 17 Jul 2026 17:54:21 -0700
make: add `kit make`, a POSIX make ported from pdpmake
A make(1) build-orchestration front-end for the toolchain, ported from the
public-domain pdpmake (rmyorston/pdpmake) and reworked onto kit's injected
IO/mem abstractions with no global state.
- Engine in src/make/*.c, compiled as one amalgamation TU (src/api/make.c
includes the fragments; all functions static). Every pdpmake global moves into
a threaded MakeCtx; memory is an arena; error() becomes setjmp/longjmp; stdio,
fopen, system/popen, stat, getenv are replaced by KitWriter, KitContext.file_io,
the KitMakeHost vtable (mtime/touch/remove + a shared KitExec for recipes and
`!=` / $(shell ...)), and the ambient env from options.
- Public one-shot API <kit/make.h> (structured KitMakeOptions); CLI parsing
(argv + MAKEFLAGS) lives in driver/cmd/make.c. -C is a context root (no chdir);
MAKEFLAGS propagates to recursive make; $(shell ...) is supported.
- Supports POSIX-2024 make plus common extensions (include, ifdef/ifeq, ::,
::=/:=, +=, ?=, !=, .PHONY). Serial execution (-j accepted, clamped).
- gated by KIT_MAKE_ENABLED / KIT_TOOL_MAKE_ENABLED.
v1 limits: no shell-glob wildcard expansion in prereqs/targets; the Windows
recipe path compiles but is verified only in the Windows VM lane.
Tests: test/make/run.sh (30 cases, `make test-driver-make`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cu5rjrydtNn6LAEwLYqQeB
Diffstat:
24 files changed, 3864 insertions(+), 2 deletions(-)
diff --git a/driver/cmd/make.c b/driver/cmd/make.c
@@ -0,0 +1,232 @@
+#include <kit/core.h>
+#include <kit/make.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "driver.h"
+#include "env.h"
+
+#define MAKE_TOOL "make"
+
+void driver_help_make(void) {
+ driver_printf(
+ "kit make — bring targets up to date from a makefile (POSIX make)\n"
+ "\n"
+ "USAGE\n"
+ " kit make [-f MAKEFILE] [OPTION]... [MACRO=VALUE]... [TARGET]...\n"
+ "\n"
+ "DESCRIPTION\n"
+ " Reads one or more makefiles, builds a dependency graph, and runs the\n"
+ " recipes needed to bring the requested targets up to date. Recipes run\n"
+ " through /bin/sh (or the SHELL macro). Ported from the public-domain\n"
+ " pdpmake; supports POSIX-2024 make plus common extensions (include,\n"
+ " ifdef/ifeq, ::, ::=/:=, $(shell), -C).\n"
+ "\n"
+ "OPTIONS\n"
+ " -f FILE read FILE as a makefile ('-' reads stdin); may repeat\n"
+ " -C DIR change to DIR before reading makefiles\n"
+ " -e environment variables override makefile macros\n"
+ " -i ignore errors from recipe commands\n"
+ " -k keep going with unrelated targets after an error\n"
+ " -n print recipes without running them (dry run)\n"
+ " -p print the macro and target database\n"
+ " -q question mode: exit 0 if up to date, 1 if not\n"
+ " -r do not use the built-in inference rules and macros\n"
+ " -s do not echo recipe lines\n"
+ " -S stop on the first error (undo -k)\n"
+ " -t touch targets instead of running recipes\n"
+ " -j N accepted for compatibility; recipes run serially\n"
+ " --posix enforce strict POSIX mode\n"
+ " -h, --help show this help and exit\n"
+ "\n"
+ "EXIT CODES\n"
+ " 0 success 1 -q: a target is out of date\n"
+ " 2 build error or bad command-line usage\n");
+}
+
+/* --- KitMakeHost callbacks, backed by the driver host shims --------------- */
+
+static int mk_mtime(void* user, const char* path, int64_t* out_ns) {
+ (void)user;
+ return driver_path_mtime_ns(path, out_ns);
+}
+
+static int mk_touch(void* user, const char* path) {
+ (void)user;
+ return driver_touch(path);
+}
+
+static int mk_remove(void* user, const char* path) {
+ (void)user;
+ return driver_remove_file(path);
+}
+
+/* An operand of the form NAME=VALUE (with NAME non-empty) is a macro. */
+static const char* macro_eq(const char* s) {
+ const char* eq = driver_strchr(s, '=');
+ return (eq && eq != s) ? eq : NULL;
+}
+
+int driver_make(int argc, char** argv) {
+ DriverEnv env;
+ KitContext ctx;
+ KitExec ex;
+ KitMakeHost host;
+ KitMakeOptions opts = {0};
+ KitMakeResult result;
+ KitWriter* out = NULL;
+ KitWriter* err = NULL;
+ const char** makefiles = NULL;
+ const char** targets = NULL;
+ KitMakeVar* macros = NULL;
+ size_t nmf = 0, ntg = 0, nmac = 0;
+ const char* chdir_to = NULL;
+ int want_stdin = 0;
+ uint8_t* stdin_buf = NULL;
+ size_t stdin_len = 0;
+ char* curdir_buf = NULL;
+ size_t curdir_size = 0;
+ size_t cap = (size_t)(argc > 0 ? argc : 1);
+ int rc = 2;
+ int i;
+
+ if (driver_argv_wants_help(argc, argv, 1)) {
+ driver_help_make();
+ return 0;
+ }
+
+ driver_env_init(&env);
+ ex = driver_exec(&env);
+
+ makefiles = driver_alloc_zeroed(&env, cap * sizeof *makefiles);
+ targets = driver_alloc_zeroed(&env, cap * sizeof *targets);
+ macros = driver_alloc_zeroed(&env, cap * sizeof *macros);
+ if (!makefiles || !targets || !macros) {
+ driver_errf(MAKE_TOOL, "out of memory");
+ goto done;
+ }
+
+ for (i = 1; i < argc; ++i) {
+ const char* a = argv[i];
+ const char* eq;
+
+ if (a[0] == '-' && a[1] != '\0') {
+ const char* p;
+ if (driver_streq(a, "--posix")) {
+ opts.flags |= KIT_MAKE_F_POSIX;
+ continue;
+ }
+ if (a[1] == '-') {
+ driver_errf(MAKE_TOOL, "unknown option: %s", a);
+ goto done;
+ }
+ for (p = a + 1; *p; ++p) {
+ char c = *p;
+ if (c == 'f' || c == 'C' || c == 'j') {
+ const char* val = p[1] ? p + 1 : NULL;
+ if (!val) {
+ if (i + 1 < argc)
+ val = argv[++i];
+ else {
+ driver_errf(MAKE_TOOL, "option -%c requires an argument", c);
+ goto done;
+ }
+ }
+ if (c == 'f') {
+ if (driver_streq(val, "-")) want_stdin = 1;
+ makefiles[nmf++] = val;
+ } else if (c == 'C') {
+ chdir_to = val;
+ } else {
+ opts.jobs = 1; /* -j: recipes run serially in v1 */
+ }
+ break; /* the rest of the cluster was the argument */
+ }
+ switch (c) {
+ case 'e': opts.flags |= KIT_MAKE_F_ENV_OVERRIDE; break;
+ case 'i': opts.flags |= KIT_MAKE_F_IGNORE_ERR; break;
+ case 'k': opts.flags |= KIT_MAKE_F_KEEP_GOING; break;
+ case 'n': opts.flags |= KIT_MAKE_F_DRY_RUN; break;
+ case 'p': opts.flags |= KIT_MAKE_F_PRINT_DB; break;
+ case 'q': opts.flags |= KIT_MAKE_F_QUESTION; break;
+ case 'r': opts.flags |= KIT_MAKE_F_NO_BUILTINS; break;
+ case 's': opts.flags |= KIT_MAKE_F_SILENT; break;
+ case 'S': opts.flags &= ~(uint32_t)KIT_MAKE_F_KEEP_GOING; break;
+ case 't': opts.flags |= KIT_MAKE_F_TOUCH; break;
+ default:
+ driver_errf(MAKE_TOOL, "unknown option: -%c", c);
+ goto done;
+ }
+ }
+ } else if ((eq = macro_eq(a)) != NULL) {
+ macros[nmac].name.s = a;
+ macros[nmac].name.len = (size_t)(eq - a);
+ macros[nmac].value.s = eq + 1;
+ macros[nmac].value.len = driver_strlen(eq + 1);
+ nmac++;
+ } else {
+ targets[ntg++] = a;
+ }
+ }
+
+ if (want_stdin && !driver_read_stdin(&env, &stdin_buf, &stdin_len)) {
+ driver_errf(MAKE_TOOL, "failed to read stdin");
+ goto done;
+ }
+
+ /* Root/working directory: make resolves relative makefile/target paths
+ * against it and runs recipes there (the engine never chdir()s). -C selects
+ * it, otherwise the current directory. Also becomes $(CURDIR). */
+ {
+ const char* dir = chdir_to ? chdir_to : ".";
+ if (driver_path_canonicalize(&env, dir, &curdir_buf, &curdir_size) != 0) {
+ if (chdir_to) {
+ driver_errf(MAKE_TOOL, "can't resolve directory %s", chdir_to);
+ goto done;
+ }
+ } else {
+ opts.curdir = curdir_buf;
+ }
+ }
+
+ out = driver_stdout_writer(&env);
+ err = driver_stderr_writer(&env);
+ if (!out || !err) {
+ driver_errf(MAKE_TOOL, "failed to open output streams");
+ goto done;
+ }
+
+ host.mtime = mk_mtime;
+ host.exec = &ex;
+ host.touch = mk_touch;
+ host.remove_file = mk_remove;
+ host.user = &env;
+
+ opts.makefiles = makefiles;
+ opts.nmakefiles = nmf;
+ opts.targets = targets;
+ opts.ntargets = ntg;
+ opts.macros = macros;
+ opts.nmacros = nmac;
+ opts.env = driver_environ();
+ opts.shell = NULL; /* engine defaults to /bin/sh; SHELL macro can override */
+ opts.stdin_makefile = stdin_buf;
+ opts.stdin_makefile_len = stdin_len;
+ opts.out = out;
+ opts.err = err;
+
+ ctx = driver_env_to_context(&env);
+ result.exit_code = 2;
+ if (kit_make_run(&ctx, &host, &opts, &result) == KIT_OK) rc = result.exit_code;
+
+done:
+ if (out) kit_writer_close(out);
+ if (err) kit_writer_close(err);
+ if (stdin_buf) driver_free(&env, stdin_buf, stdin_len);
+ if (curdir_buf) driver_free(&env, curdir_buf, curdir_size);
+ if (makefiles) driver_free(&env, makefiles, cap * sizeof *makefiles);
+ if (targets) driver_free(&env, targets, cap * sizeof *targets);
+ if (macros) driver_free(&env, macros, cap * sizeof *macros);
+ driver_env_fini(&env);
+ return rc;
+}
diff --git a/driver/driver.h b/driver/driver.h
@@ -26,6 +26,7 @@ int driver_build_exe(int argc, char** argv);
int driver_build_lib(int argc, char** argv);
int driver_build_obj(int argc, char** argv);
int driver_build(int argc, char** argv);
+int driver_make(int argc, char** argv);
int driver_build_exe_ex(int argc, char** argv, const KitDriverExtension*);
int driver_build_lib_ex(int argc, char** argv, const KitDriverExtension*);
int driver_build_obj_ex(int argc, char** argv, const KitDriverExtension*);
@@ -78,6 +79,7 @@ void driver_help_build_exe(void);
void driver_help_build_lib(void);
void driver_help_build_obj(void);
void driver_help_build(void);
+void driver_help_make(void);
void driver_help_install(void);
void driver_help_cpp(void);
void driver_help_as(void);
diff --git a/driver/env.h b/driver/env.h
@@ -339,6 +339,8 @@ const char* const* driver_environ(void);
* make`. The returned value borrows `env` (its `user`); do not outlive env.
* Implemented in driver/env/exec_{posix,windows}.c. */
KitExec driver_exec(DriverEnv* env);
+/* Set path's mtime to now, creating it empty if absent (make -t). 0 on success. */
+int driver_touch(const char* path);
/* Read all of stdin into a freshly-allocated buffer. On success returns 1
* and stores the buffer/size in out_data/out_size; the caller frees via
diff --git a/driver/env/posix.c b/driver/env/posix.c
@@ -978,6 +978,20 @@ int driver_fetch_url(const char* url, const char* dest) {
return (WIFEXITED(status) && WEXITSTATUS(status) == 0) ? 0 : 1;
}
+int driver_touch(const char* path) {
+ const struct timespec times[2] = {{0, UTIME_NOW}, {0, UTIME_NOW}};
+ if (!path) return 1;
+ if (utimensat(AT_FDCWD, path, times, 0) == 0) return 0;
+ if (errno == ENOENT) {
+ int fd = open(path, O_WRONLY | O_CREAT, 0666);
+ if (fd >= 0) {
+ close(fd);
+ return 0;
+ }
+ }
+ return 1;
+}
+
static int driver_walk_regular_files_at(DriverEnv* env, const char* dir,
const char* rel, DriverWalkFileFn cb,
void* user) {
diff --git a/driver/env/windows.c b/driver/env/windows.c
@@ -53,6 +53,7 @@
#include <windows.h>
// clang-format on
+#include <fcntl.h>
#include <io.h>
#include <process.h>
#include <psapi.h>
@@ -511,6 +512,24 @@ void driver_writer_abort(KitWriter* writer) {
const char* const* driver_environ(void) { return (const char* const*)_environ; }
+int driver_touch(const char* path) {
+ wchar_t* wpath;
+ HANDLE h;
+ FILETIME ft;
+ if (!path) return 1;
+ wpath = widen(path);
+ if (!wpath) return 1;
+ h = CreateFileW(wpath, FILE_WRITE_ATTRIBUTES,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL, NULL);
+ free(wpath);
+ if (h == INVALID_HANDLE_VALUE) return 1;
+ GetSystemTimeAsFileTime(&ft);
+ SetFileTime(h, NULL, &ft, &ft);
+ CloseHandle(h);
+ return 0;
+}
+
/* ============================================================
* file_io (CreateFileW + ReadFile/WriteFile)
* ============================================================ */
diff --git a/driver/main.c b/driver/main.c
@@ -71,6 +71,11 @@ static const DriverToolDesc driver_tools[] = {
"Resolve a target with the content-addressed build coordinator",
DRIVER_HELP_BUILD, DRIVER_GROUP_OTHER},
#endif
+#if KIT_TOOL_MAKE_ENABLED
+ {"make", driver_make, NULL, driver_help_make,
+ "Bring targets up to date from a makefile (POSIX make)",
+ DRIVER_HELP_BUILD, DRIVER_GROUP_OTHER},
+#endif
#if KIT_TOOL_INSTALL_ENABLED
{"install", driver_install, NULL, driver_help_install,
"Symlink the kit tools into a dir for drop-in toolchain use",
diff --git a/include/kit/config.h b/include/kit/config.h
@@ -92,6 +92,12 @@
* LZ4 frame layer. */
#define KIT_COMPRESS_ENABLED 1
+/* POSIX make (<kit/make.h> + `kit make`): a make(1) build-orchestration
+ * front-end ported from pdpmake. Reads makefiles and brings targets up to date
+ * by running recipes through an injected shell. Self-contained; depends on no
+ * other library subsystem. */
+#define KIT_MAKE_ENABLED 1
+
/* Parser/lexer-generator subsystem (gram): EBNF in, parse/lexer/token-machine
* tables out, with allocation-free push runtimes for generated parsers and
* lexers. Runtime + generator are gated together; the public API is
@@ -123,6 +129,7 @@
#define KIT_TOOL_BUILD_LIB_ENABLED 1
#define KIT_TOOL_BUILD_OBJ_ENABLED 1
#define KIT_TOOL_BUILD_ENABLED 1
+#define KIT_TOOL_MAKE_ENABLED 1
#define KIT_TOOL_INSTALL_ENABLED 1
#define KIT_TOOL_CPP_ENABLED 1
#define KIT_TOOL_AS_ENABLED 1
diff --git a/include/kit/make.h b/include/kit/make.h
@@ -0,0 +1,112 @@
+#ifndef KIT_MAKE_H
+#define KIT_MAKE_H
+
+#include <kit/core.h>
+#include <kit/exec.h>
+#include <stddef.h>
+#include <stdint.h>
+
+/*
+ * POSIX make: a make(1) build engine. Reads one or more makefiles, builds a
+ * target dependency graph, and brings the requested targets up to date by
+ * running their recipes through a shell. Ported from the public-domain
+ * pdpmake (rmyorston/pdpmake); supports POSIX-2024 make plus the common
+ * extensions (include, ifdef/ifeq, ::, ::=/:=, $(shell), -C).
+ *
+ * The library sources no entropy and performs no I/O or process control
+ * itself. Makefile (and include) bytes are read through KitContext.file_io;
+ * every side-effecting capability -- running a recipe, reading a file's
+ * modification time, touching or removing a target -- arrives through the
+ * KitMakeHost vtable below. Ambient data the engine only reads (the process
+ * environment, the working directory, the default shell) rides in
+ * KitMakeOptions rather than being a callback.
+ *
+ * Operational failures return a KitStatus and emit a message through
+ * ctx->diag (pass a NULL diag sink to stay quiet). make's own build
+ * diagnostics -- the ones that carry `makefile:line` context -- are written to
+ * KitMakeOptions.err so their exact POSIX text is preserved; the resulting
+ * make exit status is reported in KitMakeResult.exit_code.
+ */
+
+/* make(1) option flags, OR-ed into KitMakeOptions.flags. */
+enum {
+ KIT_MAKE_F_ENV_OVERRIDE = 1u << 0, /* -e: environment overrides makefile macros */
+ KIT_MAKE_F_IGNORE_ERR = 1u << 1, /* -i: ignore recipe non-zero exits */
+ KIT_MAKE_F_KEEP_GOING = 1u << 2, /* -k: keep building unrelated targets on error */
+ KIT_MAKE_F_DRY_RUN = 1u << 3, /* -n: print recipe lines, do not run them */
+ KIT_MAKE_F_PRINT_DB = 1u << 4, /* -p: print the macro/target database */
+ KIT_MAKE_F_QUESTION = 1u << 5, /* -q: question mode; exit 1 if a target is stale */
+ KIT_MAKE_F_NO_BUILTINS = 1u << 6, /* -r: clear the built-in rules and macros */
+ KIT_MAKE_F_SILENT = 1u << 7, /* -s: do not echo recipe lines */
+ KIT_MAKE_F_TOUCH = 1u << 8, /* -t: touch targets instead of running recipes */
+ KIT_MAKE_F_POSIX = 1u << 9 /* strict POSIX mode (.POSIX / --posix) */
+};
+
+/* One NAME=VALUE macro assignment. */
+typedef struct KitMakeVar {
+ KitSlice name;
+ KitSlice value;
+} KitMakeVar;
+
+/*
+ * Host services beyond KitContext. Each callback receives the vtable's `user`
+ * pointer. Makefile bytes are NOT read here -- they come through
+ * KitContext.file_io.read_all (a stdin makefile, passed as the path "-", uses
+ * KitMakeOptions.stdin_makefile instead). Unless noted, callbacks return 0 on
+ * success and non-zero on failure.
+ */
+typedef struct KitMakeHost {
+ /* Modification time of `path` in nanoseconds since the Unix epoch. Returns 0
+ * and sets *out_ns when the path exists; returns non-zero when it is absent
+ * (a not-yet-built target or a missing prerequisite). */
+ int (*mtime)(void* user, const char* path, int64_t* out_ns);
+
+ /* Subprocess execution (recipes as {shell,"-c",cmd}; `!=` / $(shell ...) with
+ * KitExecOpts.capture_stdout). The engine calls exec->spawn then exec->wait;
+ * exec->user is used, not the KitMakeHost user. See <kit/exec.h>. */
+ const KitExec* exec;
+
+ /* -t: set `path`'s modification time to now, creating it empty if absent. */
+ int (*touch)(void* user, const char* path);
+
+ /* Remove a failed or partially-built target (also .DELETE_ON_ERROR). Returns
+ * 0 when the file was removed or was already absent. */
+ int (*remove_file)(void* user, const char* path);
+
+ void* user;
+} KitMakeHost;
+
+typedef struct KitMakeOptions {
+ const char* const* makefiles; /* -f files (a "-" entry reads stdin_makefile) */
+ size_t nmakefiles; /* 0 => default search: makefile, Makefile */
+ const char* const* targets; /* goal targets; 0 => the makefile's default goal */
+ size_t ntargets;
+ const KitMakeVar* macros; /* command-line NAME=VALUE macros (highest precedence) */
+ size_t nmacros;
+ uint32_t flags; /* KIT_MAKE_F_* */
+ int jobs; /* -j; 0 or 1 => serial (only serial is supported today) */
+ const char* const* env; /* ambient NAME=VALUE, borrowed; NULL => empty */
+ /* Absolute working/root directory (the -C target, resolved by the caller;
+ * else the cwd). make resolves relative makefile/target paths against it and
+ * runs recipes with it as cwd, so the engine never chdir()s. Also $(CURDIR).
+ * May be NULL, in which case paths resolve against the process cwd. */
+ const char* curdir;
+ const char* shell; /* default recipe shell; NULL => "/bin/sh" */
+ const uint8_t* stdin_makefile; /* bytes for a "-" makefile entry; NULL if none */
+ size_t stdin_makefile_len;
+ KitWriter* out; /* stdout sink: recipe echo, -p, -n (required) */
+ KitWriter* err; /* stderr sink: make's build diagnostics (required) */
+} KitMakeOptions;
+
+typedef struct KitMakeResult {
+ int exit_code; /* 0 ok; 1 (-q) a target is out of date; 2 build/usage error */
+} KitMakeResult;
+
+/* Read the makefiles named in `opts`, then bring the requested targets up to
+ * date. Returns KIT_OK when the make run completed (inspect result->exit_code
+ * for make's own status), or another KitStatus for an operational failure
+ * (bad arguments, allocation failure). */
+KIT_API KitStatus kit_make_run(const KitContext* ctx, const KitMakeHost* host,
+ const KitMakeOptions* opts, KitMakeResult* result);
+
+#endif
diff --git a/mk/driver_srcs.mk b/mk/driver_srcs.mk
@@ -33,6 +33,7 @@ DRIVER_TOOL_SRCS = \
$(call tool-cmd,BUILD_LIB,build) \
$(call tool-cmd,BUILD_OBJ,build) \
$(call tool-cmd,BUILD,build_coord) \
+ $(call tool-cmd,MAKE,make) \
$(call tool-cmd,INSTALL,install) \
$(call tool-cmd,CPP,cpp) \
$(call tool-cmd,AS,as) \
diff --git a/mk/lib_srcs.mk b/mk/lib_srcs.mk
@@ -34,7 +34,8 @@ LIB_SRCS_ABI_CORE = src/abi/abi.c src/abi/registry.c
LIB_SRCS_API_CORE = $(filter-out src/api/archive.c src/api/disasm.c \
src/api/link.c src/api/build.c src/api/cas.c \
src/api/build_coord.c src/api/package.c \
- src/api/compress.c src/api/image.c src/api/stubs.c, \
+ src/api/compress.c src/api/image.c src/api/make.c \
+ src/api/stubs.c, \
$(wildcard src/api/*.c))
LIB_SRCS_ARCH_CORE = $(filter-out src/arch/%_stubs.c,$(wildcard src/arch/*.c))
LIB_SRCS_ASM_CORE = $(wildcard src/asm/*.c)
@@ -165,6 +166,11 @@ LIB_SRCS_LINK := $(filter-out %/link_jit.c,$(LIB_SRCS_LINK))
endif
# Parser/lexer-generator subsystem (runtime + generator gated together).
LIB_SRCS_GRAM := $(shell find src/gram -name '*.c' 2>/dev/null)
+# POSIX make subsystem. The engine (src/make/*.c) is an amalgamation: those
+# fragments are #included by src/api/make.c and are NOT compiled standalone
+# (same pattern as the vendored lz4/monocypher shims), so only the API shim is
+# added to the build.
+LIB_SRCS_API_MAKE = src/api/make.c
LIB_SRC_ABI_AAPCS64 = src/abi/abi_aapcs64.c
LIB_SRC_ABI_APPLE_ARM64 = src/abi/abi_apple_arm64.c
@@ -209,6 +215,9 @@ endif
ifeq ($(KIT_GRAM_ENABLED),1)
LIB_SRCS += $(LIB_SRCS_GRAM)
endif
+ifeq ($(KIT_MAKE_ENABLED),1)
+LIB_SRCS += $(LIB_SRCS_API_MAKE)
+endif
ifeq ($(KIT_CAS_ENABLED),1)
LIB_SRCS += $(LIB_SRCS_API_CAS) $(LIB_SRCS_DIST_CAS) $(LIB_SRCS_VENDOR_CAS)
endif
diff --git a/mk/test.mk b/mk/test.mk
@@ -70,6 +70,7 @@ TEST_TARGETS = \
test-driver-ar \
test-driver-cpio \
test-driver-cas \
+ test-driver-make \
test-driver-cc \
test-driver-targets \
test-driver-native-macos-sdk \
@@ -338,7 +339,7 @@ test-images:
test-cf-corpus-selftest:
@bash test/lib/kit_corpus_selftest.sh
-test-driver: test-driver-help test-driver-cc test-driver-targets test-driver-native-macos-sdk test-driver-stack-protector test-driver-build test-driver-build-coord test-driver-ar test-driver-cpio test-driver-cas test-driver-strip test-driver-objcopy test-driver-objdump test-driver-pkg test-driver-strings test-driver-tools test-driver-diagnostics test-driver-wasm
+test-driver: test-driver-help test-driver-cc test-driver-targets test-driver-native-macos-sdk test-driver-stack-protector test-driver-build test-driver-build-coord test-driver-ar test-driver-cpio test-driver-cas test-driver-make test-driver-strip test-driver-objcopy test-driver-objdump test-driver-pkg test-driver-strings test-driver-tools test-driver-diagnostics test-driver-wasm
test-driver-help: bin
@KIT=$(abspath $(BIN)) sh test/driver/help_uniform.sh
@@ -453,6 +454,9 @@ test-driver-cpio: bin
test-driver-cas: bin
@KIT=$(abspath $(BIN)) sh test/cas/run.sh
+test-driver-make: bin
+ @KIT=$(abspath $(BIN)) sh test/make/run.sh
+
test-driver-strip: bin build/test/object_rewrite_test
@build/test/object_rewrite_test
diff --git a/src/api/make.c b/src/api/make.c
@@ -0,0 +1,76 @@
+/*
+ * Public POSIX make API and engine amalgamation. See <kit/make.h>.
+ *
+ * The engine ported from pdpmake is split across several src/make fragments for
+ * readability but compiled as ONE translation unit: this file includes each one,
+ * so every engine function has internal (static) linkage and none of pdpmake's
+ * generic names escapes into libkit. kit_make_run establishes the setjmp target
+ * that the engine's fatal-error path (mk_error/mk_exit) longjmps back to, then
+ * maps the make exit status into KitMakeResult.
+ */
+
+#include <kit/make.h>
+
+#include "make/make.h"
+
+/* Engine fragments (all `static`; compiled only here). */
+#include "make/utils.c" /* IWYU pragma: keep */
+#include "make/macro.c" /* IWYU pragma: keep */
+#include "make/target.c" /* IWYU pragma: keep */
+#include "make/modtime.c" /* IWYU pragma: keep */
+#include "make/rules.c" /* IWYU pragma: keep */
+#include "make/check.c" /* IWYU pragma: keep */
+#include "make/input.c" /* IWYU pragma: keep */
+#include "make/build.c" /* IWYU pragma: keep */
+#include "make/run.c" /* IWYU pragma: keep */
+
+static uint32_t make_opts_from_flags(uint32_t flags) {
+ uint32_t o = 0;
+ if (flags & KIT_MAKE_F_ENV_OVERRIDE) o |= OPT_e;
+ if (flags & KIT_MAKE_F_IGNORE_ERR) o |= OPT_i;
+ if (flags & KIT_MAKE_F_KEEP_GOING) o |= OPT_k;
+ if (flags & KIT_MAKE_F_DRY_RUN) o |= OPT_n;
+ if (flags & KIT_MAKE_F_PRINT_DB) o |= OPT_p;
+ if (flags & KIT_MAKE_F_QUESTION) o |= OPT_q;
+ if (flags & KIT_MAKE_F_NO_BUILTINS) o |= OPT_r;
+ if (flags & KIT_MAKE_F_SILENT) o |= OPT_s;
+ if (flags & KIT_MAKE_F_TOUCH) o |= OPT_t;
+ return o;
+}
+
+KitStatus kit_make_run(const KitContext* ctx, const KitMakeHost* host,
+ const KitMakeOptions* opts, KitMakeResult* result) {
+ MakeCtx mc;
+
+ if (!ctx || !ctx->heap || !host || !opts || !result) return KIT_INVALID;
+ if (!host->mtime || !host->exec || !host->exec->spawn || !host->exec->wait ||
+ !host->touch || !host->remove_file)
+ return KIT_INVALID;
+
+ result->exit_code = 2;
+
+ memset(&mc, 0, sizeof mc);
+ mc.ctx = ctx;
+ mc.host = host;
+ mc.mkopts = opts;
+ mc.out = opts->out;
+ mc.err = opts->err;
+ /* mc.env (KitExecKV pairs) is built by mk_build_child_env before recipes run;
+ * until then it is NULL (from the memset). */
+ mc.ambient = opts->env;
+ mc.root = opts->curdir;
+ mc.shell = opts->shell ? opts->shell : "/bin/sh";
+ mc.myname = "make";
+ mc.posix_level = DEFAULT_POSIX_LEVEL;
+ mc.posix = (opts->flags & KIT_MAKE_F_POSIX) != 0;
+ mc.opts = make_opts_from_flags(opts->flags);
+
+ arena_init(&mc.arena, (Heap*)ctx->heap, 64u * 1024u);
+
+ /* mc's address is taken (setjmp/&mc), so its fields survive the longjmp. */
+ if (setjmp(mc.jmpbuf) == 0) mc.exit_code = mk_run(&mc);
+
+ result->exit_code = mc.exit_code;
+ arena_fini(&mc.arena);
+ return KIT_OK;
+}
diff --git a/src/core/config_assert.c b/src/core/config_assert.c
@@ -47,6 +47,7 @@ KIT_ASSERT_BOOL(KIT_CAS_ENABLED);
KIT_ASSERT_BOOL(KIT_PKG_ENABLED);
KIT_ASSERT_BOOL(KIT_BUILD_ENABLED);
KIT_ASSERT_BOOL(KIT_COMPRESS_ENABLED);
+KIT_ASSERT_BOOL(KIT_MAKE_ENABLED);
KIT_ASSERT_BOOL(KIT_GRAM_ENABLED);
KIT_ASSERT_BOOL(KIT_INTERP_ENABLED);
KIT_ASSERT_BOOL(KIT_INTERP_THREADED);
@@ -57,6 +58,7 @@ KIT_ASSERT_BOOL(KIT_TOOL_BUILD_EXE_ENABLED);
KIT_ASSERT_BOOL(KIT_TOOL_BUILD_LIB_ENABLED);
KIT_ASSERT_BOOL(KIT_TOOL_BUILD_OBJ_ENABLED);
KIT_ASSERT_BOOL(KIT_TOOL_BUILD_ENABLED);
+KIT_ASSERT_BOOL(KIT_TOOL_MAKE_ENABLED);
KIT_ASSERT_BOOL(KIT_TOOL_INSTALL_ENABLED);
KIT_ASSERT_BOOL(KIT_TOOL_CPP_ENABLED);
KIT_ASSERT_BOOL(KIT_TOOL_AS_ENABLED);
@@ -127,6 +129,8 @@ _Static_assert(!KIT_TOOL_BUILD_OBJ_ENABLED ||
"KIT_TOOL_BUILD_OBJ_ENABLED requires link and ar support");
_Static_assert(!KIT_TOOL_BUILD_ENABLED || KIT_BUILD_ENABLED,
"KIT_TOOL_BUILD_ENABLED requires build coordinator support");
+_Static_assert(!KIT_TOOL_MAKE_ENABLED || KIT_MAKE_ENABLED,
+ "KIT_TOOL_MAKE_ENABLED requires make subsystem support");
_Static_assert(!KIT_TOOL_CPP_ENABLED || KIT_LANG_CPP_ENABLED,
"KIT_TOOL_CPP_ENABLED requires C preprocessor support");
_Static_assert(!KIT_TOOL_LD_ENABLED || KIT_LINK_ENABLED,
diff --git a/src/make/build.c b/src/make/build.c
@@ -0,0 +1,387 @@
+/*
+ * The build engine: bring a target up to date by running its recipe through
+ * the injected shell. Ported from pdpmake make.c. Recipe execution goes through
+ * host->exec (pdpmake used system()), touch through host->touch, and
+ * failed-target removal through host->remove_file. Part of the src/api/make.c
+ * amalgamation.
+ *
+ * host->exec reports the child result in *exit_status: >= 0 is the exit code,
+ * < 0 is the negated terminating signal number; the call itself returns 0 when
+ * the child ran and was reaped, non-zero when the shell could not be spawned.
+ */
+#include "make.h"
+
+static void mk_remove_target(MakeCtx* mc) {
+ if (!dryrun && !print && !precious && mc->target &&
+ !(mc->target->n_flag & (N_PRECIOUS | N_PHONY)) &&
+ mc->host->remove_file(mc->host->user, mk_path(mc, mc->target->n_name)) ==
+ 0) {
+ mk_diagnostic(mc, "'%s' removed", mc->target->n_name);
+ }
+}
+
+/*
+ * Update the modification time of a file to now (create it if absent).
+ */
+static void mk_do_touch(MakeCtx* mc, struct name* np) {
+ if (dryrun || !silent) mk_outf(mc, "touch %s\n", np->n_name);
+
+ if (!dryrun) {
+ if (mc->host->touch(mc->host->user, mk_path(mc, np->n_name)) != 0)
+ mk_warning(mc, "touch %s failed", np->n_name);
+ }
+}
+
+/*
+ * Do commands to make a target.
+ */
+static int mk_docmds(MakeCtx* mc, struct name* np, struct cmd* cp) {
+ int estat = 0;
+ char* q;
+ char* command;
+
+ for (; cp; cp = cp->c_next) {
+ uint32_t ssilent, signore, sdomake;
+
+ /* Location of command in makefile (for error messages). */
+ mc->curr_cmd = cp;
+ mc->opts &= ~OPT_make; /* We want to know if $(MAKE) is expanded. */
+ q = command = mk_expand_macros(mc, cp->c_cmd, FALSE);
+ ssilent = silent || (np->n_flag & N_SILENT) || dotouch;
+ signore = ignore || (np->n_flag & N_IGNORE);
+ sdomake = (!dryrun || doinclude || domake) && !dotouch;
+ for (;;) {
+ if (*q == '@') /* specific silent */
+ ssilent = TRUE + 1;
+ else if (*q == '-') /* specific ignore */
+ signore = TRUE;
+ else if (*q == '+') /* specific domake */
+ sdomake = TRUE + 1;
+ else
+ break;
+ do {
+ q++;
+ } while (isblank((unsigned char)*q));
+ }
+
+ if (sdomake > TRUE) {
+ /* '+' must not override '@' or .SILENT */
+ if (ssilent != TRUE + 1 && !(np->n_flag & N_SILENT)) ssilent = FALSE;
+ } else if (!sdomake)
+ ssilent = dotouch;
+
+ if (!ssilent && *q != '\0') { /* Ignore empty commands. */
+ mk_out(mc, q);
+ mk_outc(mc, '\n');
+ }
+
+ if (quest && sdomake != TRUE + 1) {
+ /* MAKE_FAILURE means rebuild is needed. */
+ estat |= MAKE_FAILURE | MAKE_DIDSOMETHING;
+ continue;
+ }
+
+ if (sdomake && *q != '\0') { /* Ignore empty commands. */
+ int status = 0;
+ int spawned;
+ char* cmd = (!signore && mc->posix) ? mk_concat3(mc, "set -e;", q, "") : q;
+ const KitExec* ex = mc->host->exec;
+ KitExecProc* proc = NULL;
+ KitSlice av[3];
+ KitExecOpts eo;
+
+ av[0] = kit_slice_cstr(mc->shell);
+ av[1] = KIT_SLICE_LIT("-c");
+ av[2] = kit_slice_cstr(cmd);
+ memset(&eo, 0, sizeof eo);
+ eo.argv = av;
+ eo.argc = 3;
+ eo.env = mc->env;
+ eo.nenv = mc->nenv;
+ eo.cwd = mc->root ? kit_slice_cstr(mc->root) : KIT_SLICE_NULL;
+ eo.search_path = 1;
+ mc->target = np;
+ spawned = ex->spawn(ex->user, &eo, &proc);
+ if (spawned == 0) spawned = ex->wait(ex->user, proc, &status, NULL, NULL);
+ /* If this command was being run to create/refresh an include file,
+ * errors are ignored and a failure status returned. */
+ if (spawned != 0) {
+ if (!doinclude)
+ mk_error(mc, "couldn't execute '%s'", q);
+ else
+ mk_warning(mc, "failed to build '%s'", np->n_name);
+ } else if (status != 0 && !signore) {
+ int signaled = status < 0;
+ int err_value = signaled ? -status : status;
+ const char* err_type = signaled ? "signal" : "exit";
+
+ if (!mc->posix && signaled) mk_remove_target(mc);
+ if (doinclude) {
+ mk_warning(mc, "failed to build '%s'", np->n_name);
+ } else {
+ if (!quest || err_value == 127)
+ mk_diagnostic(mc, "failed to build '%s' %s %d", np->n_name, err_type,
+ err_value);
+ if (errcont) {
+ estat |= MAKE_FAILURE;
+ mc->target = NULL;
+ break;
+ }
+ mk_exit(mc, 2);
+ }
+ }
+ mc->target = NULL;
+ }
+ if (sdomake || dryrun) estat = MAKE_DIDSOMETHING;
+ }
+
+ if (dotouch && !(np->n_flag & N_PHONY) && !(estat & MAKE_DIDSOMETHING)) {
+ mk_do_touch(mc, np);
+ estat = MAKE_DIDSOMETHING;
+ }
+
+ mc->curr_cmd = NULL;
+ return estat;
+}
+
+/*
+ * Remove the suffix from a name, either the one in 'tsuff' or, if NULL, one of
+ * the known suffixes.
+ */
+static char* mk_remove_suffix(MakeCtx* mc, const char* name, const char* tsuff) {
+ char* base = NULL;
+
+ if (tsuff != NULL) {
+ base = mk_has_suffix(mc, name, tsuff);
+ } else {
+ struct name* xp = mk_newname(mc, ".SUFFIXES");
+ for (struct rule* rp = xp->n_rule; rp; rp = rp->r_next) {
+ for (struct depend* dp = rp->r_dep; dp; dp = dp->d_next) {
+ base = mk_has_suffix(mc, name, dp->d_name->n_name);
+ if (base) return base;
+ }
+ }
+ }
+ return base;
+}
+
+static int mk_make1(MakeCtx* mc, struct name* np, struct cmd* cp, char* oodate,
+ char* allsrc, char* dedup, struct name* implicit,
+ const char* tsuff) {
+ char* name;
+ char* member = NULL;
+ char* base = NULL;
+ char* prereq = NULL;
+
+ name = mk_splitlib(mc, np->n_name, &member);
+ mk_setmacro(mc, "?", oodate, 0 | M_VALID);
+ if (!POSIX_2017) {
+ mk_setmacro(mc, "+", allsrc, 0 | M_VALID);
+ mk_setmacro(mc, "^", dedup, 0 | M_VALID);
+ }
+ mk_setmacro(mc, "%", member, 0 | M_VALID);
+ mk_setmacro(mc, "@", name, 0 | M_VALID);
+ if (implicit || !mc->posix) {
+ char* s;
+
+ /* As an extension, if we're not dealing with an implicit prerequisite set
+ * $< to the first out-of-date prerequisite. */
+ if (implicit == NULL) {
+ if (oodate) {
+ s = strchr(oodate, ' ');
+ if (s) *s = '\0';
+ prereq = oodate;
+ }
+ } else
+ prereq = implicit->n_name;
+
+ if (!mc->posix && member == NULL) {
+ /* Remove a suffix (not necessarily period-led) from a target, but not for
+ * lib.a(member.o) targets. */
+ base = mk_remove_suffix(mc, name, tsuff);
+ if (base) {
+ name = base;
+ }
+ } else {
+ base = member ? member : name;
+ s = mk_suffix(base);
+ /* If not implicit and the target ends with a known suffix, set $* to the
+ * stem, else to an empty string. */
+ if (implicit == NULL && !mk_is_suffix(mc, s))
+ base = NULL;
+ else
+ *s = '\0';
+ }
+ }
+ mk_setmacro(mc, "<", prereq, 0 | M_VALID);
+ mk_setmacro(mc, "*", base, 0 | M_VALID);
+
+ return mk_docmds(mc, np, cp);
+}
+
+/*
+ * Determine if the mtime of target t is <= that of prerequisite p. If tv_nsec
+ * of either is 0 assume 1-second resolution and compare only tv_sec.
+ */
+static int mk_timespec_le(const struct mk_timespec* t,
+ const struct mk_timespec* p) {
+ if (t->tv_nsec == 0 || p->tv_nsec == 0)
+ return t->tv_sec <= p->tv_sec;
+ else if (t->tv_sec < p->tv_sec)
+ return TRUE;
+ else if (t->tv_sec == p->tv_sec)
+ return t->tv_nsec <= p->tv_nsec;
+ return FALSE;
+}
+
+static const struct mk_timespec* mk_timespec_max(const struct mk_timespec* t,
+ const struct mk_timespec* p) {
+ return mk_timespec_le(t, p) ? p : t;
+}
+
+/*
+ * Recursive routine to make a target.
+ */
+static int mk_make(MakeCtx* mc, struct name* np, int level) {
+ struct depend* dp;
+ struct rule* rp;
+ struct name* impdep = NULL; /* implicit prerequisite */
+ struct rule infrule;
+ struct cmd* sc_cmd = NULL; /* commands for single-colon rule */
+ char* oodate = NULL;
+ char* allsrc = NULL;
+ char* dedup = NULL;
+ const char* tsuff = NULL;
+ struct mk_timespec dtim = {1, 0};
+ int estat = 0;
+
+ if (np->n_flag & N_DONE) return 0;
+ if (np->n_flag & N_DOING) mk_error(mc, "circular dependency for %s",
+ np->n_name);
+ np->n_flag |= N_DOING;
+
+ if (!np->n_tim.tv_sec) mk_modtime(mc, np); /* Get modtime of this file. */
+
+ if (!(np->n_flag & N_DOUBLE)) {
+ /* Find the commands for a single-colon rule, using an inference or .DEFAULT
+ * rule if needed (but, as an extension, not for phony targets). */
+ sc_cmd = mk_getcmd(np);
+ if (!sc_cmd && (mc->posix || !(np->n_flag & N_PHONY))) {
+ impdep = mk_dyndep(mc, np, &infrule, &tsuff);
+ if (impdep) {
+ sc_cmd = infrule.r_cmd;
+ mk_addrule(mc, np, infrule.r_dep, NULL, FALSE);
+ }
+ }
+
+ /* As a last resort check for a default rule. */
+ if (!(np->n_flag & N_TARGET) && np->n_tim.tv_sec == 0) {
+ if (mc->posix || !(np->n_flag & N_PHONY))
+ sc_cmd = mk_getcmd(mk_findname(mc, ".DEFAULT"));
+ if (!sc_cmd) {
+ if (doinclude) return 1;
+ mk_error(mc, "don't know how to make %s", np->n_name);
+ }
+ impdep = np;
+ }
+ } else {
+ /* If any double-colon rule has no commands we need an inference rule. */
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ if (!rp->r_cmd) {
+ /* Phony targets don't need an inference rule. */
+ if (!mc->posix && (np->n_flag & N_PHONY)) continue;
+ impdep = mk_dyndep(mc, np, &infrule, &tsuff);
+ if (!impdep) {
+ if (doinclude) return 1;
+ mk_error(mc, "don't know how to make %s", np->n_name);
+ }
+ break;
+ }
+ }
+ }
+
+ /* Reset flag to detect duplicate prerequisites. */
+ if (!(np->n_flag & N_DOUBLE)) {
+ for (rp = np->n_rule; rp; rp = rp->r_next)
+ for (dp = rp->r_dep; dp; dp = dp->d_next) dp->d_name->n_flag &= ~N_MARK;
+ }
+
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ struct name* locdep = NULL;
+
+ /* Each double-colon rule is handled separately. */
+ if ((np->n_flag & N_DOUBLE)) {
+ /* If the rule has no commands use the inference rule (unless there isn't
+ * one, as allowed for phony targets). */
+ if (!rp->r_cmd) {
+ if (impdep) {
+ locdep = impdep;
+ infrule.r_dep->d_next = rp->r_dep;
+ rp->r_dep = infrule.r_dep;
+ rp->r_cmd = infrule.r_cmd;
+ }
+ }
+ /* A rule with no prerequisites is executed unconditionally. */
+ if (!rp->r_dep) dtim = np->n_tim;
+ /* Reset flag to detect duplicate prerequisites. */
+ for (dp = rp->r_dep; dp; dp = dp->d_next) dp->d_name->n_flag &= ~N_MARK;
+ }
+ for (dp = rp->r_dep; dp; dp = dp->d_next) {
+ /* Make prerequisite. */
+ estat |= mk_make(mc, dp->d_name, level + 1);
+
+ /* Make strings of out-of-date prerequisites ($?), all prerequisites ($+)
+ * and deduplicated prerequisites ($^). */
+ if (mk_timespec_le(&np->n_tim, &dp->d_name->n_tim)) {
+ if (mc->posix || !(dp->d_name->n_flag & N_MARK))
+ oodate = mk_appendword(mc, oodate, dp->d_name->n_name);
+ }
+ allsrc = mk_appendword(mc, allsrc, dp->d_name->n_name);
+ if (!(dp->d_name->n_flag & N_MARK))
+ dedup = mk_appendword(mc, dedup, dp->d_name->n_name);
+ dp->d_name->n_flag |= N_MARK;
+ dtim = *mk_timespec_max(&dtim, &dp->d_name->n_tim);
+ }
+ if ((np->n_flag & N_DOUBLE)) {
+ if (((np->n_flag & N_PHONY) || mk_timespec_le(&np->n_tim, &dtim))) {
+ if (!(estat & MAKE_FAILURE)) {
+ estat |= mk_make1(mc, np, rp->r_cmd, oodate, allsrc, dedup, locdep,
+ tsuff);
+ dtim = (struct mk_timespec){1, 0};
+ }
+ oodate = NULL;
+ }
+ allsrc = dedup = NULL;
+ if (locdep) {
+ rp->r_dep = rp->r_dep->d_next;
+ rp->r_cmd = NULL;
+ }
+ }
+ }
+
+ np->n_flag |= N_DONE;
+ np->n_flag &= ~N_DOING;
+
+ if (!(np->n_flag & N_DOUBLE) &&
+ ((np->n_flag & N_PHONY) || (mk_timespec_le(&np->n_tim, &dtim)))) {
+ if (!(estat & MAKE_FAILURE)) {
+ if (sc_cmd)
+ estat |= mk_make1(mc, np, sc_cmd, oodate, allsrc, dedup, impdep, tsuff);
+ else if (!doinclude && level == 0 && !(estat & MAKE_DIDSOMETHING))
+ mk_warning(mc, "nothing to be done for %s", np->n_name);
+ } else if (!doinclude && !quest) {
+ mk_diagnostic(mc, "'%s' not built due to errors", np->n_name);
+ }
+ }
+
+ if (estat & MAKE_DIDSOMETHING) {
+ mk_modtime(mc, np);
+ if (!np->n_tim.tv_sec) {
+ np->n_tim.tv_sec = mc->ctx->now > 0 ? mc->ctx->now : ((int64_t)1 << 40);
+ np->n_tim.tv_nsec = 0;
+ }
+ } else if (!quest && level == 0 && !mk_timespec_le(&np->n_tim, &dtim))
+ mk_outf(mc, "%s: '%s' is up to date\n", mc->myname, np->n_name);
+
+ return estat;
+}
diff --git a/src/make/check.c b/src/make/check.c
@@ -0,0 +1,72 @@
+/*
+ * Check structures for make.
+ */
+#include "make.h"
+
+static void
+mk_print_name(MakeCtx* mc, struct name *np)
+{
+ if (np == mc->firstname)
+ mk_out(mc, "# default target\n");
+ mk_outf(mc, "%s:", np->n_name);
+ if ((np->n_flag & N_DOUBLE))
+ mk_outc(mc, ':');
+}
+
+static void
+mk_print_prerequisites(MakeCtx* mc, struct rule *rp)
+{
+ struct depend *dp;
+
+ for (dp = rp->r_dep; dp; dp = dp->d_next)
+ mk_outf(mc, " %s", dp->d_name->n_name);
+}
+
+static void
+mk_print_commands(MakeCtx* mc, struct rule *rp)
+{
+ struct cmd *cp;
+
+ for (cp = rp->r_cmd; cp; cp = cp->c_next)
+ mk_outf(mc, "\t%s\n", cp->c_cmd);
+}
+
+static void
+mk_print_details(MakeCtx* mc)
+{
+ int i;
+ struct macro *mp;
+ struct name *np;
+ struct rule *rp;
+
+ for (i = 0; i < HTABSIZE; i++)
+ for (mp = mc->macrohead[i]; mp; mp = mp->m_next)
+ mk_outf(mc, "%s = %s\n", mp->m_name, mp->m_val);
+ mk_outc(mc, '\n');
+
+ for (i = 0; i < HTABSIZE; i++) {
+ for (np = mc->namehead[i]; np; np = np->n_next) {
+ if (!(np->n_flag & N_DOUBLE)) {
+ mk_print_name(mc, np);
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ mk_print_prerequisites(mc, rp);
+ }
+ mk_outc(mc, '\n');
+
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ mk_print_commands(mc, rp);
+ }
+ mk_outc(mc, '\n');
+ } else {
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ mk_print_name(mc, np);
+ mk_print_prerequisites(mc, rp);
+ mk_outc(mc, '\n');
+
+ mk_print_commands(mc, rp);
+ mk_outc(mc, '\n');
+ }
+ }
+ }
+ }
+}
diff --git a/src/make/input.c b/src/make/input.c
@@ -0,0 +1,1031 @@
+/*
+ * Parse a makefile and build the target/rule/macro graph. Ported from pdpmake
+ * input.c. The read path no longer uses FILE*: a MakeSource iterates either an
+ * in-memory makefile buffer or the built-in rules generator; included makefiles
+ * are read through KitContext.file_io; `!=` / $(shell) run through host->capture.
+ *
+ * v1 limitation: shell-glob wildcards in prerequisites/targets are not expanded
+ * (pdpmake used <glob.h>). Names are taken literally, with backslash escapes
+ * removed, which matches the non-wildcard path exactly. Part of the
+ * src/api/make.c amalgamation.
+ */
+#include "make.h"
+
+#define mk_find_colon(p) strchr((p), ':')
+
+/* Defined below; used by mk_expand_macros for the $(shell ...) function. */
+static char* mk_run_command(MakeCtx* mc, const char* cmd);
+
+/*
+ * Return a pointer to the next blank-delimited word or NULL if none are left.
+ */
+static char* mk_gettok(char** ptr) {
+ char* p;
+
+ while (isblank((unsigned char)**ptr)) /* Skip blanks. */
+ (*ptr)++;
+
+ if (**ptr == '\0') /* Nothing after blanks. */
+ return NULL;
+
+ p = *ptr; /* Word starts here. */
+
+ while (**ptr != '\0' && !isblank((unsigned char)**ptr))
+ (*ptr)++; /* Find end of word. */
+
+ if (**ptr != '\0') *(*ptr)++ = '\0';
+
+ return p;
+}
+
+/*
+ * Skip over (possibly adjacent or nested) macro expansions.
+ */
+static char* mk_skip_macro(const char* s) {
+ while (*s && s[0] == '$') {
+ if (s[1] == '(' || s[1] == '{') {
+ char end = *++s == '(' ? ')' : '}';
+ while (*s && *s != end) s = mk_skip_macro(s + 1);
+ if (*s == end) ++s;
+ } else if (s[1] != '\0') {
+ s += 2;
+ } else {
+ break;
+ }
+ }
+ return (char*)s;
+}
+
+/*
+ * Process each whitespace-separated word: replace paths with their directory or
+ * filename part, and replace prefixes/suffixes. Returns an arena string, or
+ * NULL if the input is unmodified.
+ */
+static char* mk_modify_words(MakeCtx* mc, const char* val, int modifier,
+ size_t lenf, size_t lenr, const char* find_pref,
+ const char* repl_pref, const char* find_suff,
+ const char* repl_suff) {
+ char *s, *copy, *word, *sep, *newword, *buf = NULL;
+ size_t find_pref_len = 0, find_suff_len = 0;
+
+ if (!modifier && lenf == 0 && lenr == 0) return buf;
+
+ if (find_pref) {
+ find_pref_len = strlen(find_pref);
+ find_suff_len = lenf - find_pref_len - 1;
+ }
+
+ s = copy = mk_strdup(mc, val);
+ while ((word = mk_gettok(&s)) != NULL) {
+ newword = NULL;
+ if (modifier) {
+ sep = strrchr(word, '/');
+ if (modifier == 'D') {
+ if (!sep) {
+ word[0] = '.';
+ sep = word + 1;
+ } else if (sep == word) {
+ sep = word + 1;
+ }
+ *sep = '\0';
+ } else if (/* modifier == 'F' && */ sep) {
+ word = sep + 1;
+ }
+ }
+ if (find_pref != NULL || lenf != 0 || lenr != 0) {
+ size_t lenw = strlen(word);
+ /* Pattern macro expansions: <prefix>%<suffix>, e.g. src/%.c. */
+ if (find_pref != NULL && lenw + 1 >= lenf) {
+ if (strncmp(word, find_pref, find_pref_len) == 0 &&
+ strcmp(word + lenw - find_suff_len, find_suff) == 0) {
+ if (!repl_suff) {
+ word = newword = mk_strdup(mc, repl_pref);
+ } else {
+ word[lenw - find_suff_len] = '\0';
+ word = newword =
+ mk_concat3(mc, repl_pref, word + find_pref_len, repl_suff);
+ }
+ }
+ } else if (lenw >= lenf && strcmp(word + lenw - lenf, find_suff) == 0) {
+ word[lenw - lenf] = '\0';
+ word = newword = mk_concat3(mc, word, repl_suff, "");
+ }
+ }
+ buf = mk_appendword(mc, buf, word);
+ }
+ return buf;
+}
+
+/*
+ * Return a pointer to the next instance of a character, skipping over macro
+ * expansions so ':' and '=' inside $(VAR:.s1=.s2) aren't seen as separators.
+ */
+static char* mk_find_char(const char* str, int c) {
+ const char* s;
+
+ for (s = mk_skip_macro(str); *s; s = mk_skip_macro(s + 1)) {
+ if (*s == c) return (char*)s;
+ }
+ return NULL;
+}
+
+/*
+ * Recursively expand any macros in str to an arena string.
+ */
+static char* mk_expand_macros(MakeCtx* mc, const char* str, int except_dollar) {
+ char *exp, *newexp, *s, *t, *p, *q, *name;
+ char *find, *replace, *modified;
+ char *expval, *expfind, *find_suff, *repl_suff;
+ char *find_pref = NULL, *repl_pref = NULL;
+ size_t lenf, lenr;
+ char modifier;
+ struct macro* mp;
+
+ exp = mk_strdup(mc, str);
+ for (t = exp; *t; t++) {
+ if (*t == '$') {
+ if (t[1] == '\0') {
+ break;
+ }
+ if (t[1] == '$' && except_dollar) {
+ t++;
+ continue;
+ }
+ /* Need to expand a macro. Find its extent (s to t inclusive) and copy. */
+ s = t;
+ t++;
+ if (*t == '{' || *t == '(') {
+ t = mk_find_char(t, *t == '{' ? '}' : ')');
+ if (t == NULL) mk_error(mc, "unterminated variable '%s'", s);
+ name = mk_strndup(mc, s + 2, t - s - 2);
+ } else {
+ name = mk_alloc(mc, 2);
+ name[0] = *t;
+ name[1] = '\0';
+ }
+
+ modified = NULL;
+ /* $(shell command): a non-POSIX function; run it and substitute its
+ * stdout. Detected before the ':'/'=' parsing below, which would
+ * otherwise mangle a command containing those characters. */
+ if (!mc->posix && strncmp(name, "shell", 5) == 0 &&
+ isblank((unsigned char)name[5])) {
+ char* scmd = mk_expand_macros(mc, name + 6, FALSE);
+ modified = mk_run_command(mc, scmd);
+ goto mk_expanded;
+ }
+
+ /* Only do suffix replacement or pattern macro expansion if both ':' and
+ * '=' are found, plus a '%' for the latter. */
+ expfind = NULL;
+ find_suff = repl_suff = NULL;
+ lenf = lenr = 0;
+ if ((find = mk_find_char(name, ':'))) {
+ *find++ = '\0';
+ expfind = mk_expand_macros(mc, find, FALSE);
+ if ((replace = mk_find_char(expfind, '='))) {
+ *replace++ = '\0';
+ lenf = strlen(expfind);
+ if (!POSIX_2017 && (find_suff = strchr(expfind, '%'))) {
+ find_pref = expfind;
+ repl_pref = replace;
+ *find_suff++ = '\0';
+ if ((repl_suff = strchr(replace, '%'))) *repl_suff++ = '\0';
+ } else {
+ if (mc->posix && !(mc->pragma & P_EMPTY_SUFFIX) && lenf == 0)
+ mk_error(mc, "empty suffix%s",
+ ": allow with pragma empty_suffix");
+ find_suff = expfind;
+ repl_suff = replace;
+ lenr = strlen(repl_suff);
+ }
+ }
+ }
+
+ p = q = name;
+ /* If not in POSIX mode expand macros in the name. */
+ if (!POSIX_2017) {
+ char* expname = mk_expand_macros(mc, name, FALSE);
+ name = expname;
+ } else
+ /* Skip over nested expansions in name. */
+ do {
+ *q++ = *p;
+ } while ((p = mk_skip_macro(p + 1)) && *p);
+
+ /* The internal macros support 'D' and 'F' modifiers. */
+ modifier = '\0';
+ switch (name[0]) {
+ case '^':
+ case '+':
+ if (POSIX_2017) break;
+ /* fall through */
+ case '@':
+ case '%':
+ case '?':
+ case '<':
+ case '*':
+ if ((name[1] == 'D' || name[1] == 'F') && name[2] == '\0') {
+ modifier = name[1];
+ name[1] = '\0';
+ }
+ break;
+ }
+
+ modified = NULL;
+ if ((mp = mk_getmp(mc, name))) {
+ /* Recursive expansion. */
+ if (mp->m_flag) mk_error(mc, "recursive macro %s", name);
+ /* Note if we've expanded $(MAKE). */
+ if (strcmp(name, "MAKE") == 0) mc->opts |= OPT_make;
+ mp->m_flag = TRUE;
+ /* Immediate-expansion macros aren't recursively expanded. */
+ if (mp->m_immediate)
+ expval = mk_strdup(mc, mp->m_val);
+ else
+ expval = mk_expand_macros(mc, mp->m_val, FALSE);
+ mp->m_flag = FALSE;
+ modified = mk_modify_words(mc, expval, modifier, lenf, lenr, find_pref,
+ repl_pref, find_suff, repl_suff);
+ if (!modified) modified = expval;
+ }
+
+ mk_expanded:
+ if (modified && *modified) {
+ /* Text replaced by the expansion is s to t inclusive. */
+ *s = '\0';
+ newexp = mk_concat3(mc, exp, modified, t + 1);
+ t = newexp + (s - exp) + strlen(modified) - 1;
+ exp = newexp;
+ } else {
+ /* Macro wasn't expanded or expanded to nothing. Close the gap. */
+ q = t + 1;
+ t = s - 1;
+ while ((*s++ = *q++)) continue;
+ }
+ }
+ }
+ return exp;
+}
+
+/*
+ * Process a non-command line: strip comment, join escaped newlines.
+ */
+static void mk_process_line(MakeCtx* mc, char* s) {
+ char* t;
+
+ /* Strip comment. In non-POSIX mode don't treat '#' inside a macro expansion
+ * as a comment, nor a backslash-escaped '#'. */
+ if (!mc->posix) {
+ char* u = s;
+ while ((t = mk_find_char(u, '#')) && t > u && t[-1] == '\\') {
+ for (u = t; *u; ++u) u[-1] = u[0];
+ *u = '\0';
+ u = t;
+ }
+ } else
+ t = strchr(s, '#');
+ if (t) *t = '\0';
+
+ /* Replace escaped newline + leading whitespace on the next line with a single
+ * space. Stop at a non-escaped newline. */
+ for (t = s; *s && *s != '\n';) {
+ if (s[0] == '\\' && s[1] == '\n') {
+ s += 2;
+ while (isspace((unsigned char)*s)) ++s;
+ *t++ = ' ';
+ } else {
+ *t++ = *s++;
+ }
+ }
+ *t = '\0';
+}
+
+enum { MK_INITIAL = 0, MK_SKIP_LINE = 1 << 0, MK_EXPECT_ELSE = 1 << 1,
+ MK_GOT_MATCH = 1 << 2 };
+
+/*
+ * Extract strings following ifeq/ifneq and compare them. Return -1 on error.
+ */
+static int mk_compare_strings(MakeCtx* mc, char* arg1) {
+ char *arg2, *end, term, *t1, *t2;
+ int ret;
+
+ if (arg1[0] == '(')
+ term = ',';
+ else if (arg1[0] == '"' || arg1[0] == '\'')
+ term = arg1[0];
+ else
+ return -1;
+
+ arg2 = mk_find_char(++arg1, term);
+ if (arg2 == NULL) return -1;
+ *arg2++ = '\0';
+
+ if (term == ',') {
+ term = ')';
+ } else {
+ while (isspace((unsigned char)arg2[0])) arg2++;
+ if (arg2[0] == '"' || arg2[0] == '\'')
+ term = arg2[0];
+ else
+ return -1;
+ ++arg2;
+ }
+
+ end = mk_find_char(arg2, term);
+ if (end == NULL) return -1;
+ *end++ = '\0';
+
+ if (mk_gettok(&end) != NULL) mk_warning(mc, "unexpected text");
+
+ t1 = mk_expand_macros(mc, arg1, FALSE);
+ t2 = mk_expand_macros(mc, arg2, FALSE);
+ ret = strcmp(t1, t2) == 0;
+ return ret;
+}
+
+/*
+ * Process conditional directives; return TRUE if the current line is skipped.
+ */
+static int mk_skip_line(MakeCtx* mc, const char* str1) {
+ char *copy, *q, *token;
+ bool new_level = TRUE;
+ int ret = mc->cstate[mc->clevel] & MK_SKIP_LINE;
+
+ q = copy = mk_strdup(mc, str1);
+ mk_process_line(mc, copy);
+ if ((token = mk_gettok(&q)) != NULL) {
+ if (strcmp(token, "endif") == 0) {
+ if (mk_gettok(&q) != NULL) mk_error_unexpected(mc, "text");
+ if (mc->clevel == 0) mk_error_unexpected(mc, token);
+ --mc->clevel;
+ ret = TRUE;
+ goto end;
+ } else if (strcmp(token, "else") == 0) {
+ if (!(mc->cstate[mc->clevel] & MK_EXPECT_ELSE))
+ mk_error_unexpected(mc, token);
+
+ if ((mc->cstate[mc->clevel] & MK_GOT_MATCH))
+ mc->cstate[mc->clevel] |= MK_SKIP_LINE;
+ else
+ mc->cstate[mc->clevel] &= ~MK_SKIP_LINE;
+
+ token = mk_gettok(&q);
+ if (token == NULL) {
+ mc->cstate[mc->clevel] &= ~MK_EXPECT_ELSE;
+ ret = TRUE;
+ goto end;
+ } else {
+ new_level = FALSE;
+ }
+ }
+
+ if (strcmp(token, "ifdef") == 0 || strcmp(token, "ifndef") == 0 ||
+ strcmp(token, "ifeq") == 0 || strcmp(token, "ifneq") == 0) {
+ int match;
+
+ if (token[2] == 'd' || token[3] == 'd') {
+ char* name = mk_gettok(&q);
+ if (name != NULL && mk_gettok(&q) == NULL) {
+ char* t = mk_expand_macros(mc, name, FALSE);
+ struct macro* mp = mk_getmp(mc, t);
+ match = mp != NULL && mp->m_val[0] != '\0';
+ } else {
+ match = -1;
+ }
+ } else {
+ match = mk_compare_strings(mc, q);
+ }
+
+ if (match >= 0) {
+ if (new_level) {
+ if (mc->clevel == MK_IF_MAX) mk_error(mc, "nesting too deep");
+ ++mc->clevel;
+ mc->cstate[mc->clevel] = MK_EXPECT_ELSE | MK_SKIP_LINE;
+ if ((mc->cstate[mc->clevel - 1] & MK_SKIP_LINE))
+ mc->cstate[mc->clevel] |= MK_GOT_MATCH;
+ }
+
+ if (!(mc->cstate[mc->clevel] & MK_GOT_MATCH)) {
+ if (token[2] == 'n') match = !match;
+ if (match) {
+ mc->cstate[mc->clevel] &= ~MK_SKIP_LINE;
+ mc->cstate[mc->clevel] |= MK_GOT_MATCH;
+ }
+ }
+ } else {
+ mk_error(mc, "invalid condition");
+ }
+ ret = TRUE;
+ } else if (!new_level) {
+ mk_error(mc, "missing conditional");
+ }
+ }
+end:
+ return ret;
+}
+
+/*
+ * fgets-style reader over a MakeSource: the built-in rules (data == NULL) or an
+ * in-memory makefile buffer.
+ */
+static char* mk_src_fgets(MakeCtx* mc, char* s, int size, MakeSource* src) {
+ int i = 0;
+
+ if (src->data == NULL) return mk_getrules(mc, s, size);
+ if (src->pos >= src->len) return NULL;
+ while (i < size - 1 && src->pos < src->len) {
+ char c = (char)src->data[src->pos++];
+ s[i++] = c;
+ if (c == '\n') break;
+ }
+ s[i] = '\0';
+ return i ? s : NULL;
+}
+
+/*
+ * Read a newline-terminated logical line into an arena string. Backslash-escaped
+ * newlines don't terminate it; comment lines are skipped. Return NULL on EOF.
+ */
+static char* mk_readline(MakeCtx* mc, MakeSource* src, int want_command) {
+ char *p, *str = NULL;
+ int pos = 0;
+ int len = 0;
+
+ for (;;) {
+ if (len - pos > 1 && mk_src_fgets(mc, str + pos, len - pos, src) == NULL) {
+ if (pos) return str;
+ return NULL; /* EOF */
+ }
+
+ if (len - pos < 2 || (p = strchr(str + pos, '\n')) == NULL) {
+ int oldlen = len;
+ if (len) pos = len - 1;
+ len += 256;
+ str = mk_realloc(mc, str, (size_t)oldlen, (size_t)len);
+ continue;
+ }
+ mc->lineno++;
+
+ if (p != str && p[-1] == '\r') {
+ p[-1] = '\n';
+ *p-- = '\0';
+ }
+
+ if (p != str && p[-1] == '\\') {
+ pos = p - str + 1;
+ continue;
+ }
+ mc->dispno = mc->lineno;
+
+ if (mc->posix || !mk_skip_line(mc, str)) {
+ if (want_command && *str == '\t') return str;
+
+ p = str;
+ while (isblank((unsigned char)*p)) p++;
+
+ if (*p != '\n' && (mc->posix ? *str != '#' : *p != '#')) return str;
+ }
+
+ pos = 0;
+ }
+}
+
+/*
+ * Return the suffix name if the argument is a known suffix, else NULL.
+ */
+static const char* mk_is_suffix(MakeCtx* mc, const char* s) {
+ struct name* np;
+ struct rule* rp;
+ struct depend* dp;
+
+ np = mk_newname(mc, ".SUFFIXES");
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ for (dp = rp->r_dep; dp; dp = dp->d_next) {
+ if (strcmp(s, dp->d_name->n_name) == 0) return dp->d_name->n_name;
+ }
+ }
+ return NULL;
+}
+
+/*
+ * Return TRUE if s is formed by concatenating two known suffixes.
+ */
+static int mk_is_inference_target(MakeCtx* mc, const char* s) {
+ struct name* np;
+ struct rule *rp1, *rp2;
+ struct depend *dp1, *dp2;
+
+ np = mk_newname(mc, ".SUFFIXES");
+ for (rp1 = np->n_rule; rp1; rp1 = rp1->r_next) {
+ for (dp1 = rp1->r_dep; dp1; dp1 = dp1->d_next) {
+ const char* suff1 = dp1->d_name->n_name;
+ size_t len = strlen(suff1);
+
+ if (strncmp(s, suff1, len) == 0) {
+ for (rp2 = np->n_rule; rp2; rp2 = rp2->r_next) {
+ for (dp2 = rp2->r_dep; dp2; dp2 = dp2->d_next) {
+ const char* suff2 = dp2->d_name->n_name;
+ if (strcmp(s + len, suff2) == 0) return TRUE;
+ }
+ }
+ }
+ }
+ }
+ return FALSE;
+}
+
+enum {
+ T_NORMAL = 0,
+ T_SPECIAL = (1 << 0),
+ T_INFERENCE = (1 << 1),
+ T_NOPREREQ = (1 << 2),
+ T_COMMAND = (1 << 3),
+};
+
+/*
+ * Determine if s is a special target and return flags describing it.
+ */
+static int mk_target_type(MakeCtx* mc, char* s) {
+ int ret;
+ static const char* s_name[] = {
+ ".DEFAULT", ".POSIX", ".IGNORE", ".PRECIOUS",
+ ".SILENT", ".SUFFIXES", ".PHONY", ".NOTPARALLEL",
+ ".WAIT", ".PRAGMA",
+ };
+ static const uint8_t s_type[] = {
+ T_SPECIAL | T_NOPREREQ | T_COMMAND,
+ T_SPECIAL | T_NOPREREQ,
+ T_SPECIAL,
+ T_SPECIAL,
+ T_SPECIAL,
+ T_SPECIAL,
+ T_SPECIAL,
+ T_SPECIAL | T_NOPREREQ,
+ T_SPECIAL | T_NOPREREQ,
+ T_SPECIAL,
+ };
+
+ for (ret = 0; (size_t)ret < sizeof(s_name) / sizeof(s_name[0]); ret++)
+ if (strcmp(s_name[ret], s) == 0) return s_type[ret];
+
+ ret = T_NORMAL;
+ if (!mc->posix) {
+ if (mk_is_suffix(mc, s) || mk_is_inference_target(mc, s))
+ ret = T_INFERENCE | T_NOPREREQ | T_COMMAND;
+ } else {
+ /* In POSIX inference rule targets must contain one or two dots. */
+ char* sfx = mk_suffix(s);
+ if (*s == '.' && mk_is_suffix(mc, sfx)) {
+ if (s == sfx) {
+ ret = T_INFERENCE | T_NOPREREQ | T_COMMAND;
+ } else {
+ *sfx = '\0';
+ if (mk_is_suffix(mc, s)) ret = T_INFERENCE | T_NOPREREQ | T_COMMAND;
+ *sfx = '.';
+ }
+ }
+ }
+ return ret;
+}
+
+static int mk_ends_with_bracket(const char* s) {
+ const char* t = strrchr(s, ')');
+ return t && t[1] == '\0';
+}
+
+/*
+ * Process a command line: strip POSIX comments, collapse escaped newlines.
+ */
+static char* mk_process_command(MakeCtx* mc, char* s) {
+ char *t, *u;
+ int len;
+ char* outside;
+
+ if (!(mc->pragma & P_COMMAND_COMMENT) && mc->posix) {
+ /* POSIX strips comments from command lines. */
+ t = strchr(s, '#');
+ if (t) {
+ *t = '\0';
+ mk_warning(mc,
+ "comment in command removed: keep with pragma command_comment");
+ }
+ }
+
+ len = (int)strlen(s) + 1;
+ outside = mk_alloc(mc, (size_t)len);
+ memset(outside, 0, (size_t)len);
+ for (t = mk_skip_macro(s); *t; t = mk_skip_macro(t + 1)) outside[t - s] = 1;
+
+ /* Process escaped newlines. Stop at first non-escaped newline. */
+ for (t = u = s; *u && *u != '\n';) {
+ if (u[0] == '\\' && u[1] == '\n') {
+ if (POSIX_2017 || outside[u - s]) {
+ /* Outside macro: remove tab following escaped newline. */
+ *t++ = *u++;
+ *t++ = *u++;
+ u += (*u == '\t');
+ } else {
+ /* Inside macro: replace escaped newline + leading whitespace with a
+ * single space. */
+ u += 2;
+ while (isspace((unsigned char)*u)) ++u;
+ *t++ = ' ';
+ }
+ } else {
+ *t++ = *u++;
+ }
+ }
+ *t = '\0';
+ return s;
+}
+
+/*
+ * Run a command and capture its stdout for `!=` / $(shell). Returns an arena
+ * string or NULL.
+ */
+static char* mk_run_command(MakeCtx* mc, const char* cmd) {
+ uint8_t* out = NULL;
+ size_t out_len = 0;
+ int status = 0;
+ char* val;
+ char* s;
+ size_t len;
+ const KitExec* ex = mc->host->exec;
+ KitExecProc* proc = NULL;
+ KitSlice av[3];
+ KitExecOpts eo;
+
+ av[0] = kit_slice_cstr(mc->shell);
+ av[1] = KIT_SLICE_LIT("-c");
+ av[2] = kit_slice_cstr(cmd);
+ memset(&eo, 0, sizeof eo);
+ eo.argv = av;
+ eo.argc = 3;
+ eo.env = mc->env;
+ eo.nenv = mc->nenv;
+ eo.cwd = mc->root ? kit_slice_cstr(mc->root) : KIT_SLICE_NULL;
+ eo.search_path = 1;
+ eo.capture_stdout = 1;
+ if (ex->spawn(ex->user, &eo, &proc) != 0) return NULL;
+ /* Like pdpmake's run_command, use the captured output regardless of status. */
+ (void)ex->wait(ex->user, proc, &status, &out, &out_len);
+ if (out == NULL || out_len == 0) {
+ if (out) mc->ctx->heap->free(mc->ctx->heap, out, out_len);
+ return NULL;
+ }
+
+ val = mk_alloc(mc, out_len + 1);
+ memcpy(val, out, out_len);
+ val[out_len] = '\0';
+ len = out_len;
+ mc->ctx->heap->free(mc->ctx->heap, out, out_len);
+
+ /* Strip leading whitespace in POSIX mode. */
+ if (mc->posix) {
+ s = val;
+ while (isspace((unsigned char)*s)) {
+ ++s;
+ --len;
+ }
+ if (len == 0) return NULL;
+ memmove(val, s, len + 1);
+ }
+
+ /* Remove one trailing newline (BSD compatibility); others become spaces. */
+ if (val[len - 1] == '\n') val[len - 1] = '\0';
+ for (s = val; *s; ++s) {
+ if (*s == '\n') *s = ' ';
+ }
+ return val;
+}
+
+/*
+ * Remove backslash escapes from a name (the non-wildcard path of pdpmake's
+ * wildcard()); v1 does not expand shell globs.
+ */
+static void mk_deglob(char* p) {
+ char* s;
+ for (s = p; *p; ++p) {
+ if (*p == '\\' && p[1] != '\0') continue;
+ *s++ = *p;
+ }
+ *s = '\0';
+}
+
+/*
+ * Determine if a line is a target rule with an inline command; return a pointer
+ * to the semicolon separator if so, else NULL.
+ */
+static char* mk_inline_command(char* line) {
+ char* p = mk_find_char(line, ':');
+ if (p) p = strchr(p, ';');
+ return p;
+}
+
+/*
+ * Parse input from a makefile source and construct the tree structure.
+ */
+static void mk_input(MakeCtx* mc, MakeSource* src, int ilevel) {
+ char *p, *q, *s, *a, *str, *expanded, *copy;
+ char *str1, *str2;
+ struct name* np;
+ struct depend* dp;
+ struct cmd* cp;
+ int startno, count;
+ bool semicolon_cmd, seen_inference;
+ uint8_t old_clevel = mc->clevel;
+ bool dbl;
+ char* lib = NULL;
+ int nfile, i;
+ char** files;
+ bool minus;
+
+ mc->lineno = 0;
+ str1 = mk_readline(mc, src, FALSE);
+ while (str1) {
+ str2 = NULL;
+
+ /* Take a copy before non-command processing in case this is a rule with an
+ * inline command (target: prereq; command). */
+ copy = mk_strdup(mc, str1);
+ mk_process_line(mc, str1);
+ str = str1;
+
+ /* Check for an include line. */
+ if (!mc->posix)
+ while (isblank((unsigned char)*str)) ++str;
+ minus = !POSIX_2017 && *str == '-';
+ p = str + minus;
+ if (strncmp(p, "include", 7) == 0 && isblank((unsigned char)p[7])) {
+ const char* old_makefile = mc->makefile;
+ int old_lineno = mc->lineno;
+
+ if (ilevel > 16) mk_error(mc, "too many includes");
+
+ count = 0;
+ q = expanded = mk_expand_macros(mc, p + 7, FALSE);
+ while ((p = mk_gettok(&q)) != NULL) {
+ KitFileData ifd;
+
+ ++count;
+ if (!POSIX_2017) {
+ /* Try to create the include file or bring it up-to-date. */
+ mc->opts |= OPT_include;
+ mk_make(mc, mk_newname(mc, p), 1);
+ mc->opts &= ~OPT_include;
+ }
+ ifd.data = NULL;
+ ifd.size = 0;
+ ifd.token = NULL;
+ if (!mc->ctx->file_io ||
+ mc->ctx->file_io->read_all(mc->ctx->file_io->user, mk_path(mc, p),
+ &ifd) != KIT_OK) {
+ if (!minus) mk_error(mc, "can't open include file '%s'", p);
+ } else {
+ MakeSource subsrc = {(const char*)ifd.data, ifd.size, 0};
+ mc->makefile = p;
+ mk_input(mc, &subsrc, ilevel + 1);
+ if (mc->ctx->file_io->release)
+ mc->ctx->file_io->release(mc->ctx->file_io->user, &ifd);
+ mc->makefile = old_makefile;
+ mc->lineno = old_lineno;
+ }
+ if (POSIX_2017) break;
+ }
+ if (POSIX_2017) {
+ if (p == NULL || mk_gettok(&q)) mk_error(mc, "one include file per line");
+ } else if (count == 0) {
+ if (mc->posix) mk_error(mc, "no include file");
+ }
+ goto end_loop;
+ }
+
+ /* Check for a macro definition. */
+ str = str1;
+ if (POSIX_2017 && *str == '\t') mk_error(mc, "command not allowed here");
+ if (mk_find_char(str, '=') != NULL) {
+ int level = (useenv || src->data == NULL) ? 4 : 3;
+ char* copy2 = mk_strdup(mc, str);
+ char* newq = NULL;
+ char eq = '\0';
+ q = mk_find_char(copy2, '='); /* q can't be NULL */
+
+ if (q - 1 > copy2) {
+ switch (q[-1]) {
+ case ':':
+ /* '::=' and ':::=' are from POSIX 2024. */
+ if (!POSIX_2017 && q - 2 > copy2 && q[-2] == ':') {
+ if (q - 3 > copy2 && q[-3] == ':') {
+ eq = 'B'; /* BSD-style ':=' */
+ q[-3] = '\0';
+ } else {
+ eq = ':'; /* GNU-style ':=' */
+ q[-2] = '\0';
+ }
+ break;
+ }
+ /* ':=' is a non-POSIX extension. */
+ if (mc->posix) break;
+ goto set_eq;
+ case '+':
+ case '?':
+ case '!':
+ /* '+=', '?=' and '!=' are from POSIX 2024. */
+ if (POSIX_2017) break;
+ set_eq:
+ eq = q[-1];
+ q[-1] = '\0';
+ break;
+ }
+ }
+ *q++ = '\0'; /* Separate name and value. */
+ while (isblank((unsigned char)*q)) q++;
+ if ((p = strrchr(q, '\n')) != NULL) *p = '\0';
+
+ /* Expand LHS of the assignment. */
+ p = expanded = mk_expand_macros(mc, copy2, FALSE);
+ if ((a = mk_gettok(&p)) == NULL) mk_error(mc, "invalid macro assignment");
+
+ /* If the expanded LHS contains ':' and ';' it might be a target rule. */
+ if ((s = strchr(a, ':')) != NULL && strchr(s, ';') != NULL) {
+ goto try_target;
+ }
+
+ if (mk_gettok(&p)) mk_error(mc, "invalid macro assignment");
+
+ if (eq == ':') {
+ /* GNU-style ':='. Expand RHS; immediate-expansion macro. */
+ q = newq = mk_expand_macros(mc, q, FALSE);
+ level |= M_IMMEDIATE;
+ } else if (eq == 'B') {
+ /* BSD-style ':='. Expand RHS but not '$$'; delayed-expansion. */
+ q = newq = mk_expand_macros(mc, q, TRUE);
+ } else if (eq == '?' && mk_getmp(mc, a) != NULL) {
+ goto end_loop; /* Skip; macro already set. */
+ } else if (eq == '+') {
+ /* Append to current value. */
+ struct macro* mp = mk_getmp(mc, a);
+ char* rhs;
+ newq = mp && mp->m_val[0] ? mk_strdup(mc, mp->m_val) : NULL;
+ if (mp && mp->m_immediate) {
+ rhs = mk_expand_macros(mc, q, FALSE);
+ level |= M_IMMEDIATE;
+ } else {
+ rhs = q;
+ }
+ newq = mk_appendword(mc, newq, rhs);
+ q = newq;
+ } else if (eq == '!') {
+ char* cmd = mk_expand_macros(mc, q, FALSE);
+ q = newq = mk_run_command(mc, cmd);
+ }
+ mk_setmacro(mc, a, q, level);
+ (void)newq;
+ goto end_loop;
+ }
+
+ /* If we get here it must be a target rule. */
+ try_target:
+ if (*str == '\t') /* Command without target. */
+ mk_error(mc, "command not allowed here");
+ p = expanded = mk_expand_macros(mc, str, FALSE);
+
+ /* Look for colon separator. */
+ q = mk_find_colon(p);
+ if (q == NULL) mk_error(mc, "expected separator");
+
+ *q++ = '\0'; /* Separate targets and prerequisites. */
+
+ /* Double colon. */
+ dbl = !mc->posix && *q == ':';
+ if (dbl) q++;
+
+ /* Look for semicolon separator. */
+ cp = NULL;
+ s = strchr(q, ';');
+ if (s) {
+ /* Retrieve command from the original or expanded copy of the line. */
+ char* copy3 = mk_expand_macros(mc, copy, FALSE);
+ if ((p = mk_inline_command(copy)) || (p = mk_inline_command(copy3)))
+ cp = mk_newcmd(mc, mk_process_command(mc, p + 1), cp);
+ *s = '\0';
+ }
+ semicolon_cmd = cp != NULL && cp->c_cmd[0] != '\0';
+
+ /* Create list of prerequisites. */
+ dp = NULL;
+ while (((p = mk_gettok(&q)) != NULL)) {
+ char* newp = NULL;
+
+ if (!mc->posix) {
+ /* Allow prerequisites of form library(member1 member2). */
+ if (!lib) {
+ s = strchr(p, '(');
+ if (s && !mk_ends_with_bracket(s) && strchr(q, ')')) {
+ lib = p;
+ if (s[1] != '\0') {
+ p = newp = mk_concat3(mc, lib, ")", "");
+ s[1] = '\0';
+ } else {
+ continue;
+ }
+ }
+ } else if (mk_ends_with_bracket(p)) {
+ if (*p != ')') p = newp = mk_concat3(mc, lib, p, "");
+ lib = NULL;
+ if (newp == NULL) continue;
+ } else {
+ p = newp = mk_concat3(mc, lib, p, ")");
+ }
+ }
+
+ /* v1: names are literal (no glob); strip backslash escapes. */
+ nfile = 1;
+ files = &p;
+ if (!mc->posix) mk_deglob(p);
+ for (i = 0; i < nfile; ++i) {
+ if (!POSIX_2017 && strcmp(files[i], ".WAIT") == 0) continue;
+ np = mk_newname(mc, files[i]);
+ dp = mk_newdep(mc, np, dp);
+ }
+ }
+ lib = NULL;
+
+ /* Create list of commands. */
+ startno = mc->dispno;
+ while ((str2 = mk_readline(mc, src, TRUE)) && *str2 == '\t') {
+ cp = mk_newcmd(mc, mk_process_command(mc, str2), cp);
+ }
+ mc->dispno = startno;
+
+ /* Create target names and attach the rule to them. */
+ q = expanded;
+ count = 0;
+ seen_inference = FALSE;
+ while ((p = mk_gettok(&q)) != NULL) {
+ nfile = 1;
+ files = &p;
+ if (!mc->posix) mk_deglob(p);
+ for (i = 0; i < nfile; ++i)
+#define p files[i]
+ {
+ int ttype = mk_target_type(mc, p);
+
+ np = mk_newname(mc, p);
+ if (ttype != T_NORMAL) {
+ /* Enforce prerequisites/commands. */
+ if ((ttype & T_NOPREREQ) && dp) mk_error_not_allowed(mc,
+ "prerequisites",
+ p);
+ if ((ttype & T_INFERENCE)) {
+ if (semicolon_cmd) mk_error_in_inference_rule(mc, "'; command'");
+ seen_inference = TRUE;
+ }
+ if ((ttype & T_COMMAND) && !cp &&
+ !((ttype & T_INFERENCE) && !semicolon_cmd))
+ mk_error(mc, "commands required for %s", p);
+ if (!(ttype & T_COMMAND) && cp) mk_error_not_allowed(mc, "commands",
+ p);
+
+ if ((ttype & T_INFERENCE)) {
+ np->n_flag |= N_INFERENCE;
+ } else if (strcmp(p, ".DEFAULT") == 0) {
+ np->n_flag |= N_SPECIAL | N_INFERENCE;
+ } else {
+ np->n_flag |= N_SPECIAL;
+ }
+ } else if (!mc->firstname) {
+ mc->firstname = np;
+ }
+ mk_addrule(mc, np, dp, cp, dbl);
+ count++;
+ }
+#undef p
+ }
+ if (seen_inference && count != 1)
+ mk_error_in_inference_rule(mc, "multiple targets");
+
+ /* Prerequisites/commands are unused if there were no targets. */
+ if (count == 0) {
+ (void)dp;
+ (void)cp;
+ }
+
+ end_loop:
+ mc->dispno = mc->lineno;
+ str1 = str2 ? str2 : mk_readline(mc, src, FALSE);
+ (void)copy;
+ (void)expanded;
+ if (!mc->seen_first && src->data) {
+ if (mk_findname(mc, ".POSIX")) {
+ /* The first non-comment line defined .POSIX. */
+ mc->posix = TRUE;
+ }
+ mc->seen_first = TRUE;
+ }
+ }
+ /* Conditionals aren't allowed to span files. */
+ if (mc->clevel != old_clevel) mk_error(mc, "invalid conditional");
+}
diff --git a/src/make/macro.c b/src/make/macro.c
@@ -0,0 +1,112 @@
+/*
+ * Macro control for make. Ported from pdpmake macro.c. Part of the
+ * src/api/make.c amalgamation.
+ */
+#include "make.h"
+
+static struct macro* mk_getmp(MakeCtx* mc, const char* name) {
+ struct macro* mp;
+
+ for (mp = mc->macrohead[mk_getbucket(name)]; mp; mp = mp->m_next)
+ if (strcmp(name, mp->m_name) == 0)
+ return mp;
+ return NULL;
+}
+
+static int mk_is_valid_macro(MakeCtx* mc, const char* name) {
+ const char* s;
+ for (s = name; *s; ++s) {
+ // In POSIX mode only a limited set of characters are guaranteed
+ // to be allowed in macro names.
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ if (mc->posix)
+#endif
+ {
+ // Find the appropriate character set
+ if (((
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ (mc->pragma & P_MACRO_NAME) ||
+#endif
+#if ENABLE_FEATURE_MAKE_POSIX_2024
+ !POSIX_2017
+#else
+ FALSE
+#endif
+ )
+ ? !isfname(*s)
+ : !ispname(*s)))
+ return FALSE;
+ }
+ // As an extension allow anything that can get through the
+ // input parser, apart from the following.
+ if (*s == '=')
+ return FALSE;
+#if ENABLE_FEATURE_MAKE_POSIX_2024
+ if (isblank(*s) || iscntrl(*s))
+ return FALSE;
+#endif
+ }
+ return TRUE;
+}
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+static int mk_potentially_valid_macro(MakeCtx* mc, const char* name) {
+ int ret = FALSE;
+
+ if (!(mc->pragma & P_MACRO_NAME)) {
+ mc->pragma |= P_MACRO_NAME;
+ ret = mk_is_valid_macro(mc, name);
+ mc->pragma &= ~P_MACRO_NAME;
+ }
+ return ret;
+}
+#endif
+
+static void mk_setmacro(MakeCtx* mc, const char* name, const char* val,
+ int level) {
+ struct macro* mp;
+ bool valid = level & M_VALID;
+ bool from_env = level & M_ENVIRON;
+#if ENABLE_FEATURE_MAKE_EXTENSIONS || ENABLE_FEATURE_MAKE_POSIX_2024
+ bool immediate = level & M_IMMEDIATE;
+#endif
+
+ level &= ~(M_IMMEDIATE | M_VALID | M_ENVIRON);
+ mp = mk_getmp(mc, name);
+ if (mp) {
+ // Don't replace existing macro from a lower level
+ if (level > mp->m_level)
+ return;
+
+ // Replace existing macro
+ } else {
+ // If not defined, allocate space for new
+ unsigned int bucket;
+
+ if (!valid && !mk_is_valid_macro(mc, name)) {
+ // Silently drop invalid names from the environment
+ if (from_env)
+ return;
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ mk_error(mc, "invalid macro name '%s'%s", name,
+ mk_potentially_valid_macro(mc, name)
+ ? ": allow with pragma macro_name"
+ : "");
+#else
+ mk_error(mc, "invalid macro name '%s'", name);
+#endif
+ }
+
+ bucket = mk_getbucket(name);
+ mp = mk_alloc(mc, sizeof(struct macro));
+ mp->m_next = mc->macrohead[bucket];
+ mc->macrohead[bucket] = mp;
+ mp->m_flag = FALSE;
+ mp->m_name = mk_strdup(mc, name);
+ }
+#if ENABLE_FEATURE_MAKE_EXTENSIONS || ENABLE_FEATURE_MAKE_POSIX_2024
+ mp->m_immediate = immediate;
+#endif
+ mp->m_level = level;
+ mp->m_val = mk_strdup(mc, val ? val : "");
+}
diff --git a/src/make/make.h b/src/make/make.h
@@ -0,0 +1,332 @@
+#ifndef KIT_MAKE_INTERNAL_H
+#define KIT_MAKE_INTERNAL_H
+
+/*
+ * Internal header for the make engine ported from the public-domain pdpmake
+ * (rmyorston/pdpmake).
+ *
+ * Amalgamation. The engine is compiled as ONE translation unit: src/api/make.c
+ * includes each engine fragment (src/make/<part>.c) after this header. Every
+ * engine function is `static`, so none of pdpmake's generic names (error,
+ * input, make, target, ...) escapes into libkit's symbol space.
+ *
+ * No global state. Every pdpmake global now lives in MakeCtx, which is threaded
+ * as the first parameter `mc` of every engine function that touches shared
+ * state. The read-only option predicates below (dryrun, silent, ...) are the
+ * one sanctioned convenience macro family; they read mc->opts and therefore
+ * require `mc` to be in scope (it always is -- it is the first parameter).
+ *
+ * No libc/OS calls. Memory comes from an arena (mk_alloc et al.); makefile and
+ * archive bytes are read through KitContext.file_io; recipe execution, file
+ * modification times, touch, and remove arrive through the KitMakeHost vtable;
+ * output goes to KitWriter sinks; a fatal error longjmps back to kit_make_run
+ * instead of calling exit().
+ */
+
+#include <kit/core.h>
+#include <kit/make.h>
+
+#include <ctype.h>
+#include <limits.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "core/arena.h"
+
+/* pdpmake feature selection: extensions + POSIX-2024 both on (its defaults). */
+#define ENABLE_FEATURE_MAKE_EXTENSIONS 1
+#define ENABLE_FEATURE_MAKE_POSIX_2024 1
+#define IF_FEATURE_MAKE_EXTENSIONS(...) __VA_ARGS__
+#define IF_NOT_FEATURE_MAKE_EXTENSIONS(...)
+#define IF_FEATURE_MAKE_POSIX_2024(...) __VA_ARGS__
+#define IF_NOT_FEATURE_MAKE_POSIX_2024(...)
+
+#define STD_POSIX_2017 0
+#define STD_POSIX_2024 1
+#define DEFAULT_POSIX_LEVEL STD_POSIX_2024
+
+#ifndef TRUE
+#define TRUE (1)
+#define FALSE (0)
+#endif
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+#define HTABSIZE 199
+
+/* ---- data structures (verbatim from pdpmake, layout preserved) ----------- */
+
+/* A file, either to be made or pre-existing. A timespec models the mtime; the
+ * host reports mtime in ns, which mk_modtime splits into sec/nsec. */
+struct mk_timespec {
+ int64_t tv_sec;
+ int64_t tv_nsec;
+};
+
+struct name {
+ struct name* n_next; /* next in hash chain */
+ char* n_name;
+ struct rule* n_rule; /* rules to build this */
+ struct mk_timespec n_tim;
+ uint16_t n_flag;
+};
+
+#define N_DOING 0x01 /* being built */
+#define N_DONE 0x02 /* looked at */
+#define N_TARGET 0x04 /* is a target */
+#define N_PRECIOUS 0x08 /* precious */
+#define N_DOUBLE 0x10 /* double-colon target */
+#define N_SILENT 0x20 /* build silently */
+#define N_IGNORE 0x40 /* ignore build errors */
+#define N_SPECIAL 0x80 /* special target */
+#define N_MARK 0x100 /* mark for deduplication */
+#define N_PHONY 0x200 /* phony target */
+#define N_INFERENCE 0x400 /* inference rule */
+
+struct rule {
+ struct rule* r_next;
+ struct depend* r_dep; /* prerequisites */
+ struct cmd* r_cmd; /* commands */
+};
+
+struct depend {
+ struct depend* d_next;
+ struct name* d_name;
+ int d_refcnt;
+};
+
+struct cmd {
+ struct cmd* c_next;
+ char* c_cmd;
+ int c_refcnt;
+ const char* c_makefile;
+ int c_dispno;
+};
+
+struct macro {
+ struct macro* m_next;
+ char* m_name;
+ char* m_val;
+ bool m_immediate; /* set with ::= */
+ bool m_flag; /* infinite-loop guard */
+ uint8_t m_level; /* level at which created */
+};
+
+/* Flags to mk_setmacro (packed into the level argument). */
+#define M_IMMEDIATE 0x08
+#define M_VALID 0x10
+#define M_ENVIRON 0x20
+
+/* .PRAGMA bits. Order must match p_name[] in target.c. */
+enum {
+ BIT_MACRO_NAME = 0,
+ BIT_TARGET_NAME,
+ BIT_COMMAND_COMMENT,
+ BIT_EMPTY_SUFFIX,
+ BIT_POSIX_2017,
+ BIT_POSIX_2024,
+ BIT_POSIX_202X,
+
+ P_MACRO_NAME = (1 << BIT_MACRO_NAME),
+ P_TARGET_NAME = (1 << BIT_TARGET_NAME),
+ P_COMMAND_COMMENT = (1 << BIT_COMMAND_COMMENT),
+ P_EMPTY_SUFFIX = (1 << BIT_EMPTY_SUFFIX)
+};
+
+/* Option bits (mc->opts). The public KIT_MAKE_F_* flags map onto these in
+ * run.c; the extra bits (precious/phony/include/make) are engine-internal. */
+enum {
+ OPTBIT_e = 0,
+ OPTBIT_i,
+ OPTBIT_k,
+ OPTBIT_n,
+ OPTBIT_q,
+ OPTBIT_r,
+ OPTBIT_s,
+ OPTBIT_t,
+ OPTBIT_p,
+ OPTBIT_precious,
+ OPTBIT_phony,
+ OPTBIT_include,
+ OPTBIT_make,
+
+ OPT_e = (1 << OPTBIT_e),
+ OPT_i = (1 << OPTBIT_i),
+ OPT_k = (1 << OPTBIT_k),
+ OPT_n = (1 << OPTBIT_n),
+ OPT_q = (1 << OPTBIT_q),
+ OPT_r = (1 << OPTBIT_r),
+ OPT_s = (1 << OPTBIT_s),
+ OPT_t = (1 << OPTBIT_t),
+ OPT_p = (1 << OPTBIT_p),
+ OPT_precious = (1 << OPTBIT_precious),
+ OPT_phony = (1 << OPTBIT_phony),
+ OPT_include = (1 << OPTBIT_include),
+ OPT_make = (1 << OPTBIT_make)
+};
+
+/* make() return status. */
+#define MAKE_FAILURE 0x01
+#define MAKE_DIDSOMETHING 0x02
+
+/* Conditional-directive nesting (ifdef/ifeq...). */
+#define MK_IF_MAX 10
+
+/* An input source for the parser: either an in-memory makefile buffer (data !=
+ * NULL, read line by line from pos) or, when data == NULL, the built-in rules
+ * generated by mk_getrules. */
+typedef struct MakeSource {
+ const char* data;
+ size_t len;
+ size_t pos;
+} MakeSource;
+
+/* The engine context: every former pdpmake global, plus the injected host. */
+typedef struct MakeCtx {
+ /* injected substrate */
+ const KitContext* ctx; /* heap / file_io / diag / now (borrowed) */
+ const KitMakeHost* host; /* mtime / run_recipe / capture / touch / remove */
+ const KitMakeOptions* mkopts; /* makefiles / targets / macros / flags */
+ Arena arena; /* all engine allocations */
+ KitWriter* out; /* stdout: recipe echo, -p, -n */
+ KitWriter* err; /* stderr: make diagnostics */
+
+ /* error handling: mk_error/mk_exit longjmp here; kit_make_run reads code */
+ jmp_buf jmpbuf;
+ int exit_code;
+
+ /* recipe launch context */
+ const KitExecKV* env; /* child env for recipes (KitExecKV pairs, run.c) */
+ size_t nenv;
+ const char* const* ambient; /* borrowed ambient NAME=VALUE (from options) */
+ const char* root; /* absolute working dir: path root, recipe cwd, $(CURDIR) */
+ const char* shell; /* recipe shell (SHELL macro, default /bin/sh) */
+
+ /* former globals */
+ struct name* namehead[HTABSIZE];
+ struct macro* macrohead[HTABSIZE];
+ struct name* firstname; /* default goal */
+ struct name* target; /* target currently being built (for remove) */
+ uint32_t opts;
+ int lineno;
+ int dispno;
+ struct cmd* curr_cmd;
+ const char* makefile; /* current makefile name (diagnostics) */
+ const char* myname; /* "make" */
+ bool posix;
+ bool seen_first;
+ unsigned char pragma;
+ unsigned char posix_level;
+
+ /* conditional-directive state (was file statics in input.c) */
+ uint8_t clevel;
+ uint8_t cstate[MK_IF_MAX + 1];
+
+ /* built-in rules generator state (was function statics in rules.c) */
+ const char* rulepos;
+ int rule_idx;
+} MakeCtx;
+
+/* Read-only option predicates. Require `mc` in scope (always the first
+ * parameter of an engine function). */
+#define useenv (mc->opts & OPT_e)
+#define ignore (mc->opts & OPT_i)
+#define errcont (mc->opts & OPT_k)
+#define dryrun (mc->opts & OPT_n)
+#define print (mc->opts & OPT_p)
+#define quest (mc->opts & OPT_q)
+#define norules (mc->opts & OPT_r)
+#define silent (mc->opts & OPT_s)
+#define dotouch (mc->opts & OPT_t)
+#define precious (mc->opts & OPT_precious)
+#define doinclude (mc->opts & OPT_include)
+#define domake (mc->opts & OPT_make)
+
+#define POSIX_2017 (mc->posix && mc->posix_level == STD_POSIX_2017)
+
+/* Character-class predicates for names. */
+#define ispname(c) (isalpha(c) || isdigit(c) || (c) == '.' || (c) == '_')
+#define isfname(c) (ispname(c) || (c) == '-')
+
+/* ---- memory + diagnostics + output helpers (utils.c) --------------------- */
+
+static void* mk_alloc(MakeCtx* mc, size_t n);
+static void* mk_realloc(MakeCtx* mc, void* p, size_t oldn, size_t newn);
+static char* mk_strdup(MakeCtx* mc, const char* s);
+static char* mk_strndup(MakeCtx* mc, const char* s, size_t n);
+static char* mk_concat3(MakeCtx* mc, const char* a, const char* b,
+ const char* c);
+static char* mk_appendword(MakeCtx* mc, const char* str, const char* word);
+
+#define MK_NORETURN __attribute__((noreturn))
+static void mk_exit(MakeCtx* mc, int code) MK_NORETURN; /* longjmp, no message */
+static void mk_error(MakeCtx* mc, const char* fmt, ...)
+ MK_NORETURN; /* message + mk_exit(2) */
+static void mk_warning(MakeCtx* mc, const char* fmt, ...);
+static void mk_diagnostic(MakeCtx* mc, const char* fmt, ...);
+static void mk_error_unexpected(MakeCtx* mc, const char* s) MK_NORETURN;
+static void mk_error_in_inference_rule(MakeCtx* mc, const char* s) MK_NORETURN;
+static void mk_error_not_allowed(MakeCtx* mc, const char* s, const char* t)
+ MK_NORETURN;
+
+static void mk_out(MakeCtx* mc, const char* s); /* to mc->out */
+static void mk_out_bytes(MakeCtx* mc, const char* p, size_t n);
+static void mk_outc(MakeCtx* mc, char c);
+static void mk_outf(MakeCtx* mc, const char* fmt, ...); /* to mc->out */
+
+static unsigned mk_getbucket(const char* name);
+
+/* Resolve `name` against mc->root (a relative name gets joined to root; an
+ * absolute name or a NULL root is returned unchanged). Used for every path the
+ * engine hands to a host file op, so make never chdir()s. */
+static const char* mk_path(MakeCtx* mc, const char* name);
+
+/* ---- macro.c ------------------------------------------------------------- */
+static struct macro* mk_getmp(MakeCtx* mc, const char* name);
+static void mk_setmacro(MakeCtx* mc, const char* name, const char* val,
+ int level);
+
+/* ---- target.c ------------------------------------------------------------ */
+static struct depend* mk_newdep(MakeCtx* mc, struct name* np,
+ struct depend* dphead);
+static struct cmd* mk_newcmd(MakeCtx* mc, char* str, struct cmd* cphead);
+static struct name* mk_findname(MakeCtx* mc, const char* name);
+static struct name* mk_newname(MakeCtx* mc, const char* name);
+static struct cmd* mk_getcmd(struct name* np);
+static void mk_freerules(struct rule* rp);
+static int mk_is_valid_target(MakeCtx* mc, const char* name);
+static void mk_set_pragma(MakeCtx* mc, const char* name);
+static void mk_addrule(MakeCtx* mc, struct name* np, struct depend* dp,
+ struct cmd* cp, int flag);
+
+/* ---- check.c ------------------------------------------------------------- */
+static void mk_print_details(MakeCtx* mc);
+
+/* ---- rules.c ------------------------------------------------------------- */
+static char* mk_suffix(const char* name);
+static char* mk_has_suffix(MakeCtx* mc, const char* name, const char* suffix);
+static struct name* mk_dyndep(MakeCtx* mc, struct name* np,
+ struct rule* infrule, const char** ptsuff);
+static char* mk_getrules(MakeCtx* mc, char* s, int size);
+
+/* ---- input.c ------------------------------------------------------------- */
+static char* mk_expand_macros(MakeCtx* mc, const char* str, int except_dollar);
+static const char* mk_is_suffix(MakeCtx* mc, const char* s);
+static void mk_input(MakeCtx* mc, MakeSource* src, int ilevel);
+
+/* ---- modtime.c ----------------------------------------------------------- */
+static char* mk_splitlib(MakeCtx* mc, const char* name, char** member);
+static void mk_modtime(MakeCtx* mc, struct name* np);
+
+/* ---- build.c ------------------------------------------------------------- */
+static void mk_remove_target(MakeCtx* mc);
+static int mk_make(MakeCtx* mc, struct name* np, int level);
+
+/* ---- run.c --------------------------------------------------------------- */
+static int mk_run(MakeCtx* mc);
+
+#endif
diff --git a/src/make/modtime.c b/src/make/modtime.c
@@ -0,0 +1,148 @@
+/*
+ * Modification times of files and archive members. Ported from pdpmake
+ * modtime.c. File mtimes come from the injected host->mtime callback; archive
+ * member timestamps are parsed from the archive bytes read through
+ * KitContext.file_io (pdpmake used <ar.h> + fopen/fread/fseek). Part of the
+ * src/api/make.c amalgamation.
+ */
+#include "make.h"
+
+/* ar(5) constants (System V / GNU format). */
+#define MK_SARMAG 8
+enum { AR_NAME = 0, AR_DATE = 16, AR_SIZE = 48, AR_FMAG = 58, AR_HDRLEN = 60 };
+
+/*
+ * Read a right-blank-padded decimal number from an archive header field.
+ */
+static size_t mk_argetnum(MakeCtx* mc, const char* str, int len) {
+ const char* s;
+ size_t val = 0;
+ for (s = str; s < str + len && isdigit((unsigned char)*s); s++) {
+ if (val > (size_t)(INT_MAX - 1) / 10) break;
+ val = val * 10 + (size_t)(*s - '0');
+ }
+ if (s != str + len && *s != ' ') mk_error(mc, "invalid archive");
+ return val;
+}
+
+/*
+ * Search a System V/GNU archive image for the given member and return its
+ * timestamp (seconds), or 0 if not found.
+ */
+static int64_t mk_arsearch(MakeCtx* mc, const uint8_t* data, size_t len,
+ const char* member) {
+ size_t pos = MK_SARMAG;
+ const char* names = NULL;
+ size_t names_len = 0;
+ size_t memlen = strlen(member);
+
+ while (pos + AR_HDRLEN <= len) {
+ const char* hdr = (const char*)data + pos;
+ size_t msize;
+ const char* t;
+ size_t namelim;
+ size_t nl;
+
+ if (memcmp(hdr + AR_FMAG, "`\n", 2) != 0) mk_error(mc, "invalid archive");
+ msize = mk_argetnum(mc, hdr + AR_SIZE, 10);
+ pos += AR_HDRLEN;
+ if (msize > len - pos) msize = len - pos; /* clamp to buffer */
+
+ t = hdr + AR_NAME;
+ namelim = 16;
+ if (hdr[AR_NAME] == '/') {
+ if (hdr[AR_NAME + 1] == ' ') {
+ /* Symbol table: skip. */
+ pos += msize + (msize & 1);
+ continue;
+ } else if (hdr[AR_NAME + 1] == '/' && names == NULL) {
+ /* Extended filename table: remember it for later. */
+ names = (const char*)data + pos;
+ names_len = msize;
+ pos += msize + (msize & 1);
+ continue;
+ } else if (isdigit((unsigned char)hdr[AR_NAME + 1]) && names) {
+ size_t off = mk_argetnum(mc, hdr + AR_NAME + 1, 15);
+ if (off > names_len) mk_error(mc, "invalid archive");
+ t = names + off;
+ namelim = names_len - off;
+ } else {
+ mk_error(mc, "invalid archive");
+ }
+ }
+
+ /* The member name runs up to the terminating '/'. */
+ nl = 0;
+ while (nl < namelim && t[nl] != '/') nl++;
+ if (nl == namelim) mk_error(mc, "invalid archive");
+
+ if (nl == memlen && memcmp(t, member, memlen) == 0)
+ return (int64_t)mk_argetnum(mc, hdr + AR_DATE, 12);
+
+ pos += msize + (msize & 1);
+ }
+ return 0;
+}
+
+static int64_t mk_artime(MakeCtx* mc, const char* archive, const char* member) {
+ KitFileData fd;
+ const KitFileIO* io = mc->ctx->file_io;
+ int64_t mtime;
+
+ fd.data = NULL;
+ fd.size = 0;
+ fd.token = NULL;
+ if (!io || io->read_all(io->user, archive, &fd) != KIT_OK) return 0;
+ if (fd.size < MK_SARMAG ||
+ memcmp(fd.data, "!<arch>\n", MK_SARMAG) != 0) {
+ if (io->release) io->release(io->user, &fd);
+ mk_error(mc, "%s: not an archive", archive);
+ }
+ mtime = mk_arsearch(mc, fd.data, fd.size, member);
+ if (io->release) io->release(io->user, &fd);
+ return mtime;
+}
+
+/*
+ * If name is 'libname(member.o)' split it into name and member parts; else copy
+ * name and leave *member alone. The return value is an arena string.
+ */
+static char* mk_splitlib(MakeCtx* mc, const char* name, char** member) {
+ char* s;
+ char* t;
+ size_t len;
+
+ t = mk_strdup(mc, name);
+ s = strchr(t, '(');
+ if (s) {
+ *s++ = '\0';
+ len = strlen(s);
+ if (len <= 1 || s[len - 1] != ')' || *t == '\0')
+ mk_error(mc, "invalid name '%s'", name);
+ s[len - 1] = '\0';
+ *member = s;
+ }
+ return t;
+}
+
+/*
+ * Get the modification time of a file (or archive member). Set it to 0 if the
+ * file doesn't exist.
+ */
+static void mk_modtime(MakeCtx* mc, struct name* np) {
+ char* name;
+ char* member = NULL;
+ int64_t ns;
+
+ name = mk_splitlib(mc, np->n_name, &member);
+ if (member) {
+ np->n_tim.tv_sec = mk_artime(mc, mk_path(mc, name), member);
+ np->n_tim.tv_nsec = 0;
+ } else if (mc->host->mtime(mc->host->user, mk_path(mc, name), &ns) == 0) {
+ np->n_tim.tv_sec = ns / 1000000000;
+ np->n_tim.tv_nsec = ns % 1000000000;
+ } else {
+ np->n_tim.tv_sec = 0;
+ np->n_tim.tv_nsec = 0;
+ }
+}
diff --git a/src/make/rules.c b/src/make/rules.c
@@ -0,0 +1,305 @@
+/*
+ * Control of the implicit suffix rules
+ */
+#include "make.h"
+
+/*
+ * Return a pointer to the suffix of a name (which may be the
+ * terminating NUL if there's no suffix).
+ */
+static char *
+mk_suffix(const char *name)
+{
+ char *p = strrchr(name, '.');
+ return p ? p : (char *)name + strlen(name);
+}
+
+/*
+ * Find a name structure whose name is formed by concatenating two
+ * strings. If 'create' is TRUE the name is created if necessary.
+ */
+static struct name *
+mk_namecat(MakeCtx* mc, const char *s, const char *t, int create)
+{
+ char *p;
+ struct name *np;
+
+ p = mk_concat3(mc, s, t, "");
+ np = create ? mk_newname(mc, p) : mk_findname(mc, p);
+ return np;
+}
+
+/*
+ * Search for an inference rule to convert some suffix ('psuff')
+ * to the target suffix 'tsuff'. The basename of the prerequisite
+ * is 'base'.
+ */
+static struct name *
+mk_dyndep0(MakeCtx* mc, char *base, const char *tsuff, struct rule *infrule)
+{
+ char *psuff;
+ struct name *xp; // Suffixes
+ struct name *sp; // Suffix rule
+ struct rule *rp;
+ struct depend *dp;
+ IF_NOT_FEATURE_MAKE_EXTENSIONS(const) bool chain = FALSE;
+
+ xp = mk_newname(mc, ".SUFFIXES");
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ retry:
+#endif
+ for (rp = xp->n_rule; rp; rp = rp->r_next) {
+ for (dp = rp->r_dep; dp; dp = dp->d_next) {
+ // Generate new suffix rule to try
+ psuff = dp->d_name->n_name;
+ sp = mk_namecat(mc, psuff, tsuff, FALSE);
+ if (sp && sp->n_rule) {
+ struct name *ip;
+ int got_ip;
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ // Has rule already been used in this chain?
+ if ((sp->n_flag & N_MARK))
+ continue;
+#endif
+ // Generate a name for an implicit prerequisite
+ ip = mk_namecat(mc, base, psuff, TRUE);
+ if ((ip->n_flag & N_DOING))
+ continue;
+
+ if (!ip->n_tim.tv_sec)
+ mk_modtime(mc, ip);
+
+ if (!chain) {
+ got_ip = ip->n_tim.tv_sec || (ip->n_flag & N_TARGET);
+ }
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ else {
+ sp->n_flag |= N_MARK;
+ got_ip = mk_dyndep(mc, ip, NULL, NULL) != NULL;
+ sp->n_flag &= ~N_MARK;
+ }
+#endif
+
+ if (got_ip) {
+ // Prerequisite exists or we know how to make it
+ if (infrule) {
+ infrule->r_dep = mk_newdep(mc, ip, NULL);
+ infrule->r_cmd = sp->n_rule->r_cmd;
+ }
+ return ip;
+ }
+ }
+ }
+ }
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ // If we didn't find an existing file or an explicit rule try
+ // again, this time looking for a chained inference rule.
+ if (!mc->posix && !chain) {
+ chain = TRUE;
+ goto retry;
+ }
+#endif
+ return NULL;
+}
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+/*
+ * If 'name' ends with 'suffix' return an allocated string containing
+ * the name with the suffix removed, else return NULL.
+ */
+static char *
+mk_has_suffix(MakeCtx* mc, const char *name, const char *suffix)
+{
+ ptrdiff_t delta = strlen(name) - strlen(suffix);
+ char *base = NULL;
+
+ if (delta > 0 && strcmp(name + delta, suffix) == 0) {
+ base = mk_strdup(mc, name);
+ base[delta] = '\0';
+ }
+
+ return base;
+}
+#endif
+
+/*
+ * Dynamic dependency. This routine applies the suffix rules
+ * to try and find a source and a set of rules for a missing
+ * target. NULL is returned on failure. On success the name of
+ * the implicit prerequisite is returned and the rule used is
+ * placed in the infrule structure provided by the caller.
+ */
+static struct name *
+mk_dyndep(MakeCtx* mc, struct name *np, struct rule *infrule, const char **ptsuff)
+{
+ const char *tsuff;
+ char *base, *name, *member;
+ struct name *pp = NULL; // Implicit prerequisite
+
+ member = NULL;
+ name = mk_splitlib(mc, np->n_name, &member);
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ // POSIX only allows inference rules with one or two periods.
+ // As an extension this restriction is lifted, but not for
+ // targets of the form lib.a(member.o).
+ if (!mc->posix && member == NULL) {
+ struct name *xp = mk_newname(mc, ".SUFFIXES");
+ int found_suffix = FALSE;
+
+ for (struct rule *rp = xp->n_rule; rp; rp = rp->r_next) {
+ for (struct depend *dp = rp->r_dep; dp; dp = dp->d_next) {
+ tsuff = dp->d_name->n_name;
+ base = mk_has_suffix(mc, name, tsuff);
+ if (base) {
+ found_suffix = TRUE;
+ pp = mk_dyndep0(mc, base, tsuff, infrule);
+ if (pp) {
+ goto done;
+ }
+ }
+ }
+ }
+
+ if (!found_suffix) {
+ // The name didn't have a known suffix. Try single-suffix rule.
+ tsuff = "";
+ pp = mk_dyndep0(mc, name, tsuff, infrule);
+ if (pp) {
+ done:
+ if (ptsuff) {
+ *ptsuff = tsuff;
+ }
+ }
+ }
+ } else
+#endif
+ {
+ tsuff = mk_strdup(mc, mk_suffix(name));
+ base = member ? member : name;
+ *mk_suffix(base) = '\0';
+
+ pp = mk_dyndep0(mc, base, tsuff, infrule);
+ }
+
+ return pp;
+}
+
+#define RULES \
+ ".c.o:\n" \
+ " $(CC) $(CFLAGS) -c $<\n" \
+ ".y.o:\n" \
+ " $(YACC) $(YFLAGS) $<\n" \
+ " $(CC) $(CFLAGS) -c y.tab.c\n" \
+ " rm -f y.tab.c\n" \
+ " mv y.tab.o $@\n" \
+ ".y.c:\n" \
+ " $(YACC) $(YFLAGS) $<\n" \
+ " mv y.tab.c $@\n" \
+ ".l.o:\n" \
+ " $(LEX) $(LFLAGS) $<\n" \
+ " $(CC) $(CFLAGS) -c lex.yy.c\n" \
+ " rm -f lex.yy.c\n" \
+ " mv lex.yy.o $@\n" \
+ ".l.c:\n" \
+ " $(LEX) $(LFLAGS) $<\n" \
+ " mv lex.yy.c $@\n" \
+ ".c.a:\n" \
+ " $(CC) -c $(CFLAGS) $<\n" \
+ " $(AR) $(ARFLAGS) $@ $*.o\n" \
+ " rm -f $*.o\n" \
+ ".c:\n" \
+ " $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<\n" \
+ ".sh:\n" \
+ " cp $< $@\n" \
+ " chmod a+x $@\n"
+
+#define RULES_2017 \
+ ".SUFFIXES:.o .c .y .l .a .sh .f\n" \
+ ".f.o:\n" \
+ " $(FC) $(FFLAGS) -c $<\n" \
+ ".f.a:\n" \
+ " $(FC) -c $(FFLAGS) $<\n" \
+ " $(AR) $(ARFLAGS) $@ $*.o\n" \
+ " rm -f $*.o\n" \
+ ".f:\n" \
+ " $(FC) $(FFLAGS) $(LDFLAGS) -o $@ $<\n"
+
+#define RULES_2024 \
+ ".SUFFIXES:.o .c .y .l .a .sh\n"
+
+#define MACROS \
+ "CFLAGS=-O1\n" \
+ "YACC=yacc\n" \
+ "YFLAGS=\n" \
+ "LEX=lex\n" \
+ "LFLAGS=\n" \
+ "AR=ar\n" \
+ "ARFLAGS=-rv\n" \
+ "LDFLAGS=\n"
+
+#define MACROS_2017 \
+ "CC=c99\n" \
+ "FC=fort77\n" \
+ "FFLAGS=-O1\n" \
+
+#define MACROS_2024 \
+ "CC=c17\n"
+
+#define MACROS_EXT \
+ "CC=cc\n"
+
+/*
+ * Read the built-in rules using a fake fgets-like interface.
+ */
+static char *
+mk_getrules(MakeCtx* mc, char *s, int size)
+{
+ char *r = s;
+
+ if (mc->rulepos == NULL || *mc->rulepos == '\0') {
+ if (mc->rule_idx == 0) {
+ mc->rulepos = MACROS;
+ mc->rule_idx++;
+ } else if (mc->rule_idx == 1) {
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ if (POSIX_2017)
+ mc->rulepos = MACROS_2017;
+ else if (mc->posix)
+ mc->rulepos = MACROS_2024;
+ else
+ mc->rulepos = MACROS_EXT;
+#elif ENABLE_FEATURE_MAKE_POSIX_2024
+ mc->rulepos = MACROS_2024;
+#else
+ mc->rulepos = MACROS_2017;
+#endif
+ mc->rule_idx++;
+ } else if (!norules) {
+ if (mc->rule_idx == 2) {
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ mc->rulepos = POSIX_2017 ? RULES_2017 : RULES_2024;
+#elif ENABLE_FEATURE_MAKE_POSIX_2024
+ mc->rulepos = RULES_2024;
+#else
+ mc->rulepos = RULES_2017;
+#endif
+ mc->rule_idx++;
+ } else if (mc->rule_idx == 3) {
+ mc->rulepos = RULES;
+ mc->rule_idx++;
+ }
+ }
+ }
+
+ if (*mc->rulepos == '\0')
+ return NULL;
+
+ while (--size) {
+ if ((*r++ = *mc->rulepos++) == '\n')
+ break;
+ }
+ *r = '\0';
+ return s;
+}
diff --git a/src/make/run.c b/src/make/run.c
@@ -0,0 +1,308 @@
+/*
+ * Top-level orchestration: install default macros/rules, import command-line,
+ * MAKEFLAGS, and environment macros, read the makefiles, build the child
+ * environment (with the outgoing MAKEFLAGS so recursive make keeps its flags),
+ * then bring the requested targets up to date. This is pdpmake main() minus the
+ * CLI/getopt machinery (now in driver/cmd/make.c). Part of the src/api/make.c
+ * amalgamation.
+ */
+#include "make.h"
+
+static char* mk_slice_dup(MakeCtx* mc, KitSlice s) {
+ char* t = mk_alloc(mc, s.len + 1);
+ if (s.len) memcpy(t, s.s, s.len);
+ t[s.len] = '\0';
+ return t;
+}
+
+/* Look up NAME in the ambient environment; return its value or NULL. */
+static const char* mk_getenv(MakeCtx* mc, const char* name) {
+ size_t nlen = strlen(name);
+ const char* const* e;
+ if (!mc->ambient) return NULL;
+ for (e = mc->ambient; *e; ++e)
+ if (strncmp(*e, name, nlen) == 0 && (*e)[nlen] == '=') return *e + nlen + 1;
+ return NULL;
+}
+
+/*
+ * If the option flag for a special target isn't set, mark its prerequisites;
+ * if the target had no prerequisites, set the global option flag.
+ */
+static void mk_mark_special(MakeCtx* mc, const char* special, uint32_t oflag,
+ uint16_t nflag) {
+ struct name* np;
+ struct rule* rp;
+ struct depend* dp;
+ int marked = FALSE;
+
+ if (!(mc->opts & oflag) && (np = mk_findname(mc, special))) {
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ for (dp = rp->r_dep; dp; dp = dp->d_next) {
+ dp->d_name->n_flag |= nflag;
+ marked = TRUE;
+ }
+ }
+ if (!marked) mc->opts |= oflag;
+ }
+}
+
+/* Import NAME=VALUE entries from the environment as level-3 macros. */
+static void mk_import_env(MakeCtx* mc) {
+ const char* const* e;
+
+ if (!mc->ambient) return;
+ for (e = mc->ambient; *e; ++e) {
+ const char* eq = strchr(*e, '=');
+ char* name;
+ if (!eq || eq == *e) continue;
+ name = mk_strndup(mc, *e, (size_t)(eq - *e));
+ /* Exceptions for particular environment values (see pdpmake). */
+ if (strcmp(name, "MAKEFLAGS") == 0 || strcmp(name, "SHELL") == 0) continue;
+ if (strcmp(name, "CURDIR") == 0 && !useenv && !POSIX_2017) continue;
+ mk_setmacro(mc, name, eq + 1, 3 | M_ENVIRON);
+ }
+}
+
+/*
+ * Parse the incoming MAKEFLAGS environment variable: whitespace-separated
+ * tokens are either option letters (merged into mc->opts) or NAME=VALUE macro
+ * definitions (level 2). This is how a recursive `make` inherits the flags and
+ * macros of its parent. (Escaped blanks in values are not handled in v1.)
+ */
+static void mk_makeflags_in(MakeCtx* mc) {
+ const char* mf = mk_getenv(mc, "MAKEFLAGS");
+ char* dup;
+ char* q;
+ char* tok;
+
+ if (!mf || !*mf) return;
+ dup = mk_strdup(mc, mf);
+ q = dup;
+ while ((tok = mk_gettok(&q)) != NULL) {
+ char* eq = strchr(tok, '=');
+ if (eq && eq != tok) {
+ *eq = '\0';
+ mk_setmacro(mc, tok, eq + 1, 2);
+ } else {
+ const char* p = tok;
+ if (*p == '-') ++p;
+ for (; *p; ++p) {
+ switch (*p) {
+ case 'e': mc->opts |= OPT_e; break;
+ case 'i': mc->opts |= OPT_i; break;
+ case 'k': mc->opts |= OPT_k; break;
+ case 'n': mc->opts |= OPT_n; break;
+ case 'q': mc->opts |= OPT_q; break;
+ case 'r': mc->opts |= OPT_r; break;
+ case 's': mc->opts |= OPT_s; break;
+ case 't': mc->opts |= OPT_t; break;
+ default: break; /* ignore unknown (e.g. -j and its digits) */
+ }
+ }
+ }
+ }
+}
+
+/* Backslash-escape blanks and backslashes for embedding in MAKEFLAGS. */
+static char* mk_escape_value(MakeCtx* mc, const char* val) {
+ size_t n = 0;
+ size_t i;
+ size_t j = 0;
+ char* out;
+ for (i = 0; val[i]; ++i)
+ n += (val[i] == '\\' || val[i] == ' ' || val[i] == '\t') ? 2 : 1;
+ out = mk_alloc(mc, n + 1);
+ for (i = 0; val[i]; ++i) {
+ if (val[i] == '\\' || val[i] == ' ' || val[i] == '\t') out[j++] = '\\';
+ out[j++] = val[i];
+ }
+ out[j] = '\0';
+ return out;
+}
+
+/*
+ * Build the child environment vector used to launch recipes: the ambient
+ * environment (minus MAKEFLAGS), the outgoing MAKEFLAGS (option flags plus
+ * level-1/2 macros), and the command-line (level-1) macros exported as
+ * individual variables. Stored in mc->env.
+ */
+static void mk_build_child_env(MakeCtx* mc) {
+ static const struct {
+ uint32_t bit;
+ const char* flag;
+ } fl[] = {{OPT_e, "-e"}, {OPT_i, "-i"}, {OPT_k, "-k"}, {OPT_n, "-n"},
+ {OPT_q, "-q"}, {OPT_r, "-r"}, {OPT_s, "-s"}, {OPT_t, "-t"}};
+ char* mf = NULL;
+ int i;
+ size_t k;
+ struct macro* mp;
+ size_t n_ambient = 0;
+ size_t n_export = 0;
+ size_t count = 0;
+ const char* const* e;
+ KitExecKV* env;
+
+ for (k = 0; k < sizeof fl / sizeof fl[0]; ++k)
+ if (mc->opts & fl[k].bit) mf = mk_appendword(mc, mf, fl[k].flag);
+ for (i = 0; i < HTABSIZE; ++i)
+ for (mp = mc->macrohead[i]; mp; mp = mp->m_next)
+ if ((mp->m_level == 1 || mp->m_level == 2) &&
+ strcmp(mp->m_name, "MAKEFLAGS") != 0)
+ mf = mk_appendword(
+ mc, mf,
+ mk_concat3(mc, mp->m_name, "=", mk_escape_value(mc, mp->m_val)));
+ if (!mf) mf = mk_strdup(mc, "");
+ /* Also expose it as the MAKEFLAGS macro so $(MAKEFLAGS) expands. */
+ mk_setmacro(mc, "MAKEFLAGS", mf, 0);
+
+ if (mc->ambient)
+ for (e = mc->ambient; *e; ++e)
+ if (strncmp(*e, "MAKEFLAGS=", 10) != 0) n_ambient++;
+ for (i = 0; i < HTABSIZE; ++i)
+ for (mp = mc->macrohead[i]; mp; mp = mp->m_next)
+ if (mp->m_level == 1 && strcmp(mp->m_name, "SHELL") != 0 &&
+ strcmp(mp->m_name, "MAKEFLAGS") != 0)
+ n_export++;
+
+ env = mk_alloc(mc, (n_ambient + n_export + 1) * sizeof *env);
+ if (mc->ambient)
+ for (e = mc->ambient; *e; ++e) {
+ const char* eq;
+ if (strncmp(*e, "MAKEFLAGS=", 10) == 0) continue;
+ eq = strchr(*e, '=');
+ if (!eq) continue;
+ env[count].key.s = *e;
+ env[count].key.len = (size_t)(eq - *e);
+ env[count].value = kit_slice_cstr(eq + 1);
+ count++;
+ }
+ env[count].key = kit_slice_cstr("MAKEFLAGS");
+ env[count].value = kit_slice_cstr(mf);
+ count++;
+ for (i = 0; i < HTABSIZE; ++i)
+ for (mp = mc->macrohead[i]; mp; mp = mp->m_next)
+ if (mp->m_level == 1 && strcmp(mp->m_name, "SHELL") != 0 &&
+ strcmp(mp->m_name, "MAKEFLAGS") != 0) {
+ env[count].key = kit_slice_cstr(mp->m_name);
+ env[count].value = kit_slice_cstr(mp->m_val);
+ count++;
+ }
+ mc->env = env;
+ mc->nenv = count;
+}
+
+/* Read one makefile source into the graph (name is for diagnostics only). */
+static void mk_read_source(MakeCtx* mc, const char* name, MakeSource* src) {
+ mc->makefile = name;
+ mk_input(mc, src, 0);
+ mc->makefile = NULL;
+}
+
+/* Read a makefile by path (resolved against root) through file_io. */
+static int mk_read_path(MakeCtx* mc, const char* path, int required) {
+ const KitFileIO* io = mc->ctx->file_io;
+ KitFileData fd;
+ MakeSource src;
+
+ fd.data = NULL;
+ fd.size = 0;
+ fd.token = NULL;
+ if (!io || io->read_all(io->user, mk_path(mc, path), &fd) != KIT_OK) {
+ if (required) mk_error(mc, "can't open %s", path);
+ return 0;
+ }
+ src.data = (const char*)fd.data;
+ src.len = fd.size;
+ src.pos = 0;
+ mk_read_source(mc, path, &src);
+ if (io->release) io->release(io->user, &fd);
+ return 1;
+}
+
+static int mk_run(MakeCtx* mc) {
+ const KitMakeOptions* o = mc->mkopts;
+ MakeSource builtin = {NULL, 0, 0};
+ struct macro* shell_mp;
+ size_t i;
+ int estat;
+ int found_target;
+
+ mc->myname = "make";
+ mk_setmacro(mc, "$", "$", 0 | M_VALID);
+
+ /* Macro definitions from the command line (highest precedence, level 1). */
+ for (i = 0; i < o->nmacros; ++i) {
+ char* name = mk_slice_dup(mc, o->macros[i].name);
+ char* val = mk_slice_dup(mc, o->macros[i].value);
+ mk_setmacro(mc, name, val, 1);
+ }
+
+ /* MAKEFLAGS from the environment: option flags + macros (level 2). */
+ mk_makeflags_in(mc);
+
+ /* Macro definitions from the environment (level 3). */
+ mk_import_env(mc);
+
+ /* Built-in rules. */
+ mk_input(mc, &builtin, 0);
+
+ /* SHELL / MAKE / CURDIR (level 4; a makefile can still override SHELL). */
+ mk_setmacro(mc, "SHELL", mc->shell ? mc->shell : "/bin/sh", 4);
+ mk_setmacro(mc, "MAKE", "make", 4);
+ if (mc->root && !POSIX_2017) mk_setmacro(mc, "CURDIR", mc->root, 4);
+
+ /* User makefiles (paths resolved against root; no chdir). */
+ if (o->nmakefiles == 0) {
+ const char* names[3];
+ int nn = 0;
+ int opened = 0;
+ int k;
+ if (!mc->posix) names[nn++] = "PDPmakefile";
+ names[nn++] = "makefile";
+ names[nn++] = "Makefile";
+ for (k = 0; k < nn && !opened; ++k) opened = mk_read_path(mc, names[k], 0);
+ if (!opened) mk_error(mc, "no makefile found");
+ } else {
+ for (i = 0; i < o->nmakefiles; ++i) {
+ const char* fn = o->makefiles[i];
+ if (strcmp(fn, "-") == 0) {
+ MakeSource src;
+ src.data = (const char*)o->stdin_makefile;
+ src.len = o->stdin_makefile_len;
+ src.pos = 0;
+ mk_read_source(mc, "stdin", &src);
+ } else {
+ mk_read_path(mc, fn, TRUE);
+ }
+ }
+ }
+
+ /* Honor a makefile-provided SHELL for recipe execution. */
+ shell_mp = mk_getmp(mc, "SHELL");
+ if (shell_mp && shell_mp->m_val[0]) mc->shell = shell_mp->m_val;
+
+ if (print) mk_print_details(mc);
+
+ mk_mark_special(mc, ".SILENT", OPT_s, N_SILENT);
+ mk_mark_special(mc, ".IGNORE", OPT_i, N_IGNORE);
+ mk_mark_special(mc, ".PRECIOUS", OPT_precious, N_PRECIOUS);
+ if (!POSIX_2017) mk_mark_special(mc, ".PHONY", OPT_phony, N_PHONY);
+
+ /* Assemble the recipe environment now that all macros/flags are known. */
+ mk_build_child_env(mc);
+
+ /* Build the requested targets (or the default goal). */
+ estat = 0;
+ found_target = FALSE;
+ for (i = 0; i < o->ntargets; ++i) {
+ found_target = TRUE;
+ estat |= mk_make(mc, mk_newname(mc, o->targets[i]), 0);
+ }
+ if (!found_target) {
+ if (!mc->firstname) mk_error(mc, "no targets defined");
+ estat = mk_make(mc, mc->firstname, 0);
+ }
+
+ return estat & MAKE_FAILURE;
+}
diff --git a/src/make/target.c b/src/make/target.c
@@ -0,0 +1,310 @@
+/*
+ * Process name, rule, command and prerequisite structures. Ported from pdpmake
+ * target.c. Part of the src/api/make.c amalgamation.
+ */
+#include "make.h"
+
+/*
+ * Add a prerequisite to the end of the supplied list.
+ * Return the new head pointer for that list.
+ */
+static struct depend* mk_newdep(MakeCtx* mc, struct name* np,
+ struct depend* dphead) {
+ struct depend* dpnew;
+ struct depend* dp;
+
+ dpnew = mk_alloc(mc, sizeof(struct depend));
+ dpnew->d_next = NULL;
+ dpnew->d_name = np;
+ dpnew->d_refcnt = 0;
+
+ if (dphead == NULL)
+ return dpnew;
+
+ for (dp = dphead; dp->d_next; dp = dp->d_next)
+ ;
+
+ dp->d_next = dpnew;
+
+ return dphead;
+}
+
+/*
+ * Add a command to the end of the supplied list of commands.
+ * Return the new head pointer for that list.
+ */
+static struct cmd* mk_newcmd(MakeCtx* mc, char* str, struct cmd* cphead) {
+ struct cmd* cpnew;
+ struct cmd* cp;
+
+ while (isspace(*str))
+ str++;
+
+ cpnew = mk_alloc(mc, sizeof(struct cmd));
+ cpnew->c_next = NULL;
+ cpnew->c_cmd = mk_strdup(mc, str);
+ cpnew->c_refcnt = 0;
+ cpnew->c_makefile = mk_strdup(mc, mc->makefile);
+ cpnew->c_dispno = mc->dispno;
+
+ if (cphead == NULL)
+ return cpnew;
+
+ for (cp = cphead; cp->c_next; cp = cp->c_next)
+ ;
+
+ cp->c_next = cpnew;
+
+ return cphead;
+}
+
+static struct name* mk_findname(MakeCtx* mc, const char* name) {
+ struct name* np;
+
+ for (np = mc->namehead[mk_getbucket(name)]; np; np = np->n_next) {
+ if (strcmp(name, np->n_name) == 0)
+ return np;
+ }
+ return NULL;
+}
+
+static int mk_check_name(MakeCtx* mc, const char* name) {
+ const char* s;
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ if (!mc->posix) {
+ for (s = name; *s; ++s) {
+ if (*s == '=')
+ return FALSE;
+ }
+ return TRUE;
+ }
+#endif
+
+ for (s = name; *s; ++s) {
+ if ((
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ (mc->pragma & P_TARGET_NAME) ||
+#endif
+#if ENABLE_FEATURE_MAKE_POSIX_2024
+ !POSIX_2017
+#else
+ FALSE
+#endif
+ )
+ ? !(isfname(*s) || *s == '/')
+ : !ispname(*s))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static int mk_is_valid_target(MakeCtx* mc, const char* name) {
+ char* archive;
+ char* member = NULL;
+ int ret;
+
+ /* Names of the form 'lib(member)' are referred to as 'expressions'
+ * in POSIX and are subjected to special treatment. The 'lib'
+ * and 'member' elements must each be a valid target name. */
+ archive = mk_splitlib(mc, name, &member);
+ ret = mk_check_name(mc, archive) &&
+ (member == NULL || mk_check_name(mc, member));
+
+ return ret;
+}
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+static int mk_potentially_valid_target(MakeCtx* mc, const char* name) {
+ int ret = FALSE;
+
+ if (!(mc->pragma & P_TARGET_NAME)) {
+ mc->pragma |= P_TARGET_NAME;
+ ret = mk_is_valid_target(mc, name);
+ mc->pragma &= ~P_TARGET_NAME;
+ }
+ return ret;
+}
+#endif
+
+/*
+ * Intern a name. Return a pointer to the name struct
+ */
+static struct name* mk_newname(MakeCtx* mc, const char* name) {
+ struct name* np = mk_findname(mc, name);
+
+ if (np == NULL) {
+ unsigned int bucket;
+
+ if (!mk_is_valid_target(mc, name))
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ mk_error(mc, "invalid target name '%s'%s", name,
+ mk_potentially_valid_target(mc, name)
+ ? ": allow with pragma target_name"
+ : "");
+#else
+ mk_error(mc, "invalid target name '%s'", name);
+#endif
+
+ bucket = mk_getbucket(name);
+ np = mk_alloc(mc, sizeof(struct name));
+ np->n_next = mc->namehead[bucket];
+ mc->namehead[bucket] = np;
+ np->n_name = mk_strdup(mc, name);
+ np->n_rule = NULL;
+ np->n_tim = (struct mk_timespec){0, 0};
+ np->n_flag = 0;
+ }
+ return np;
+}
+
+/*
+ * Return the commands on the first rule that has them or NULL.
+ */
+static struct cmd* mk_getcmd(struct name* np) {
+ struct rule* rp;
+
+ if (np == NULL)
+ return NULL;
+
+ for (rp = np->n_rule; rp; rp = rp->r_next)
+ if (rp->r_cmd)
+ return rp->r_cmd;
+ return NULL;
+}
+
+static void mk_freerules(struct rule* rp) { (void)rp; }
+
+static void* mk_inc_ref(MakeCtx* mc, void* vp) {
+ if (vp) {
+ struct depend* dp = vp;
+ if (dp->d_refcnt == INT_MAX)
+ mk_error(mc, "out of memory");
+ dp->d_refcnt++;
+ }
+ return vp;
+}
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+// Order must match constants in make.h
+// POSIX levels must be last and in increasing order
+static const char* p_name[] = {
+ "macro_name",
+ "target_name",
+ "command_comment",
+ "empty_suffix",
+ "posix_2017",
+ "posix_2024",
+ "posix_202x"
+};
+
+static void mk_set_pragma(MakeCtx* mc, const char* name) {
+ int i;
+
+ // posix_202x is an alias for posix_2024
+ for (i = 0; i < (int)(sizeof(p_name) / sizeof(p_name[0])); ++i) {
+ if (strcmp(name, p_name[i]) == 0) {
+#if !ENABLE_FEATURE_MAKE_POSIX_2024
+ if (i == BIT_POSIX_2024 || i == BIT_POSIX_202X) {
+ break;
+ }
+#endif
+ if (i >= BIT_POSIX_2017) {
+ // POSIX level is stored in a separate variable.
+ // No bits in 'pragma' are used.
+ if (mc->posix_level == DEFAULT_POSIX_LEVEL) {
+ mc->posix_level = i - BIT_POSIX_2017;
+ if (mc->posix_level > STD_POSIX_2024)
+ mc->posix_level = STD_POSIX_2024;
+ } else if (mc->posix_level != i - BIT_POSIX_2017)
+ mk_warning(mc, "unable to change POSIX level");
+ } else {
+ mc->pragma |= 1 << i;
+ }
+ return;
+ }
+ }
+ mk_warning(mc, "invalid pragma '%s'", name);
+}
+#endif
+
+/*
+ * Add a new rule to a target. This checks to see if commands already
+ * exist for the target. If flag is TRUE the target can have multiple
+ * rules with commands (double-colon rules).
+ *
+ * i) If the name is a special target and there are no prerequisites
+ * or commands to be added remove all prerequisites and commands.
+ * This is necessary when clearing a built-in inference rule.
+ * ii) If name is a special target and has commands, replace them.
+ * This is for redefining commands for an inference rule.
+ */
+static void mk_addrule(MakeCtx* mc, struct name* np, struct depend* dp,
+ struct cmd* cp, int flag) {
+ struct rule* rp;
+ struct rule** rpp;
+ struct cmd* old_cp;
+
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ // Can't mix single-colon and double-colon rules
+ if (!mc->posix && (np->n_flag & N_TARGET)) {
+ if (!(np->n_flag & N_DOUBLE) != !flag) // like xor
+ mk_error(mc, "inconsistent rules for target %s", np->n_name);
+ }
+#endif
+
+ // Clear out prerequisites and commands
+ if ((np->n_flag & N_SPECIAL) && !dp && !cp) {
+#if ENABLE_FEATURE_MAKE_POSIX_2024
+ if (strcmp(np->n_name, ".PHONY") == 0)
+ return;
+#endif
+ mk_freerules(np->n_rule);
+ np->n_rule = NULL;
+ return;
+ }
+
+ if (cp && !(np->n_flag & N_DOUBLE) && (old_cp = mk_getcmd(np))) {
+ // Handle the inference rule redefinition case
+ // .DEFAULT rule can also be redefined (as an extension).
+ if ((np->n_flag & N_INFERENCE)
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ && !(mc->posix && (np->n_flag & N_SPECIAL))
+#endif
+ ) {
+ mk_freerules(np->n_rule);
+ np->n_rule = NULL;
+ } else {
+ // We're adding commands to a single colon rule which
+ // already has some. Clear the old ones first.
+ mk_warning(mc, "overriding rule for target %s", np->n_name);
+ mc->curr_cmd = old_cp;
+ mk_warning(mc, "previous rule for target %s", np->n_name);
+ mc->curr_cmd = NULL;
+
+ for (rp = np->n_rule; rp; rp = rp->r_next) {
+ rp->r_cmd = NULL;
+ }
+ }
+ }
+
+ rpp = &np->n_rule;
+ while (*rpp)
+ rpp = &(*rpp)->r_next;
+
+ *rpp = rp = mk_alloc(mc, sizeof(struct rule));
+ rp->r_next = NULL;
+ rp->r_dep = mk_inc_ref(mc, dp);
+ rp->r_cmd = mk_inc_ref(mc, cp);
+
+ np->n_flag |= N_TARGET;
+ if (flag)
+ np->n_flag |= N_DOUBLE;
+#if ENABLE_FEATURE_MAKE_EXTENSIONS
+ if (strcmp(np->n_name, ".PRAGMA") == 0) {
+ for (; dp; dp = dp->d_next) {
+ mk_set_pragma(mc, dp->d_name->n_name);
+ }
+ }
+#endif
+}
diff --git a/src/make/utils.c b/src/make/utils.c
@@ -0,0 +1,175 @@
+/*
+ * Utility functions: arena-backed allocation, diagnostics that longjmp instead
+ * of exit(), and output routed through the KitWriter sinks. Ported from
+ * pdpmake utils.c. Part of the src/api/make.c amalgamation.
+ */
+#include "make.h"
+
+/* ---- allocation (arena; OOM longjmps via mk_error) ----------------------- */
+
+static void* mk_alloc(MakeCtx* mc, size_t n) {
+ void* p = arena_alloc(&mc->arena, n ? n : 1, 16);
+ if (!p) mk_error(mc, "out of memory");
+ return p;
+}
+
+static void* mk_realloc(MakeCtx* mc, void* p, size_t oldn, size_t newn) {
+ void* q = mk_alloc(mc, newn);
+ if (p && oldn) memcpy(q, p, oldn < newn ? oldn : newn);
+ return q;
+}
+
+static char* mk_strdup(MakeCtx* mc, const char* s) {
+ size_t len;
+ char* t;
+ if (s == NULL) return NULL;
+ len = strlen(s);
+ t = mk_alloc(mc, len + 1);
+ memcpy(t, s, len + 1);
+ return t;
+}
+
+static char* mk_strndup(MakeCtx* mc, const char* s, size_t n) {
+ size_t len = 0;
+ char* t;
+ while (len < n && s[len] != '\0') ++len;
+ t = mk_alloc(mc, len + 1);
+ memcpy(t, s, len);
+ t[len] = '\0';
+ return t;
+}
+
+static char* mk_concat3(MakeCtx* mc, const char* s1, const char* s2,
+ const char* s3) {
+ const size_t len1 = strlen(s1);
+ const size_t len2 = strlen(s2);
+ const size_t len3 = strlen(s3);
+ char* t = mk_alloc(mc, len1 + len2 + len3 + 1);
+ char* s = t;
+ s = (char*)memcpy(s, s1, len1) + len1;
+ s = (char*)memcpy(s, s2, len2) + len2;
+ s = (char*)memcpy(s, s3, len3) + len3;
+ *s = '\0';
+ return t;
+}
+
+/*
+ * Append a word to a space-separated string of words. The first call should
+ * pass NULL for str. Unlike pdpmake there is no free() -- the arena owns the
+ * old string, which is left to be reclaimed wholesale.
+ */
+static char* mk_appendword(MakeCtx* mc, const char* str, const char* word) {
+ return str ? mk_concat3(mc, str, " ", word) : mk_strdup(mc, word);
+}
+
+/* ---- error handling: longjmp back to kit_make_run ------------------------ */
+
+static void mk_exit(MakeCtx* mc, int code) {
+ mc->exit_code = code;
+ longjmp(mc->jmpbuf, 1);
+}
+
+/*
+ * Format "myname: (makefile:line): message\n" to a writer. Mirrors pdpmake's
+ * vwarning but targets a KitWriter instead of a stdio stream.
+ */
+static void mk_vwarn(MakeCtx* mc, KitWriter* w, const char* fmt, va_list ap) {
+ char buf[2048];
+ size_t cap = sizeof buf;
+ size_t n = 0;
+ int r;
+ const char* m = NULL;
+ int d = 0;
+
+ if (mc->curr_cmd) {
+ m = mc->curr_cmd->c_makefile;
+ d = mc->curr_cmd->c_dispno;
+ } else if (mc->makefile) {
+ m = mc->makefile;
+ d = mc->dispno;
+ }
+
+ r = snprintf(buf + n, cap - n, "%s: ", mc->myname ? mc->myname : "make");
+ if (r > 0) n += (size_t)r < cap - n ? (size_t)r : cap - n - 1;
+ if (m) {
+ r = snprintf(buf + n, cap - n, "(%s:%d): ", m, d);
+ if (r > 0) n += (size_t)r < cap - n ? (size_t)r : cap - n - 1;
+ }
+ r = vsnprintf(buf + n, cap - n, fmt, ap);
+ if (r > 0) n += (size_t)r < cap - n ? (size_t)r : cap - n - 1;
+ if (n >= cap) n = cap - 1;
+ buf[n] = '\n';
+ n++;
+ if (w) kit_writer_write(w, buf, n);
+}
+
+static void mk_error(MakeCtx* mc, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ mk_vwarn(mc, mc->err, fmt, ap);
+ va_end(ap);
+ mk_exit(mc, 2);
+}
+
+static void mk_warning(MakeCtx* mc, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ mk_vwarn(mc, mc->out, fmt, ap); /* pdpmake sends warnings to stdout */
+ va_end(ap);
+}
+
+static void mk_diagnostic(MakeCtx* mc, const char* fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ mk_vwarn(mc, mc->err, fmt, ap);
+ va_end(ap);
+}
+
+static void mk_error_unexpected(MakeCtx* mc, const char* s) {
+ mk_error(mc, "unexpected %s", s);
+}
+
+static void mk_error_in_inference_rule(MakeCtx* mc, const char* s) {
+ mk_error(mc, "%s in inference rule", s);
+}
+
+static void mk_error_not_allowed(MakeCtx* mc, const char* s, const char* t) {
+ mk_error(mc, "%s not allowed for %s", s, t);
+}
+
+/* ---- output through the KitWriter sinks ---------------------------------- */
+
+static void mk_out_bytes(MakeCtx* mc, const char* p, size_t n) {
+ if (mc->out && n) kit_writer_write(mc->out, p, n);
+}
+
+static void mk_out(MakeCtx* mc, const char* s) {
+ if (s) mk_out_bytes(mc, s, strlen(s));
+}
+
+static void mk_outc(MakeCtx* mc, char c) { mk_out_bytes(mc, &c, 1); }
+
+static void mk_outf(MakeCtx* mc, const char* fmt, ...) {
+ char buf[2048];
+ va_list ap;
+ int r;
+ va_start(ap, fmt);
+ r = vsnprintf(buf, sizeof buf, fmt, ap);
+ va_end(ap);
+ if (r < 0) return;
+ mk_out_bytes(mc, buf, (size_t)r < sizeof buf ? (size_t)r : sizeof buf - 1);
+}
+
+/* ---- misc ---------------------------------------------------------------- */
+
+static unsigned mk_getbucket(const char* name) {
+ unsigned hashval = 0;
+ const unsigned char* p = (const unsigned char*)name;
+ while (*p) hashval ^= (unsigned)(hashval << 5) + (hashval >> 2) + *p++;
+ return hashval % HTABSIZE;
+}
+
+static const char* mk_path(MakeCtx* mc, const char* name) {
+ if (!mc->root || !name || name[0] == '/') return name;
+ return mk_concat3(mc, mc->root, "/", name);
+}
diff --git a/test/make/run.sh b/test/make/run.sh
@@ -0,0 +1,195 @@
+#!/bin/sh
+# Driver-level checks for `kit make`.
+# Self-checking (no golden files): each case builds a fixture makefile in a
+# fresh work dir, runs `kit make`, and asserts exit status and output/artifacts
+# via the shared kit_* verbs (ok/not_ok) recorded over $work.
+
+set -u
+
+script_dir=$(cd "$(dirname "$0")" && pwd)
+repo_root=$(cd "$script_dir/../.." && pwd)
+
+KIT="${KIT:-$repo_root/build/kit}"
+
+if [ ! -x "$KIT" ]; then
+ echo "make: kit binary not found at $KIT" >&2
+ exit 2
+fi
+
+work=$(mktemp -d "${TMPDIR:-/tmp}/kit-make-test.XXXXXX")
+trap 'rm -rf "$work"' EXIT
+
+KIT_KIT_DIR="$repo_root/test/lib"
+. "$repo_root/test/lib/kit_sh_kit.sh"
+kit_report_init
+
+n=0
+# fresh_dir: make a new numbered case directory and echo its path.
+fresh_dir() {
+ n=$((n + 1))
+ d="$work/case$n"
+ mkdir -p "$d"
+ echo "$d"
+}
+
+# expect_status WANT NAME DIR ARGS...: run `kit make ARGS` in DIR, assert exit.
+expect_status() {
+ es_want=$1; es_name=$2; es_dir=$3; shift 3
+ ( cd "$es_dir" && "$KIT" make "$@" ) > "$es_dir/out" 2> "$es_dir/err"
+ es_got=$?
+ if [ "$es_got" -eq "$es_want" ]; then
+ ok "$es_name"
+ else
+ { printf 'wanted exit %s, got %s\n' "$es_want" "$es_got"; cat "$es_dir/err"; } \
+ > "$es_dir/diag"
+ not_ok "$es_name" "$es_dir/diag"
+ fi
+}
+
+# assert_contains NAME FILE NEEDLE
+assert_contains() {
+ if grep -q -- "$3" "$2" 2>/dev/null; then
+ ok "$1"
+ else
+ { printf 'expected to find: %s\nin:\n' "$3"; cat "$2"; } > "$work/c.diag"
+ not_ok "$1" "$work/c.diag"
+ fi
+}
+
+# ---- 1. basic build: prerequisite ordering + recipe execution -------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+all: out.txt
+out.txt: in.txt
+ printf 'built\n' > out.txt
+ cat in.txt >> out.txt
+in.txt:
+ printf 'src\n' > in.txt
+EOF
+expect_status 0 build_ok "$d"
+assert_contains build_made_out "$d/out.txt" built
+assert_contains build_made_dep "$d/out.txt" src
+
+# ---- 2. up-to-date short-circuit ------------------------------------------
+expect_status 0 uptodate "$d"
+assert_contains uptodate_msg "$d/out" "up to date\|nothing to be done"
+
+# ---- 3. dry run (-n): echoes recipes, runs nothing ------------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+t:
+ printf 'ran\n' > witness
+EOF
+expect_status 0 dryrun "$d" -n t
+if [ -e "$d/witness" ]; then not_ok dryrun_noop "$d/out"; else ok dryrun_noop; fi
+
+# ---- 4. print database (-p) -----------------------------------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+FOO = barval
+t: ; @true
+EOF
+expect_status 0 printdb "$d" -p
+assert_contains printdb_macro "$d/out" 'FOO = barval'
+
+# ---- 5. macros: =, :=, != and command-line override -----------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+NAME = default
+CAP != printf 'captured'
+show:
+ @printf 'name=%s cap=%s\n' "$(NAME)" "$(CAP)"
+EOF
+expect_status 0 macro_run "$d" show NAME=override
+assert_contains macro_override "$d/out" 'name=override'
+assert_contains macro_capture "$d/out" 'cap=captured'
+
+# ---- 6. include directive -------------------------------------------------
+d=$(fresh_dir)
+printf 'INCVAR = included\n' > "$d/conf.mk"
+cat > "$d/Makefile" <<'EOF'
+include conf.mk
+show:
+ @printf '[%s]\n' "$(INCVAR)"
+EOF
+expect_status 0 include_run "$d" show
+assert_contains include_val "$d/out" '\[included\]'
+
+# ---- 7. inference rule (.c.o) ---------------------------------------------
+d=$(fresh_dir)
+printf 'int f(void){return 0;}\n' > "$d/u.c"
+cat > "$d/Makefile" <<'EOF'
+CFLAGS = -O0
+u.o: u.c
+EOF
+expect_status 0 inference "$d" u.o
+if [ -e "$d/u.o" ]; then ok inference_artifact; else not_ok inference_artifact "$d/err"; fi
+
+# ---- 8. failing recipe -> exit 2 ------------------------------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+bad: ; false
+EOF
+expect_status 2 fail_exit "$d" bad
+
+# ---- 9. -k keeps going, still fails ---------------------------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+all: a b
+a: ; @false
+b: ; @printf 'b-ran\n' > witness
+EOF
+expect_status 1 keepgoing "$d" -k all
+if [ -e "$d/witness" ]; then ok keepgoing_ran_b; else not_ok keepgoing_ran_b "$d/err"; fi
+
+# ---- 10. --posix injects `set -e` -----------------------------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+t:
+ printf 'before\n'; false; printf 'after\n'
+EOF
+expect_status 2 posix_seterr "$d" --posix t
+# `after` also appears in the echoed recipe line, so match a whole output line:
+# with set -e the second printf must not run, so no line is exactly "after".
+if grep -qx after "$d/out"; then not_ok posix_stops "$d/out"; else ok posix_stops; fi
+
+# ---- 11. -q question mode: stale -> exit 1 --------------------------------
+d=$(fresh_dir)
+cat > "$d/Makefile" <<'EOF'
+out: src
+ cp src out
+src:
+ printf 'x\n' > src
+EOF
+expect_status 1 question_stale "$d" -q out
+
+# ---- 12. bad usage -> exit 2 ----------------------------------------------
+d=$(fresh_dir)
+: > "$d/Makefile"
+expect_status 2 unknown_opt "$d" -Z
+
+# ---- 13. $(shell ...) function (incl. nested macro) -----------------------
+d=$(fresh_dir)
+printf 'W = world\nOUT := $(shell echo hello-$(W))\nt:\n\t@echo "[$(OUT)]"\n' \
+ > "$d/Makefile"
+expect_status 0 shell_func "$d" t
+assert_contains shell_func_val "$d/out" '\[hello-world\]'
+
+# ---- 14. -C builds in the target dir without chdir'ing --------------------
+d=$(fresh_dir)
+mkdir -p "$d/sub"
+printf 'out.txt:\n\tprintf done > out.txt\n' > "$d/sub/Makefile"
+expect_status 0 dashC "$d" -C sub
+if [ -e "$d/sub/out.txt" ]; then ok dashC_artifact; else not_ok dashC_artifact "$d/err"; fi
+
+# ---- 15. MAKEFLAGS: recursive make inherits flags + macros ----------------
+d=$(fresh_dir)
+printf 'all:\n\t@$(MAKE) -f inner.mk child\n' > "$d/Makefile"
+printf 'child:\n\t@echo "mf=[$(MAKEFLAGS)] v=[$(V)]"\n' > "$d/inner.mk"
+( cd "$d" && MAKE="$KIT make" "$KIT" make -k V=xyz all ) > "$d/out" 2> "$d/err"
+if [ $? -eq 0 ]; then ok makeflags_run; else not_ok makeflags_run "$d/err"; fi
+assert_contains makeflags_flag "$d/out" 'mf=\[.*-k'
+assert_contains makeflags_macro "$d/out" 'v=\[xyz\]'
+
+kit_summary make-driver
+kit_exit