ww: add -w persistent workdir builds with content-identity reuse

A -w DIR workdir replaces the fresh .sepwork scratch with a caller-owned
persistent package-artifact tree. A package is reused only when its
freshly composed unit byte-equals the committed unit and byte copies of
the compiler/assembler recorded in the dir equal the live tools — pure
content identity, no mtimes, no hashes, every decision reproducible
with cmp against plain files. Recompiles stage at .new names and commit
by rename, unit strictly last, so an interrupted build forces a
recompile and can never leave a committed unit vouching for uncommitted
artifacts; .o/.a additionally reject zero size (ELF/ar are never
empty), while .s/.wwi accept legitimate empties (FFI-only rt). A mode
stamp pins the -T/-S shape and the artifact protocol revision. Classic
scratch keeps its exact acquire/refuse/cleanup contract; run rejects
-w; dir-mode test rejects -w; both driver stages implement identical
behavior and wording.
This commit is contained in:
2026-08-08 03:51:45 +09:00
parent e1141330f2
commit e06b871ab9
3 changed files with 693 additions and 72 deletions

View File

@@ -22,9 +22,9 @@
static const char *usage = static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n" "usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n" " -V print version and exit\n"
" build [-S] [-o FILE] [path] compile module; -S stops after package asm\n" " build [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\n"
" run [path] ... build then exec, passing extra args to the program\n" " run [path] ... build then exec, passing extra args to the program\n"
" test [-S -o STEM] [options] [path] build/run tests; -S emits package asm\n" " test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n"
" fmt <path> reformat ww source\n" " fmt <path> reformat ww source\n"
" version print version and exit\n" " version print version and exit\n"
"\n" "\n"
@@ -868,16 +868,104 @@ archive_o(const char *objpath, const char *apath)
return 0; return 0;
} }
/* ---- -w workdir freshness ---------------------------------------------
* A `-w DIR` workdir is a caller-owned persistent package-artifact tree
* that replaces the fresh `.sepwork` scratch. Staleness is pure content
* identity, never mtime: a package is reused only when its freshly
* composed unit byte-equals the committed unit AND the tool copies
* recorded in the dir byte-equal the live tools — every decision is
* reproducible by hand with cmp(1) against plain files. Artifacts commit
* via temp + rename with the unit renamed last, so a killed build can
* never leave a committed unit vouching for uncommitted artifacts. The
* caller serializes invocations per workdir (Make target = one workdir)
* and `make clean` reclaims the state; the wwstage twin is the
* fileequal/copyfileatomic/workdirstamp group in selfhost/cmd/ww/main.ww. */
/* `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt
* emits no text), so committed presence is their freshness test; the
* rename-commit protocol owns integrity. `.o`/`.a` are never empty
* (ELF/ar headers), so a zero size there is always a torn write. */
static int
file_is_reg(const char *path)
{
struct stat st;
return stat(path, &st) == 0 && S_ISREG(st.st_mode);
}
static int
file_size_nonzero(const char *path)
{
struct stat st;
return stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
}
/* Byte equality of two files; absence or IO error is inequality. */
static int
file_equal(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb");
if (fa == NULL) return 0;
FILE *fb = fopen(b, "rb");
if (fb == NULL) { fclose(fa); return 0; }
static char ba[65536], bb[65536];
int eq = 1;
for (;;) {
size_t na = fread(ba, 1, sizeof ba, fa);
size_t nb = fread(bb, 1, sizeof bb, fb);
if (na != nb || memcmp(ba, bb, na) != 0) { eq = 0; break; }
if (na < sizeof ba) {
if (ferror(fa) || ferror(fb)) eq = 0;
break;
}
}
fclose(fa); fclose(fb);
return eq;
}
/* Replace dst with src's bytes via temp + rename, so a torn write can
* never masquerade as a committed tool copy. */
static int
copy_file_atomic(const char *src, const char *dst)
{
char tmp[1100];
snprintf(tmp, sizeof tmp, "%s.new", dst);
FILE *in = fopen(src, "rb");
if (in == NULL) return -1;
FILE *out = fopen(tmp, "wb");
if (out == NULL) { fclose(in); return -1; }
static char buf[65536];
size_t n;
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
if (fwrite(buf, 1, n, out) != n) {
fclose(in); fclose(out); return -1;
}
int bad = ferror(in);
fclose(in);
if (fclose(out) != 0 || bad) return -1;
return rename(tmp, dst);
}
/* The stamp pins the non-content build inputs a unit compare cannot see:
* the -T/-S shape of the producer pass and the artifact protocol
* revision (bump "fmt" when the unit/archive/commit format changes). */
static void
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
{
snprintf(buf, bufsz, "ww workdir fmt 1 mode %s asm %d\n",
is_test ? "test" : "build", emit_asm);
}
/* build_one_sep — discover_deps, reverse_topo, /* build_one_sep — discover_deps, reverse_topo,
* the transitive producer loop (one `w6c -c -I` per package, dep-first, * the transitive producer loop (one `w6c -c -I` per package, dep-first,
* each DEP `.o` wrapped in its own deterministic `.a`), then a * each DEP `.o` wrapped in its own deterministic `.a`), then a
* reverse-topo `w6l` of the root `.o` + dep `.a` set + libwwrt.a. Side * reverse-topo `w6l` of the root `.o` + dep `.a` set + libwwrt.a. Side
* files land in a cold `<stem>.sepwork` dir. */ * files land in a cold `<stem>.sepwork` dir, or under the persistent
* `-w` workdir with content-identity package reuse. */
static int static int
build_one_sep_impl(const char *src, int entry_is_dir, const char *out, build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs, const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, char *scratchout, const char *extra_libdirs, int is_test, int emit_asm, const char *workdir,
size_t scratchoutsz, struct sepgraph **graphout) char *scratchout, size_t scratchoutsz, struct sepgraph **graphout)
{ {
const char *c6 = toolpath("WW_W6C", "w6c"); const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a"); const char *a6 = toolpath("WW_W6A", "w6a");
@@ -930,16 +1018,50 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
} }
const char *ostem = (objstem && objstem[0]) ? objstem : stem; const char *ostem = (objstem && objstem[0]) ? objstem : stem;
int warm = workdir != NULL && workdir[0] != 0;
char scratch[1100]; char scratch[1100];
snprintf(scratch, sizeof scratch, "%s.sepwork", ostem); if (warm) {
if (mkdir(scratch, 0755) != 0) { struct stat wst;
fprintf(stderr, "ww: cannot create scratch %s\n", scratch); if (stat(workdir, &wst) != 0 || !S_ISDIR(wst.st_mode)) {
return 1; fprintf(stderr, "ww: workdir %s is not a directory\n",
workdir);
return 1;
}
/* The workdir is caller-owned and persistent: no acquisition,
* no refusal, and scratchout stays empty so the wrapper never
* cleans it. */
snprintf(scratch, sizeof scratch, "%s", workdir);
} else {
snprintf(scratch, sizeof scratch, "%s.sepwork", ostem);
if (mkdir(scratch, 0755) != 0) {
fprintf(stderr, "ww: cannot create scratch %s\n", scratch);
return 1;
}
/* Hand the scratch path back only after mkdir succeeds. The
* wrapper therefore never removes a pre-existing path that this
* build failed to acquire. */
if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch);
}
int stale_all = 0, stampok = 0;
char toolc[1200] = {0}, toola[1200] = {0}, stampf[1200] = {0};
char stampwant[128];
if (warm) {
snprintf(toolc, sizeof toolc, "%s/.wwtool.w6c", scratch);
snprintf(toola, sizeof toola, "%s/.wwtool.w6a", scratch);
snprintf(stampf, sizeof stampf, "%s/.wwtool.stamp", scratch);
workdir_stamp_text(stampwant, sizeof stampwant, is_test, emit_asm);
char got[128] = {0};
FILE *sf = fopen(stampf, "rb");
if (sf) {
size_t rn = fread(got, 1, sizeof got - 1, sf);
got[rn] = 0;
fclose(sf);
}
stampok = strcmp(stampwant, got) == 0;
if (!stampok || !file_equal(toolc, c6)
|| (!emit_asm && !file_equal(toola, a6)))
stale_all = 1;
} }
/* Hand the scratch path back only after mkdir succeeds. The wrapper
* therefore never removes a pre-existing path that this build failed
* to acquire. */
if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch);
struct sepgraph *g = calloc(1, sizeof *g); struct sepgraph *g = calloc(1, sizeof *g);
if (g == NULL) return 1; if (g == NULL) return 1;
@@ -978,13 +1100,40 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
/* producer loop — dep-first, one `w6c -c -I` pass per package. */ /* producer loop — dep-first, one `w6c -c -I` pass per package. */
for (int oi = 0; oi < norder; oi++) { for (int oi = 0; oi < norder; oi++) {
int pi = order[oi]; int pi = order[oi];
char unitf[1024], wwi[1024], asmf[1024], obj[1024], cmd[8192]; char unitf[1024], wwi[1024], asmf[1024], obj[1024], apath[1024];
char unitnew[1024], wwinew[1024], asmnew[1024], objnew[1024];
char anew[1024], cmd[8192];
sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf); sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf);
sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi); sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi);
sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf); sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf);
sep_fname(g, pi, scratch, ".o", obj, sizeof obj); sep_fname(g, pi, scratch, ".o", obj, sizeof obj);
sep_fname(g, pi, scratch, ".a", apath, sizeof apath);
sep_fname(g, pi, scratch, ".unit.new", unitnew, sizeof unitnew);
sep_fname(g, pi, scratch, ".wwi.new", wwinew, sizeof wwinew);
sep_fname(g, pi, scratch, ".s.new", asmnew, sizeof asmnew);
sep_fname(g, pi, scratch, ".o.new", objnew, sizeof objnew);
sep_fname(g, pi, scratch, ".a.new", anew, sizeof anew);
/* Warm mode compiles from staged `.new` paths and commits by
* rename; classic mode keeps its exact in-place paths. */
const char *cu = warm ? unitnew : unitf;
const char *cw = warm ? wwinew : wwi;
const char *cs = warm ? asmnew : asmf;
const char *co = warm ? objnew : obj;
const char *ca = warm ? anew : apath;
if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, if (sep_compose_unit(g, pi, scratch, order, norder, srcdir,
unitf) < 0) { free(order); return 1; } cu) < 0) { free(order); return 1; }
if (warm && !stale_all && file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& (pi == root || file_is_reg(wwi))
&& (emit_asm || (file_size_nonzero(obj)
&& (pi == root || file_size_nonzero(apath))))) {
if (unlink(unitnew) != 0) {
fprintf(stderr, "ww: cannot remove %s\n",
unitnew);
free(order); return 1;
}
continue;
}
/* BUG-1 (#69): -I <wwi> is purely the root's UNUSED /* BUG-1 (#69): -I <wwi> is purely the root's UNUSED
* `.wwi` output path, but it triggers wwi_emit → * `.wwi` output path, but it triggers wwi_emit →
* check_exported_type on the root. A terminal binary's * check_exported_type on the root. A terminal binary's
@@ -997,17 +1146,17 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* so w6c synthesizes the test main. Deps never * so w6c synthesizes the test main. Deps never
* get -T. */ * get -T. */
snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s",
c6, is_test ? "-T " : "", asmf, unitf); c6, is_test ? "-T " : "", cs, cu);
else else
snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s",
c6, wwi, asmf, unitf); c6, cw, cs, cu);
if (run(cmd) != 0) { if (run(cmd) != 0) {
fprintf(stderr, "ww: w6c failed for %s\n", fprintf(stderr, "ww: w6c failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1; free(order); return 1;
} }
if (!emit_asm) { if (!emit_asm) {
snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf); snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, co, cs);
if (run(cmd) != 0) { if (run(cmd) != 0) {
fprintf(stderr, "ww: w6a failed for %s\n", fprintf(stderr, "ww: w6a failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
@@ -1020,14 +1169,53 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* before any archive is processed. The link consumes `.o`/`.a`, * before any archive is processed. The link consumes `.o`/`.a`,
* never `.wwi`. */ * never `.wwi`. */
if (!emit_asm && pi != root) { if (!emit_asm && pi != root) {
char apath[1024]; if (archive_o(co, ca) != 0) {
sep_fname(g, pi, scratch, ".a", apath, sizeof apath);
if (archive_o(obj, apath) != 0) {
fprintf(stderr, "ww: archive failed for %s\n", fprintf(stderr, "ww: archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1; free(order); return 1;
} }
} }
/* Commit order: artifacts before the unit that vouches for
* them, unit strictly last. */
if (warm) {
if ((pi != root && rename(wwinew, wwi) != 0)
|| rename(asmnew, asmf) != 0
|| (!emit_asm && rename(objnew, obj) != 0)
|| (!emit_asm && pi != root
&& rename(anew, apath) != 0)
|| rename(unitnew, unitf) != 0) {
fprintf(stderr, "ww: cannot commit %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
free(order); return 1;
}
}
}
/* Tool identity commits only after every package artifact it vouches
* for is itself committed; a killed pass leaves the old identity and
* forces a full recompile, never a false reuse. */
if (warm) {
if (!file_equal(toolc, c6)
&& copy_file_atomic(c6, toolc) != 0) {
fprintf(stderr, "ww: cannot record %s\n", toolc);
free(order); return 1;
}
if (!emit_asm && !file_equal(toola, a6)
&& copy_file_atomic(a6, toola) != 0) {
fprintf(stderr, "ww: cannot record %s\n", toola);
free(order); return 1;
}
if (!stampok) {
char stampnew[1300];
snprintf(stampnew, sizeof stampnew, "%s.new", stampf);
FILE *sf = fopen(stampnew, "wb");
int bad = sf == NULL || fputs(stampwant, sf) == EOF;
if (sf != NULL && fclose(sf) != 0) bad = 1;
if (bad || rename(stampnew, stampf) != 0) {
fprintf(stderr, "ww: cannot record %s\n",
stampf);
free(order); return 1;
}
}
} }
if (emit_asm) { free(order); return 0; } if (emit_asm) { free(order); return 0; }
@@ -1077,13 +1265,14 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
static int static int
build_one_sep(const char *src, int entry_is_dir, const char *out, build_one_sep(const char *src, int entry_is_dir, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs, const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, int keepscratch) const char *extra_libdirs, int is_test, int emit_asm, int keepscratch,
const char *workdir)
{ {
char scratch[1100] = {0}; char scratch[1100] = {0};
struct sepgraph *g = NULL; struct sepgraph *g = NULL;
int r = build_one_sep_impl(src, entry_is_dir, out, objstem, int r = build_one_sep_impl(src, entry_is_dir, out, objstem,
extra_includes, extra_libs, extra_libdirs, is_test, emit_asm, extra_includes, extra_libs, extra_libdirs, is_test, emit_asm,
scratch, sizeof scratch, &g); workdir, scratch, sizeof scratch, &g);
sep_graph_free(g); sep_graph_free(g);
if (!keepscratch && scratch[0]) { if (!keepscratch && scratch[0]) {
size_t sl = strlen(scratch); size_t sl = strlen(scratch);
@@ -1197,6 +1386,7 @@ parse_build_flags(const char *cmd, int argc, char **argv,
char *libdirs, size_t libdirsz, char *libdirs, size_t libdirsz,
char *libs, size_t libsz, char *libs, size_t libsz,
char *outpath, size_t outsz, char *outpath, size_t outsz,
char *workdir, size_t workdirsz,
const char **src_out, int *emit_asm_out) const char **src_out, int *emit_asm_out)
{ {
*src_out = NULL; *src_out = NULL;
@@ -1209,6 +1399,23 @@ parse_build_flags(const char *cmd, int argc, char **argv,
return -1; return -1;
} }
*emit_asm_out = 1; *emit_asm_out = 1;
} else if (strcmp(argv[i], "-w") == 0) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
if (i + 1 >= argc) {
fprintf(stderr,
"ww %s: -w needs an argument\n", cmd);
return -1;
}
snprintf(workdir, workdirsz, "%s", argv[++i]);
} else if (strncmp(argv[i], "-w", 2) == 0 && argv[i][2]) {
if (workdir == NULL) {
fprintf(stderr, "ww %s: unknown flag\n", cmd);
return -1;
}
snprintf(workdir, workdirsz, "%s", argv[i] + 2);
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { } else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
size_t n = strlen(libs); size_t n = strlen(libs);
snprintf(libs + n, libsz - n, snprintf(libs + n, libsz - n,
@@ -1277,10 +1484,12 @@ do_build(int argc, char **argv)
char libdirs[2048] = {0}; char libdirs[2048] = {0};
char incs[2048] = {0}; char incs[2048] = {0};
char outflag[1024] = {0}; char outflag[1024] = {0};
char workdir[1024] = {0};
int emit_asm = 0; int emit_asm = 0;
if (parse_build_flags("build", argc, argv, incs, sizeof incs, if (parse_build_flags("build", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs, libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, &src, &emit_asm) < 0) outflag, sizeof outflag, workdir, sizeof workdir,
&src, &emit_asm) < 0)
return 2; return 2;
if (src == NULL) src = "."; /* default: build cwd */ if (src == NULL) src = "."; /* default: build cwd */
char resolved[1024]; char resolved[1024];
@@ -1307,7 +1516,7 @@ do_build(int argc, char **argv)
basename_no_ext(resolved, out, sizeof out); basename_no_ext(resolved, out, sizeof out);
} }
return build_one_sep(resolved, is_dir, out, objstem, incs, libs, return build_one_sep(resolved, is_dir, out, objstem, incs, libs,
libdirs, 0, emit_asm, 1); libdirs, 0, emit_asm, 1, workdir);
} }
static int static int
@@ -1320,7 +1529,7 @@ do_run(int argc, char **argv)
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */ char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, sizeof incs, int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs, libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, &src, NULL); outflag, sizeof outflag, NULL, 0, &src, NULL);
if (next < 0) return 2; if (next < 0) return 2;
if (src == NULL) src = "."; if (src == NULL) src = ".";
char resolved[1024]; char resolved[1024];
@@ -1339,7 +1548,7 @@ do_run(int argc, char **argv)
/* The freshly acquired directory owns both the executable and the /* The freshly acquired directory owns both the executable and the
* adjacent main.sepwork tree. Nothing outside it is adopted or removed. */ * adjacent main.sepwork tree. Nothing outside it is adopted or removed. */
if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, 0, if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, 0,
0) != 0) { 0, NULL) != 0) {
if (unlink(tmp) != 0 && errno != ENOENT) if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0) if (rmdir(tmpdir) != 0)
@@ -1398,6 +1607,7 @@ do_test(int argc, char **argv)
int compileonly = 0; int compileonly = 0;
int emit_asm = 0; int emit_asm = 0;
char outstem[1024] = {0}; char outstem[1024] = {0};
char workdir[1024] = {0};
int packageopts = 0; int packageopts = 0;
int afterdash = 0; int afterdash = 0;
/* #17: an optional second positional after the target is a fnmatch /* #17: an optional second positional after the target is a fnmatch
@@ -1455,6 +1665,15 @@ do_test(int argc, char **argv)
snprintf(outstem, sizeof outstem, "%s", argv[++i]); snprintf(outstem, sizeof outstem, "%s", argv[++i]);
} else if (argv[i][1] == 'o' && argv[i][2]) { } else if (argv[i][1] == 'o' && argv[i][2]) {
snprintf(outstem, sizeof outstem, "%s", argv[i] + 2); snprintf(outstem, sizeof outstem, "%s", argv[i] + 2);
} else if (strcmp(argv[i], "-w") == 0) {
if (i + 1 >= argc) {
fprintf(stderr,
"ww test: -w needs an argument\n");
return 2;
}
snprintf(workdir, sizeof workdir, "%s", argv[++i]);
} else if (argv[i][1] == 'w' && argv[i][2]) {
snprintf(workdir, sizeof workdir, "%s", argv[i] + 2);
} else { } else {
fprintf(stderr, "ww test: unknown flag\n"); fprintf(stderr, "ww test: unknown flag\n");
return 2; return 2;
@@ -1487,6 +1706,11 @@ do_test(int argc, char **argv)
"ww test: -c/-S/-o need a single test file\n"); "ww test: -c/-S/-o need a single test file\n");
return 2; return 2;
} }
if (workdir[0]) {
fprintf(stderr,
"ww test: -w needs a single test file\n");
return 2;
}
if (pattern) { if (pattern) {
fprintf(stderr, fprintf(stderr,
"ww test: pattern needs a single test file\n"); "ww test: pattern needs a single test file\n");
@@ -1501,8 +1725,14 @@ do_test(int argc, char **argv)
} }
char tmpdir[1024] = {0}, tmp[1024]; char tmpdir[1024] = {0}, tmp[1024];
const char *outp; const char *outp;
int owntmp = !outstem[0] && !workdir[0];
if (outstem[0]) outp = outstem; if (outstem[0]) outp = outstem;
else { else if (workdir[0]) {
/* The workdir owns the persistent test binary the same
* way it owns the package artifacts. */
snprintf(tmp, sizeof tmp, "%s/main", workdir);
outp = tmp;
} else {
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid()); snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) { if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n", fprintf(stderr, "ww: cannot create temporary directory %s\n",
@@ -1516,32 +1746,32 @@ do_test(int argc, char **argv)
* source. An explicit -o names the caller-owned artifact stem. */ * source. An explicit -o names the caller-owned artifact stem. */
int br = build_one_sep(resolved, is_dir, outp, int br = build_one_sep(resolved, is_dir, outp,
outstem[0] ? outstem : tmp, incs, "", "", 1, outstem[0] ? outstem : tmp, incs, "", "", 1,
emit_asm, outstem[0] ? 1 : 0); emit_asm, outstem[0] ? 1 : 0, workdir);
if (br != 0) { if (br != 0) {
if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (!outstem[0] && rmdir(tmpdir) != 0) if (owntmp && rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr); fputs("ww: cannot remove temporary directory\n", stderr);
return 1; return 1;
} }
if (compileonly || emit_asm) { if (compileonly || emit_asm) {
int cleanfail = 0; int cleanfail = 0;
if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
cleanfail = 1; cleanfail = 1;
} }
if (!outstem[0] && rmdir(tmpdir) != 0) { if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr); fputs("ww: cannot remove temporary directory\n", stderr);
cleanfail = 1; cleanfail = 1;
} }
return cleanfail ? 1 : 0; return cleanfail ? 1 : 0;
} }
int rc = run_test_bin(outp, pattern); int rc = run_test_bin(outp, pattern);
if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1; if (rc == 0) rc = 1;
} }
if (!outstem[0] && rmdir(tmpdir) != 0) { if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr); fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1; if (rc == 0) rc = 1;
} }
@@ -1556,8 +1786,12 @@ do_test(int argc, char **argv)
/* single .ww file — build, then run unless -c (compile-only). */ /* single .ww file — build, then run unless -c (compile-only). */
char tmpdir[1024] = {0}, tmp[1024]; char tmpdir[1024] = {0}, tmp[1024];
const char *outp; const char *outp;
int owntmp = !outstem[0] && !workdir[0];
if (outstem[0]) outp = outstem; if (outstem[0]) outp = outstem;
else { else if (workdir[0]) {
snprintf(tmp, sizeof tmp, "%s/main", workdir);
outp = tmp;
} else {
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid()); snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
if (mkdir(tmpdir, 0700) != 0) { if (mkdir(tmpdir, 0700) != 0) {
fprintf(stderr, "ww: cannot create temporary directory %s\n", fprintf(stderr, "ww: cannot create temporary directory %s\n",
@@ -1569,32 +1803,32 @@ do_test(int argc, char **argv)
} }
/* See module-mode note: no-o scratch is redirected to /tmp. */ /* See module-mode note: no-o scratch is redirected to /tmp. */
int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp, int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp,
incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0); incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0, workdir);
if (br != 0) { if (br != 0) {
if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (!outstem[0] && rmdir(tmpdir) != 0) if (owntmp && rmdir(tmpdir) != 0)
fputs("ww: cannot remove temporary directory\n", stderr); fputs("ww: cannot remove temporary directory\n", stderr);
return 1; return 1;
} }
if (compileonly || emit_asm) { if (compileonly || emit_asm) {
int cleanfail = 0; int cleanfail = 0;
if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
cleanfail = 1; cleanfail = 1;
} }
if (!outstem[0] && rmdir(tmpdir) != 0) { if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr); fputs("ww: cannot remove temporary directory\n", stderr);
cleanfail = 1; cleanfail = 1;
} }
return cleanfail ? 1 : 0; return cleanfail ? 1 : 0;
} }
int rc = run_test_bin(outp, pattern); int rc = run_test_bin(outp, pattern);
if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1; if (rc == 0) rc = 1;
} }
if (!outstem[0] && rmdir(tmpdir) != 0) { if (owntmp && rmdir(tmpdir) != 0) {
fputs("ww: cannot remove temporary directory\n", stderr); fputs("ww: cannot remove temporary directory\n", stderr);
if (rc == 0) rc = 1; if (rc == 0) rc = 1;
} }
@@ -1608,6 +1842,10 @@ do_test(int argc, char **argv)
fprintf(stderr, "ww test: -c/-S/-o need a single test file\n"); fprintf(stderr, "ww test: -c/-S/-o need a single test file\n");
return 2; return 2;
} }
if (workdir[0]) {
fprintf(stderr, "ww test: -w needs a single test file\n");
return 2;
}
/* The second bare positional remains the legacy single-file filter form; /* The second bare positional remains the legacy single-file filter form;
* package filtering uses explicit -run/-filter options. */ * package filtering uses explicit -run/-filter options. */
if (pattern) { if (pattern) {

View File

@@ -201,6 +201,22 @@ beneath one freshly acquired directory, remove both after every build result,
and make cleanup failure fail the command. Make recipes build driver-produced and make cleanup failure fail the command. Make recipes build driver-produced
tools in invocation-owned directories and apply the same exact cleanup rule. tools in invocation-owned directories and apply the same exact cleanup rule.
`ww build -w DIR` and single-file `ww test -w DIR` replace that scratch with a
caller-owned persistent package-artifact workdir: the directory must already
exist, is never cleaned by the driver, and holds one committed unit, `.wwi`,
`.s`, `.o`, and dep `.a` per package plus byte copies of the compiler and
assembler and a small mode stamp. A package is reused only when its freshly
composed unit byte-equals the committed unit and the recorded tool copies
byte-equal the live tools — content identity only, no mtimes, no hashes, every
decision reproducible with `cmp` against plain files. Recompiled artifacts
land at staged `.new` names and commit by rename with the unit renamed last,
so an interrupted build forces a recompile rather than a false reuse; the
link always reruns. One workdir serves one invocation at a time and one
(root, mode) shape; both driver stages implement the identical contract.
This is build staleness in the Make/mk/Go sense, not a result cache: tests
always run, and the byte-identity and bootstrap gates keep building on fresh
scratch. `make clean` reclaims every workdir under `out/`.
`test/lang` currently uses one package per source file, so its complete gate `test/lang` currently uses one package per source file, so its complete gate
still performs independent package builds. That remaining source layout is not still performs independent package builds. That remaining source layout is not
hidden behind caching or concurrency; it is outside the small `test` target. hidden behind caching or concurrency; it is outside the small `test` target.

View File

@@ -1294,9 +1294,174 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = {
// reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a. // reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a.
// Side files land in a cold `<stem>.sepwork` scratch dir. Twin of cstage // Side files land in a cold `<stem>.sepwork` scratch dir. Twin of cstage
// build_one_sep. // build_one_sep.
// ---- -w workdir freshness ----------------------------------------------
// A `-w DIR` workdir is a caller-owned persistent package-artifact tree
// that replaces the fresh `.sepwork` scratch. Staleness is pure content
// identity, never mtime: a package is reused only when its freshly
// composed unit byte-equals the committed unit AND the tool copies
// recorded in the dir byte-equal the live tools — every decision is
// reproducible by hand with cmp(1) against plain files. Artifacts commit
// via temp + rename with the unit renamed last, so a killed build can
// never leave a committed unit vouching for uncommitted artifacts. The
// caller serializes invocations per workdir and `make clean` reclaims
// the state. Cstage twin: cmd/ww/main.c file_equal/copy_file_atomic/
// workdir_stamp_text group.
// `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt
// emits no text), so committed presence is their freshness test; the
// rename-commit protocol owns integrity. `.o`/`.a` are never empty
// (ELF/ar headers), so a zero size there is always a torn write.
fn fileisreg(path: *u8) bool = {
let fi: os.filestat;
let ok: bool = false;
match (os.stat(&fi, pathstr(path))) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.REG: u32) { ok = true; };
};
case let e: os.oserror => void;
};
return ok;
};
fn filesizenonzero(path: *u8) bool = {
let fi: os.filestat;
let ok: bool = false;
match (os.stat(&fi, pathstr(path))) {
case void => {
let t: u32 = (fi.mode: u32) & 61440u32; // S_IFMT
if (t == os.mode.REG: u32) {
if (fi.sz > 0u64) { ok = true; };
};
};
case let e: os.oserror => void;
};
return ok;
};
// Byte equality of two files; absence or IO error is inequality.
fn fileequal(a: *u8, b: *u8) bool = {
let fa: i32 = os.open(pathstr(a), os.flag.RDONLY, 0i32);
if (fa < 0) { return false; };
let fb: i32 = os.open(pathstr(b), os.flag.RDONLY, 0i32);
if (fb < 0) { os.close(fa); return false; };
let bufa: []u8 = alloc([], 65536u64)!;
bufa.len = 65536;
let bufb: []u8 = alloc([], 65536u64)!;
bufb.len = 65536;
let eq: bool = true;
let done: bool = false;
for (!done) {
let na: i64 = os.read(fa, bufa.ptr, 65536u64);
let nb: i64 = os.read(fb, bufb.ptr, 65536u64);
if (na < 0 || na != nb) { eq = false; done = true; }
else { if (na == 0) { done = true; }
else {
let k: u64 = 0u64;
for (k < (na: u64)) {
if (bufa[k] != bufb[k]) {
eq = false; done = true; k = (na: u64);
};
k += 1u64;
};
}; };
};
os.close(fa);
os.close(fb);
return eq;
};
// Replace dst with src's bytes via temp + rename, so a torn write can
// never masquerade as a committed tool copy.
fn copyfileatomic(src: *u8, dst: *u8) i32 = {
let tmpp: *u8 = appendlit(dst, ".new");
let in: i32 = os.open(pathstr(src), os.flag.RDONLY, 0i32);
if (in < 0) { return -1; };
let out: i32 = os.open(pathstr(tmpp),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (out < 0) { os.close(in); return -1; };
let buf: []u8 = alloc([], 65536u64)!;
buf.len = 65536;
let bad: bool = false;
let done: bool = false;
for (!done) {
let n: i64 = os.read(in, buf.ptr, 65536u64);
if (n < 0) { bad = true; done = true; }
else { if (n == 0) { done = true; }
else {
match (os.writeall(out, buf.ptr, n: u64)) {
case let w: i64 => void;
case let e: os.oserror => { bad = true; done = true; };
};
}; };
};
os.close(in);
if (os.close(out) != 0) { bad = true; };
if (bad) { return -1; };
return os.rename(pathstr(tmpp), pathstr(dst));
};
// The stamp pins the non-content build inputs a unit compare cannot see:
// the -T/-S shape of the producer pass and the artifact protocol
// revision (bump "fmt" when the unit/archive/commit format changes).
fn workdirstamptext(istest: i32, emitasm: i32) str = {
if (istest != 0) {
if (emitasm != 0) {
return "ww workdir fmt 1 mode test asm 1\n";
};
return "ww workdir fmt 1 mode test asm 0\n";
};
if (emitasm != 0) {
return "ww workdir fmt 1 mode build asm 1\n";
};
return "ww workdir fmt 1 mode build asm 0\n";
};
fn stampmatches(path: *u8, want: str) bool = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return false; };
let buf: []u8 = alloc([], 128u64)!;
buf.len = 128;
let n: i64 = os.read(fd, buf.ptr, 127u64);
os.close(fd);
if (n < 0) { return false; };
if ((n: i32) != want.len) { return false; };
let k: u64 = 0u64;
let eq: bool = true;
for (k < (n: u64)) {
if (buf[k] != want.ptr[k]) { eq = false; k = (n: u64); };
k += 1u64;
};
return eq;
};
fn writestampatomic(path: *u8, want: str) i32 = {
let tmpp: *u8 = appendlit(path, ".new");
let fd: i32 = os.open(pathstr(tmpp),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (fd < 0) { return -1; };
let bad: bool = false;
match (os.writeall(fd, want.ptr, want.len: u64)) {
case let w: i64 => void;
case let e: os.oserror => { bad = true; };
};
if (os.close(fd) != 0) { bad = true; };
if (bad) { return -1; };
return os.rename(pathstr(tmpp), pathstr(path));
};
// cerrpath — the "ww: <head><path>\n" diagnostic shape shared by the
// workdir error sites; byte-identical wording to the cstage twin's
// fprintf(..., "%s", path) forms.
fn cerrpath(head: str, path: *u8, tail: str) void = {
cerr(head);
os.write(2, path, cstrlen(path));
cerr(tail);
};
fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32,
scratchout: **u8, graphout: **sepgraph) i32 = { workdir: *u8, scratchout: **u8, graphout: **sepgraph) i32 = {
let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); let c6: *u8 = joinpathlit(selfdir, "w6c_ww");
let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); let a6: *u8 = joinpathlit(selfdir, "w6a_ww");
let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); let l6: *u8 = joinpathlit(selfdir, "w6l_ww");
@@ -1371,14 +1536,63 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
}; };
let effstem: *u8 = stem.ptr; let effstem: *u8 = stem.ptr;
if (objstem != nil) { effstem = objstem; }; if (objstem != nil) { effstem = objstem; };
let scratch: *u8 = appendlit(effstem, ".sepwork"); let warm: bool = false;
if (os.mkdir(pathstr(scratch), 493i32) != 0) { if (workdir != nil) {
cerr("ww: cannot create scratch\n"); if (workdir[0u64] != 0u8) { warm = true; };
return 1; };
let scratch: *u8 = nil;
if (warm) {
let wfi: os.filestat;
let wok: bool = false;
match (os.stat(&wfi, pathstr(workdir))) {
case void => {
let wt: u32 = (wfi.mode: u32) & 61440u32; // S_IFMT
if (wt == os.mode.DIR: u32) { wok = true; };
};
case let e: os.oserror => void;
};
if (!wok) {
cerrpath("ww: workdir ", workdir,
" is not a directory\n");
return 1;
};
// The workdir is caller-owned and persistent: no acquisition,
// no refusal, and scratchout stays nil so the wrapper never
// cleans it.
scratch = workdir;
} else {
scratch = appendlit(effstem, ".sepwork");
if (os.mkdir(pathstr(scratch), 493i32) != 0) {
cerr("ww: cannot create scratch\n");
return 1;
};
// Hand the path back only after mkdir succeeds, so the wrapper
// never removes a pre-existing path that this invocation failed
// to acquire.
if (scratchout != nil) { *scratchout = scratch; };
};
let staleall: bool = false;
let stampok: bool = false;
let toolc: *u8 = nil;
let toola: *u8 = nil;
let stampf: *u8 = nil;
let stampwant: str = "";
if (warm) {
toolc = joinpathlit(scratch, ".wwtool.w6c");
toola = joinpathlit(scratch, ".wwtool.w6a");
stampf = joinpathlit(scratch, ".wwtool.stamp");
stampwant = workdirstamptext(istest, emitasm);
stampok = stampmatches(stampf, stampwant);
staleall = !stampok;
if (!staleall) {
if (!fileequal(toolc, c6)) { staleall = true; };
};
if (!staleall) {
if (emitasm == 0) {
if (!fileequal(toola, a6)) { staleall = true; };
};
};
}; };
// Hand the path back only after mkdir succeeds, so the wrapper never
// removes a pre-existing path that this invocation failed to acquire.
if (scratchout != nil) { *scratchout = scratch; };
// libwwrt.a path: <selfdir>/../lib/libwwrt.a // libwwrt.a path: <selfdir>/../lib/libwwrt.a
let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!;
@@ -1441,9 +1655,56 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
let wwi: *u8 = sepfname(g, pi, scratch, ".wwi"); let wwi: *u8 = sepfname(g, pi, scratch, ".wwi");
let asmf: *u8 = sepfname(g, pi, scratch, ".s"); let asmf: *u8 = sepfname(g, pi, scratch, ".s");
let objf: *u8 = sepfname(g, pi, scratch, ".o"); let objf: *u8 = sepfname(g, pi, scratch, ".o");
if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) { let apath: *u8 = sepfname(g, pi, scratch, ".a");
let unitnew: *u8 = sepfname(g, pi, scratch, ".unit.new");
let wwinew: *u8 = sepfname(g, pi, scratch, ".wwi.new");
let asmnew: *u8 = sepfname(g, pi, scratch, ".s.new");
let objnew: *u8 = sepfname(g, pi, scratch, ".o.new");
let anew: *u8 = sepfname(g, pi, scratch, ".a.new");
// Warm mode compiles from staged `.new` paths and commits by
// rename; classic mode keeps its exact in-place paths.
let cu: *u8 = unitf;
let cw: *u8 = wwi;
let cs: *u8 = asmf;
let co: *u8 = objf;
let ca: *u8 = apath;
if (warm) {
cu = unitnew; cw = wwinew; cs = asmnew;
co = objnew; ca = anew;
};
if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, cu) < 0) {
return 1; return 1;
}; };
let fresh: bool = false;
if (warm) {
if (!staleall) {
fresh = fileequal(unitnew, unitf);
if (fresh) { fresh = fileisreg(asmf); };
if (fresh) {
if (pi != root) {
fresh = fileisreg(wwi);
};
};
if (fresh) {
if (emitasm == 0) {
fresh = filesizenonzero(objf);
};
};
if (fresh) {
if (emitasm == 0 && pi != root) {
fresh = filesizenonzero(apath);
};
};
};
};
if (fresh) {
if (os.remove(pathstr(unitnew)) != 0) {
cerrpath("ww: cannot remove ", unitnew, "\n");
return 1;
};
oi += 1;
continue;
};
{ {
// BUG-1 (#69): -I <wwi> is purely the root's UNUSED // BUG-1 (#69): -I <wwi> is purely the root's UNUSED
// `.wwi` output path, but it triggers wwiemit -> // `.wwi` output path, but it triggers wwiemit ->
@@ -1463,11 +1724,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
append(argv, "-c"); append(argv, "-c");
if (pi != root) { if (pi != root) {
append(argv, "-I"); append(argv, "-I");
append(argv, pathstr(wwi)); append(argv, pathstr(cw));
}; };
append(argv, "-o"); append(argv, "-o");
append(argv, pathstr(asmf)); append(argv, pathstr(cs));
append(argv, pathstr(unitf)); append(argv, pathstr(cu));
let env: []str = os.getenvs(); let env: []str = os.getenvs();
let result: exec.result; let result: exec.result;
exec.runstdio(pathstr(c6), argv, env, &result); exec.runstdio(pathstr(c6), argv, env, &result);
@@ -1485,8 +1746,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
let argv: []str = alloc([], 4u64)!; let argv: []str = alloc([], 4u64)!;
append(argv, "w6a"); append(argv, "w6a");
append(argv, "-o"); append(argv, "-o");
append(argv, pathstr(objf)); append(argv, pathstr(co));
append(argv, pathstr(asmf)); append(argv, pathstr(cs));
let env: []str = os.getenvs(); let env: []str = os.getenvs();
let result: exec.result; let result: exec.result;
exec.runstdio(pathstr(a6), argv, env, &result); exec.runstdio(pathstr(a6), argv, env, &result);
@@ -1506,14 +1767,81 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// processed. The link // processed. The link
// consumes `.o`/`.a`, never `.wwi`. // consumes `.o`/`.a`, never `.wwi`.
if (emitasm == 0 && pi != root) { if (emitasm == 0 && pi != root) {
let apath: *u8 = sepfname(g, pi, scratch, ".a"); if (archiveo(co, ca) != 0) {
if (archiveo(objf, apath) != 0) {
cerr("ww: archive failed\n"); cerr("ww: archive failed\n");
return 1; return 1;
}; };
}; };
// Commit order: artifacts before the unit that vouches for
// them, unit strictly last.
if (warm) {
let bad: bool = false;
if (pi != root) {
if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) {
bad = true;
};
};
if (!bad) {
if (os.rename(pathstr(asmnew), pathstr(asmf)) != 0) {
bad = true;
};
};
if (!bad) {
if (emitasm == 0) {
if (os.rename(pathstr(objnew), pathstr(objf)) != 0) {
bad = true;
};
};
};
if (!bad) {
if (emitasm == 0 && pi != root) {
if (os.rename(pathstr(anew), pathstr(apath)) != 0) {
bad = true;
};
};
};
if (!bad) {
if (os.rename(pathstr(unitnew), pathstr(unitf)) != 0) {
bad = true;
};
};
if (bad) {
if (g.pkg[pi].path[0u64] != 0u8) {
cerrpath("ww: cannot commit ",
g.pkg[pi].path, "\n");
} else {
cerr("ww: cannot commit (root)\n");
};
return 1;
};
};
oi += 1; oi += 1;
}; };
// Tool identity commits only after every package artifact it vouches
// for is itself committed; a killed pass leaves the old identity and
// forces a full recompile, never a false reuse.
if (warm) {
if (!fileequal(toolc, c6)) {
if (copyfileatomic(c6, toolc) != 0) {
cerrpath("ww: cannot record ", toolc, "\n");
return 1;
};
};
if (emitasm == 0) {
if (!fileequal(toola, a6)) {
if (copyfileatomic(a6, toola) != 0) {
cerrpath("ww: cannot record ", toola, "\n");
return 1;
};
};
};
if (!stampok) {
if (writestampatomic(stampf, stampwant) != 0) {
cerrpath("ww: cannot record ", stampf, "\n");
return 1;
};
};
};
if (emitasm != 0) { return 0; }; if (emitasm != 0) { return 0; };
// Reverse-topo link: root `.o` first (order[norder-1]), dependency `.a` // Reverse-topo link: root `.o` first (order[norder-1]), dependency `.a`
@@ -1591,11 +1919,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
// exact tree. Twin of the cstage build_one_sep wrapper. // exact tree. Twin of the cstage build_one_sep wrapper.
fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8,
objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32,
keepscratch: i32) i32 = { keepscratch: i32, workdir: *u8) i32 = {
let scratch: *u8 = nil; let scratch: *u8 = nil;
let g: *sepgraph = nil; let g: *sepgraph = nil;
let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem, let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem,
incs, lf, istest, emitasm, &scratch, &g); incs, lf, istest, emitasm, workdir, &scratch, &g);
sepgraphfree(g); sepgraphfree(g);
if (keepscratch == 0 && scratch != nil) { if (keepscratch == 0 && scratch != nil) {
if (cstrendswithlit(scratch, ".sepwork")) { if (cstrendswithlit(scratch, ".sepwork")) {
@@ -1716,7 +2044,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
// ---- Subcommand handlers ---------------------------------------------- // ---- Subcommand handlers ----------------------------------------------
fn writeusage(fd: i32) void = { fn writeusage(fd: i32) void = {
let s: str = "usage: ww [-V] <subcommand> [args...]\n -V print version and exit\n build [-S] [-o FILE] [path] compile module; -S stops after package asm\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [options] [path] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's <basename>.ww\n"; let s: str = "usage: ww [-V] <subcommand> [args...]\n -V print version and exit\n build [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's <basename>.ww\n";
os.write(fd, s.ptr, s.len: u64); os.write(fd, s.ptr, s.len: u64);
}; };
@@ -1761,6 +2089,7 @@ fn defaultoutpath(src: *u8) *u8 = {
fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let src: *u8 = nil; let src: *u8 = nil;
let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3
let workdir: *u8 = nil; // -w persistent package-artifact workdir
let emitasm: i32 = 0; let emitasm: i32 = 0;
let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!;
incs.len = ((os.PATH_MAX: u64) * 2u64): i32; incs.len = ((os.PATH_MAX: u64) * 2u64): i32;
@@ -1846,10 +2175,21 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
i += 1; i += 1;
outflag = argv[i]; outflag = argv[i];
}; };
} else { if (p[1u64] == 119u8) { // '-w'
if (p[2u64] != 0u8) {
workdir = p + 2u64;
} else {
if (i + 1 >= argc) {
cerr("ww build: -w needs an argument\n");
return 2;
};
i += 1;
workdir = argv[i];
};
} else { } else {
cerr("ww build: unknown flag\n"); cerr("ww build: unknown flag\n");
return 2; return 2;
}; }; }; }; }; }; }; }; }; }; };
} else { } else {
if (src == nil) { src = p; }; if (src == nil) { src = p; };
}; };
@@ -1897,7 +2237,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.libs = libs.ptr; lf.libs = libs.ptr;
lf.nlibs = nlibs; lf.nlibs = nlibs;
return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf,
0i32, emitasm, 1i32); 0i32, emitasm, 1i32, workdir);
}; };
// Format the owned driver workspace /tmp/<prefix><pid> into buf. Pid is // Format the owned driver workspace /tmp/<prefix><pid> into buf. Pid is
@@ -2063,7 +2403,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
lf.nlibs = nlibs; lf.nlibs = nlibs;
// The freshly acquired directory owns both main and main.sepwork. // The freshly acquired directory owns both main and main.sepwork.
if (buildonesep(selfdir, resolved, isdir, outp, outp, incs.ptr, &lf, if (buildonesep(selfdir, resolved, isdir, outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32) != 0) { 0i32, 0i32, 0i32, nil) != 0) {
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n"); cerr("ww: cannot remove temporary output\n");
@@ -2118,17 +2458,26 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
// compatibility route; directory/default requests delegate to wwtest. // compatibility route; directory/default requests delegate to wwtest.
fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
emitasm: i32, outstem: *u8, pattern: *u8) i32 = { emitasm: i32, outstem: *u8, workdir: *u8, pattern: *u8) i32 = {
let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!;
tmp.len = os.PATH_MAX; tmp.len = os.PATH_MAX;
// -o redirects the binary + its caller-owned sepwork intermediates // -o redirects the binary + its caller-owned sepwork intermediates
// (objstem, T3) to <stem>; without -o both are driver-owned /tmp paths. // (objstem, T3) to <stem>; without -o both are driver-owned /tmp paths.
let outp: *u8 = nil; let outp: *u8 = nil;
let objstem: *u8 = nil; let objstem: *u8 = nil;
// owntmp: the driver owns (and must clean) the /tmp workspace; with
// -o or -w the binary lands in a caller-owned location instead.
let owntmp: bool = false;
if (outstem != nil) { if (outstem != nil) {
outp = outstem; outp = outstem;
objstem = outstem; objstem = outstem;
} else { if (workdir != nil) {
// The workdir owns the persistent test binary the same way it
// owns the package artifacts.
outp = joinpathlit(workdir, "main");
objstem = outp;
} else { } else {
owntmp = true;
makedrivertmp(tmp.ptr, "ww_test_"); makedrivertmp(tmp.ptr, "ww_test_");
if (os.mkdir(pathstr(tmp.ptr), 448i32) != 0) { if (os.mkdir(pathstr(tmp.ptr), 448i32) != 0) {
cerr("ww: cannot create temporary directory\n"); cerr("ww: cannot create temporary directory\n");
@@ -2136,7 +2485,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
}; };
outp = joinpathlit(tmp.ptr, "main"); outp = joinpathlit(tmp.ptr, "main");
objstem = outp; objstem = outp;
}; }; };
// E3-C1: separate compilation is the sole build path (task #87). // E3-C1: separate compilation is the sole build path (task #87).
let lf: lflags; let lf: lflags;
lf.libdirs = nil; lf.libdirs = nil;
@@ -2146,9 +2495,9 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
let keep: i32 = 0; let keep: i32 = 0;
if (outstem != nil) { keep = 1; }; if (outstem != nil) { keep = 1; };
let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf,
1i32, emitasm, keep); 1i32, emitasm, keep, workdir);
if (bres != 0) { if (bres != 0) {
if (outstem == nil) { if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n"); cerr("ww: cannot remove temporary output\n");
@@ -2160,7 +2509,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
return 1; return 1;
}; };
if (compileonly != 0 || emitasm != 0) { if (compileonly != 0 || emitasm != 0) {
if (outstem == nil) { if (owntmp) {
let cleanbad: bool = false; let cleanbad: bool = false;
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
@@ -2197,7 +2546,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
rc = -1; rc = -1;
}; };
}; }; }; };
if (outstem == nil) { if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n"); cerr("ww: cannot remove temporary output\n");
@@ -2232,6 +2581,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let compileonly: i32 = 0; let compileonly: i32 = 0;
let emitasm: i32 = 0; let emitasm: i32 = 0;
let outstem: *u8 = nil; let outstem: *u8 = nil;
let workdir: *u8 = nil;
let packageopts: bool = false; let packageopts: bool = false;
let afterdash: bool = false; let afterdash: bool = false;
let i: i32 = start; let i: i32 = start;
@@ -2293,6 +2643,19 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
}; };
i += 1; continue; i += 1; continue;
}; };
if (p[1u64] == 119u8) { // '-w'
if (p[2u64] != 0u8) {
workdir = p + 2u64;
} else {
if (i + 1 >= argc) {
cerr("ww test: -w needs an argument\n");
return 2;
};
i += 1;
workdir = argv[i];
};
i += 1; continue;
};
cerr("ww test: unknown flag\n"); return 2; cerr("ww test: unknown flag\n"); return 2;
} else { } else {
if (target == nil) { target = p; targetindex = i; } if (target == nil) { target = p; targetindex = i; }
@@ -2334,13 +2697,17 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
cerr("ww test: package options need a directory\n"); return 2; cerr("ww test: package options need a directory\n"); return 2;
}; };
return runsingletest(selfdir, resolved, incs.ptr, compileonly, return runsingletest(selfdir, resolved, incs.ptr, compileonly,
emitasm, outstem, patarg); emitasm, outstem, workdir, patarg);
}; };
if (outstem != nil) { if (outstem != nil) {
cerr("ww test: -c/-S/-o need a single test file\n"); cerr("ww test: -c/-S/-o need a single test file\n");
return 2; return 2;
}; };
if (workdir != nil) {
cerr("ww test: -w needs a single test file\n");
return 2;
};
if (patarg != nil) { if (patarg != nil) {
cerr("ww test: pattern needs a single test file\n"); cerr("ww test: pattern needs a single test file\n");
return 2; return 2;