kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Adriver/cmd/make.c | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdriver/driver.h | 2++
Mdriver/env.h | 2++
Mdriver/env/posix.c | 14++++++++++++++
Mdriver/env/windows.c | 19+++++++++++++++++++
Mdriver/main.c | 5+++++
Minclude/kit/config.h | 7+++++++
Ainclude/kit/make.h | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmk/driver_srcs.mk | 1+
Mmk/lib_srcs.mk | 11++++++++++-
Mmk/test.mk | 6+++++-
Asrc/api/make.c | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/core/config_assert.c | 4++++
Asrc/make/build.c | 387+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/check.c | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/input.c | 1031+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/macro.c | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/make.h | 332+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/modtime.c | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/rules.c | 305++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/run.c | 308+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/target.c | 310+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/make/utils.c | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/make/run.sh | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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