From 71a9da20584d7a7522c8ee2d8108618a62c9929e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 20 Aug 2026 23:17:23 +0900 Subject: [PATCH] ww build: discard exact null outputs --- cmd/ww/main.c | 61 ++++- docs/build-system.md | 128 +++++++++ docs/spec.md | 19 +- docs/test-system-v2.md | 5 +- internal/wwpackage/package.ww | 26 +- selfhost/cmd/ww/main.ww | 46 +++- test/package/package_test.ww | 497 ++++++++++++++++++++++++++++++++++ 7 files changed, 751 insertions(+), 31 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 54cf79ee..8157b5b4 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -7059,7 +7059,8 @@ do_build(int argc, char **argv) } char out[PATH_MAX]; const char *objstem = NULL; - if (outflag[0]) { + int discard_output = strcmp(outflag, "/dev/null") == 0; + if (outflag[0] && !discard_output) { /* -o sets both the binary path and the intermediate stem so * artifacts land beside the requested output (T3). */ memcpy(out, outflag, strlen(outflag) + 1); @@ -7075,6 +7076,39 @@ do_build(int argc, char **argv) basename_no_ext(resolved, out, sizeof out); } const char *root_identity = !literal && is_dir ? src : NULL; + if (discard_output) { + char tmpdir[PATH_MAX], tmp[PATH_MAX]; + int dn = snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_build_%d", getpid()); + if (dn < 0 || (size_t)dn >= sizeof tmpdir + || mkdir(tmpdir, 0700) != 0) { + fputs("ww: cannot create temporary directory\n", stderr); + free(incs); + return 1; + } + int tn = snprintf(tmp, sizeof tmp, "%s/main", tmpdir); + if (tn < 0 || (size_t)tn >= sizeof tmp) { + (void)rmdir(tmpdir); + free(incs); + return 1; + } + /* Go's null output removes installation, while command linking and + * library compilation still need request-private product paths. */ + int rc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, + &linkflags, 0, 0, 0, SEP_VARIANT_PRODUCTION, NULL, + emit_asm, 0, workdir); + int cleanfail = 0; + if (unlink(tmp) != 0 && errno != ENOENT) { + fputs("ww: cannot remove temporary output\n", stderr); + cleanfail = 1; + } + if (rmdir(tmpdir) != 0) { + fputs("ww: cannot remove temporary directory\n", stderr); + cleanfail = 1; + } + free(incs); + if (cleanfail && rc == 0) rc = 1; + return rc; + } int rc = build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, &linkflags, outflag[0] != '\0', 0, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm, 1, workdir); @@ -7491,6 +7525,7 @@ do_test(int argc, char **argv) } } } + int discard_output = strcmp(outstem, "/dev/null") == 0; if (pattern != NULL) { struct stat first; if (stat(target, &first) != 0 || !S_ISREG(first.st_mode)) { @@ -7580,8 +7615,8 @@ do_test(int argc, char **argv) } char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX]; const char *outp; - int owntmp = !outstem[0] && !workdir[0]; - if (outstem[0]) outp = outstem; + int owntmp = (!outstem[0] || discard_output) && !workdir[0]; + if (outstem[0] && !discard_output) outp = outstem; else if (workdir[0]) { /* The workdir owns the persistent test binary the same * way it owns the package artifacts. */ @@ -7602,12 +7637,13 @@ do_test(int argc, char **argv) if (tn < 0 || (size_t)tn >= sizeof tmp) return 1; outp = tmp; } - /* No-o redirects internal scratch to /tmp rather than beside the - * source. An explicit -o names the caller-owned artifact stem. */ + /* No retained output keeps scratch private; exact /dev/null follows + * Go's no-install test action rather than naming an artifact stem. */ int br = build_one_sep(resolved, is_dir, NULL, outp, - outstem[0] ? outstem : tmp, incs, NULL, 0, 0, 1, + outstem[0] && !discard_output ? outstem : tmp, + incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, - outstem[0] ? 1 : 0, workdir); + outstem[0] && !discard_output ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); @@ -7651,8 +7687,8 @@ do_test(int argc, char **argv) } char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX]; const char *outp; - int owntmp = !outstem[0] && !workdir[0]; - if (outstem[0]) outp = outstem; + int owntmp = (!outstem[0] || discard_output) && !workdir[0]; + if (outstem[0] && !discard_output) outp = outstem; else if (workdir[0]) { int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir); if (tn < 0 || (size_t)tn >= sizeof tmp) { @@ -7671,10 +7707,11 @@ do_test(int argc, char **argv) if (tn < 0 || (size_t)tn >= sizeof tmp) return 1; outp = tmp; } - /* See module-mode note: no-o scratch is redirected to /tmp. */ - int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp, + /* See module-mode note: an unretained output uses private scratch. */ + int br = build_one_sep(target, 0, NULL, outp, + outstem[0] && !discard_output ? outstem : tmp, incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, - outstem[0] ? 1 : 0, workdir); + outstem[0] && !discard_output ? 1 : 0, workdir); if (br != 0) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) fputs("ww: cannot remove temporary output\n", stderr); diff --git a/docs/build-system.md b/docs/build-system.md index b81e2a91..a7aa6521 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -6405,6 +6405,134 @@ injected late-link request rollback over old files and modes; occupied-stage rejection; simultaneous builds with different umasks; diagnostic parity; artifact and binary byte identity; and absence of `.new` or umask-probe residue. +### 11.29 Implemented exact null-output discard for builds + +Exact `ww build -o /dev/null` now removes output installation while preserving +the ordinary load and action graph. It is not a request to create an archive, +executable, interface, or scratch tree at the null-device pathname. + +#### Pinned Go evidence and fact classification + +The sole authority is official Go 1.26.5 at commit +`c19862e5f8415b4f24b189d065ed739517c548ba`: + +- `runBuild` loads packages and reports load errors first, then recognizes the + null output and clears `BuildO` before any output-directory, multi-package, + or install-action branch. It finally constructs ordinary `ModeBuild` actions + for every selected package + ([`cmd/go/internal/work/build.go`, lines 459–558](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L558)). +- `AutoAction` maps a main package in that ordinary mode to a link action and a + non-main package to a compile action + ([`cmd/go/internal/work/action.go`, lines 450–455](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/action.go#L450-L455)). +- On the pinned Unix target, `IsNull` accepts exact `os.DevNull`, which is + `/dev/null`; its only additional spelling rule is the Windows-only + case-insensitive `NUL` exception + ([`cmd/go/internal/base/path.go`, lines 81–92](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/base/path.go#L81-L92)). +- The test builder treats a nonempty `-o` as a binary-retention request, exempts + the null device from multi-package output rejection, and makes a null target + use the private build action instead of an install action + ([`cmd/go/internal/test/test.go`, lines 631–646, 771–804, and 1259–1294](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1259-L1294)). +- Official `devnull.txt` requires `go test -c -o $devnull` and a non-main + package `go build -o $devnull` to succeed without changing the device + ([lines 3–25](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/devnull.txt#L3-L25)). + `build_dash_o_dev_null.txt` requires a command-line source build to succeed + without its default executable + ([lines 1–12](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_dash_o_dev_null.txt#L1-L12)). + `build_cache_link.txt` requires a cold null build to compile and link and an + unchanged warm null build to skip compilation but link again + ([lines 4–22](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_cache_link.txt#L4-L22)). + `TestRemoveDevNull` requires cleanup never to remove the device + ([`cmd/go/internal/work/build_test.go`, lines 22–35](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build_test.go#L22-L35)). + +The load-before-output order, exact spelling, absence of installation, normal +command link, normal non-command compile, cold/warm link behavior, raw-source +behavior, and device preservation are directly implemented or asserted by the +pinned sources above. Acceptance of multiple mixed package roots and an empty +matched set is derived from clearing `BuildO` before the output-cardinality +branches and then iterating the ordinary package action list. That conclusion +applies to WW's manifest-free local package set without importing Go's module, +cache, or distribution model. No installed host Go behavior is authority. + +#### Direct pre-fix measurements + +Fresh public-driver probes measured both stages before production edits: + +- raw source, one command directory, and one non-main library directory each + failed with exit 1 and `ww: cannot create scratch /dev/null.sepwork`; +- a two-command-plus-library request failed with exit 2 and + `wwtest package: cannot use -o with multiple packages`; +- a persistent command request reached the linker but failed through + `/dev/null.new`: Cstage reported `w6l: cannot open /dev/null.new`, WWstage + reported `w6l: cannot open output`, and both left the caller workdir empty; +- raw `ww test -c -o /dev/null FILE` and its running form failed at the same + adjacent-scratch acquisition, while directory `test -c` and running test + requests already built privately, discarded the retained copy, and preserved + their compile-only versus run distinction; +- a blank import of a missing package produced the same positioned + `cannot find package missing.pkg` diagnostic in Cstage and WWstage before + output setup; and +- `/dev/null` remained the same character device, mode, device/inode, and size + throughout the failed probes. + +Those are directly measured WW facts. The externally observable gap was thus +the build/raw-driver interpretation of exact null as an artifact stem, plus the +coordinator's ordinary multi-output rejection, rather than a loader, compiler, +linker, or device-write defect. + +#### Ownership, actions, publication, and identity + +`internal/wwpackage.packagecommand` owns the shared package-request output +policy. After argument parsing and before output planning it records exact Unix +null discard. Loading, source classification, package/import validation, +canonical grouping, graph construction, and diagnostic precedence remain +unchanged. The coordinator suppresses default names, output-directory setup, +ordinary non-directory fan-out rejection, caller publication, and the +visibility-only `-S` workdir requirement, then gives every selected group a +request-private plan product. Commands still link; libraries still compile and +archive; mixed and repeated roots still use their canonical graph/action +deduplication. A recursive pattern matching no package emits its ordinary +warning and has no output-cardinality error. + +The direct Cstage and WWstage drivers own raw or single-directory requests that +do not enter the coordinator. For exact null they allocate a private command +product, run the unchanged separate-compilation pipeline, and remove that +product and its scratch on every return. Their raw-test paths use the existing +unretained private test binary rather than setting `/dev/null` as output and +object stem. `-c` still suppresses execution; a running request still reports +ordinary pass, assertion, signal, and harness outcomes. Directory tests retain +their previously established private-runnable/null-publication behavior. + +There is no caller-visible stage or destination to commit, occupy, replace, or +chmod. Producer failure or signal removes private plan state; persistent action +rollback preserves every prior unit, interface, assembly, object, archive, +tool record, and stamp. Successful persistent requests commit semantic actions +normally, unchanged actions are warm-reused, source changes invalidate their +owners, and command links still run for each request. Separate simultaneous +Cstage and WWstage requests own disjoint private products and workdirs. Exact +lookalikes remain normal caller-owned outputs and retain their existing +fan-out, `.sepwork`, permission, occupied-stage, transaction, and diagnostic +rules. + +Output disposition remains request metadata. Dotted package identity, declared +name, physical source directory, import binding, graph edges, action/storage +keys, compiler/assembler/linker semantic argv, symbols, `.wwi`, unit and +artifact bytes, and persistent invalidation are unchanged. No persisted byte +contract changed: build workdir format remains `18`, test workdir format remains +`19`, and semantic storage remains `3`. + +The WW-native owner `exact_null_output_discards_build_products` covers Cstage +and WWstage command, library, mixed-root, raw-build, assembly-only, and raw-test +routes; load/import precedence; empty-pattern warning; exact lookalike +rejection/publication; cold, warm, and invalidated persistence; normal link +actions and captured runnable bytes; injected linker failure and signal; +persistent rollback; concurrent stage isolation; device preservation; private +path and `.new` cleanup; semantic-artifact and captured-binary byte identity; +and diagnostic/output parity. Existing directory-test owners cover test-product +parallelism, timeout, child-setup failure, retained-output transactions, and +runtime cwd/environment/stdin. Build runtime behavior is inapplicable, and +caller-output rollback, occupied caller stages, and output permissions are +inapplicable to the exact discard branch because it creates no public inode. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/docs/spec.md b/docs/spec.md index 5494ba0e..0c8bdb4f 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -319,6 +319,19 @@ ImportPath = ident { "." ident } . build therefore reuses unchanged semantic actions while refreshing the caller-visible output with the current invocation's mode. Assembly-only builds publish no executable or archive. +- On the Unix target, exact `-o /dev/null` is a build-output discard request, + not an ordinary output filename. Package loading, import validation, graph + construction, compilation, assembly, archiving, command linking, failure, + and persistent invalidation proceed normally; only installation of a + caller-visible command, archive, interface, default basename, or adjacent + scratch tree is omitted. One such request may select any number of command + and non-command packages, and an empty recursive match is successful after + its normal warning. `-S` likewise needs no caller workdir merely to retain + discarded assembly. Raw-file and directory roots use the same rule. A + non-exact spelling is an ordinary output and keeps all established fan-out, + rejection, permission, scratch, and publication behavior. Output discard is + request metadata; it does not change package, import, graph, action, + artifact, symbol, `.wwi`, or persistence identity. - Only names marked `export` (§5) are visible across module boundaries. Import paths remain unquoted and dotted. Grouped imports, quoted import paths, @@ -662,7 +675,11 @@ directory. An output ending in `/` or naming an existing directory receives that basename and may have missing parent directories created. One non-directory output may name only one package. Multiple packages whose visible import leaves would produce the same test-binary name reject before -tools or output creation; exact `/dev/null` is the discard exception. Declared +tools or output creation; exact `/dev/null` is the discard exception. It keeps +the ordinary private link and, unless `-c` is present, the ordinary execution, +but installs no retained copy. The raw single-file compatibility route uses +the same private-output rule and never treats `/dev/null` as an explicit +artifact or adjacent-scratch stem. Declared package names, test variants, source filenames, physical directories, output paths, and retained binary names remain presentation or loader metadata and do not become canonical package or action identity. diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 5186df10..ca2f2012 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -227,7 +227,10 @@ naming an existing directory receives one such name per selected package and missing parents are created. A single non-directory output is legal for one package only. Multi-package non-directory output and duplicate visible binary names reject before tools; exact `/dev/null` discards every copy and permits -duplicate names. A package with no selected test source validates production +duplicate names. The private test link still runs unless `-c` suppresses it; +the raw single-file compatibility route also links into driver-owned private +storage instead of using `/dev/null` as an artifact or `.sepwork` stem. A +package with no selected test source validates production but publishes nothing and does not create an otherwise unneeded output directory. Successful `-c` output is silent apart from no-test reporting. diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index 80507ccf..137c7ebf 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -1844,6 +1844,8 @@ export fn packagecommand(args: []str) int = { }; if (roots.len == 0) { append(roots, "."); }; pkgsortstrings(roots); + let buildnull: bool = buildonly && explicitout + && strings.compare(outname, "/dev/null") == 0; if (compileonly && !buildonly && (list || filters.len != 0 || timeoutarg.len != 0)) { pkgusage(); @@ -1946,11 +1948,11 @@ export fn packagecommand(args: []str) int = { ds.paths = pkgdedup(ds.paths); if (ds.paths.len == 0) { if (buildonly) { - if (explicitout && pkgoutputdir(outname)) { + if (explicitout && !buildnull && pkgoutputdir(outname)) { pkgputln(os.STDERR_FILENO, "ww: no main packages to build"); return 1; }; - if (explicitout) { + if (explicitout && !buildnull) { pkgputln(os.STDERR_FILENO, "ww: no packages to build"); return 1; }; @@ -2177,11 +2179,11 @@ export fn packagecommand(args: []str) int = { }; if (groups.len == 0) { if (buildonly) { - if (explicitout && pkgoutputdir(outname)) { + if (explicitout && !buildnull && pkgoutputdir(outname)) { pkgputln(os.STDERR_FILENO, "ww: no main packages to build"); return 1; }; - if (explicitout) { + if (explicitout && !buildnull) { pkgputln(os.STDERR_FILENO, "ww: no packages to build"); return 1; }; @@ -2192,7 +2194,7 @@ export fn packagecommand(args: []str) int = { }; pkgsortgroups(groups); let defaultout: bool = false; - if (buildonly && outname.len == 0 && folders.len == 1 + if (buildonly && !buildnull && outname.len == 0 && folders.len == 1 && groups.len == 1 && strings.compare(groups[0].pkg, "main") == 0) { outname = pkgbase(groups[0].dir); defaultout = true; @@ -2203,7 +2205,8 @@ export fn packagecommand(args: []str) int = { pkgputln(os.STDERR_FILENO, "\" already exists and is a directory"); return 1; }; - let outputdir: bool = buildonly && explicitout && pkgoutputdir(outname); + let outputdir: bool = buildonly && explicitout && !buildnull + && pkgoutputdir(outname); if (outputdir) { let mainpackages: i32 = 0; i = 0; @@ -2330,7 +2333,8 @@ export fn packagecommand(args: []str) int = { }; // A non-directory caller-owned build name cannot fan out. A directory // build output publishes each selected command under its directory leaf. - if (buildonly && outname.len != 0 && !outputdir && groups.len > 1) { + if (buildonly && !buildnull && outname.len != 0 + && !outputdir && groups.len > 1) { pkgputln(os.STDERR_FILENO, "wwtest package: cannot use -o with multiple packages"); return 2; @@ -2338,7 +2342,7 @@ export fn packagecommand(args: []str) int = { // Recursive/multi-root -S needs a caller-owned artifact tree. Without -w // every assembly file would otherwise live only in the coordinator's // temporary plan and disappear on successful return. - if (buildonly && emitasm && workroot.len == 0) { + if (buildonly && emitasm && workroot.len == 0 && !buildnull) { pkgputln(os.STDERR_FILENO, "wwtest package: recursive -S needs -w"); return 2; @@ -2360,7 +2364,7 @@ export fn packagecommand(args: []str) int = { plan.end = groups.len; plan.state = PKGQUEUED; plan.buildonly = buildonly; - plan.publish = buildonly && explicitout && outname.len != 0 + plan.publish = buildonly && explicitout && !buildnull && outname.len != 0 && groups.len == 1 && strings.compare(groups[0].pkg, "main") != 0; plan.emitasm = emitasm; @@ -2380,11 +2384,13 @@ export fn packagecommand(args: []str) int = { }; let tmproot: str = temp.dir(); + let planout: str = outname; + if (buildnull) { planout = ""; }; let failed: i32 = 0; i = 0; for (i < plans.len) { if (!pkgsetplanpaths(&plans[i], groups, tmproot, i, - compileonly, buildonly, outname, outputdir, createdir, + compileonly, buildonly, planout, outputdir, createdir, workroot)) { if (!pkgremoveall(tmproot)) { pkgput(os.STDERR_FILENO, diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 58f919d9..c4cc5671 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -8502,7 +8502,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; let out: *u8 = nil; let objstem: *u8 = nil; - if (outflag != nil && outflag[0u64] != 0u8) { + let discardoutput: bool = outflag != nil + && cstreqlit(outflag, "/dev/null"); + if (outflag != nil && outflag[0u64] != 0u8 && !discardoutput) { // -o sets both the binary path and the intermediate stem so // artifacts land beside the requested output (T3). out = outflag; @@ -8531,6 +8533,34 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibs = nlibs; let rootidentity: *u8 = nil; if (!requestedliteral && isdir != 0) { rootidentity = src; }; + if (discardoutput) { + let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; + tmp.len = os.PATH_MAX; + makedrivertmp(tmp.ptr, "ww_build_"); + if (os.mkdir(pathstr(tmp.ptr), 448i32) != 0) { + cerr("ww: cannot create temporary directory\n"); + return 1; + }; + let outp: *u8 = joinpathlit(tmp.ptr, "main"); + // Go's null output removes installation, while command linking and + // library compilation still need request-private product paths. + let rc: i32 = buildonesep(selfdir, resolved, isdir, rootidentity, + outp, outp, incs.ptr, &lf, + 0i32, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil, + emitasm, 0i32, workdir); + let cleanbad: bool = false; + let cleanrc: i32 = os.remove(pathstr(outp)); + if (cleanrc != 0 && cleanrc != -2i32) { + cerr("ww: cannot remove temporary output\n"); + cleanbad = true; + }; + if (os.rmdir(pathstr(tmp.ptr)) != 0) { + cerr("ww: cannot remove temporary directory\n"); + cleanbad = true; + }; + if (cleanbad && rc == 0) { rc = 1; }; + return rc; + }; let publishpackage: i32 = 0; if (outflag != nil && outflag[0u64] != 0u8) { publishpackage = 1; }; return buildonesep(selfdir, resolved, isdir, rootidentity, @@ -8774,14 +8804,16 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, emitasm: i32, outstem: *u8, workdir: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; - // -o redirects the binary + its caller-owned sepwork intermediates - // (objstem, T3) to ; without -o both are driver-owned /tmp paths. + // A retained -o redirects the binary and sepwork intermediates to its stem; + // exact /dev/null needs the no-install path used by Go's test builder. let outp: *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. + // A retained output or -w has caller-owned storage; every other binary is + // request-private and must be removed here. let owntmp: bool = false; - if (outstem != nil) { + let retainout: bool = outstem != nil + && !cstreqlit(outstem, "/dev/null"); + if (retainout) { outp = outstem; objstem = outstem; } else { if (workdir != nil) { @@ -8806,7 +8838,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, lf.libs = nil; lf.nlibs = 0; let keep: i32 = 0; - if (outstem != nil) { keep = 1; }; + if (retainout) { keep = 1; }; let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf, 0i32, 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil, emitasm, keep, workdir); diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 92743d5f..be533d8f 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -542,6 +542,16 @@ fn directoryisempty(path: str) bool = { return true; }; +fn discardedlinkcleaned(marker: str) void = { + let output: str = readfile(marker); + assert(output.len != 0); + assert(!has(output, "/dev/null")); + let slash: i32 = output.len - 1; + for (slash >= 0 && output[slash] != '/') { slash -= 1; }; + assert(slash > 0); + assert(!os.exists(strings.sub(output, 0, slash))); +}; + fn byteshasat(s: str, off: i32, want: str) bool = { if (off < 0 || off + want.len > s.len) { return false; }; let i: i32 = 0; @@ -12990,3 +13000,490 @@ fn runtimepath(relative: str) str = { assert(!directoryhasnew(parallelwwwork)); clean(root); }; + +@test fn exact_null_output_discards_build_products() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source"); + let base: str = strings.concat(source, "/base"); + let alpha: str = strings.concat(source, "/alpha"); + let beta: str = strings.concat(source, "/beta"); + let library: str = strings.concat(source, "/library"); + let bad: str = strings.concat(source, "/bad"); + let absent: str = strings.concat(source, "/absent"); + mkdirall(base); mkdirall(alpha); mkdirall(beta); + mkdirall(library); mkdirall(bad); mkdirall(absent); + writefile(strings.concat(base, "/base.ww"), strings.concat( + "package base;\n", + "export fn value() i32 = { return 4; };\n")); + let alphafile: str = strings.concat(alpha, "/main.ww"); + let alphaoriginal: str = strings.concat( + "package main;\nimport base;\n", + "fn main() i32 = { return base.value(); };\n"); + let alphachanged: str = strings.concat( + "package main;\nimport base;\n", + "fn main() i32 = { return base.value() + 1; };\n"); + let alphafailing: str = strings.concat( + "package main;\nimport base;\n", + "fn main() i32 = { return base.value() + 2; };\n"); + writefile(alphafile, alphaoriginal); + writefile(strings.concat(beta, "/main.ww"), + "package main;\nfn main() i32 = { return 6; };\n"); + writefile(strings.concat(library, "/library.ww"), strings.concat( + "package odd;\n", + "export fn value() i32 = { return 7; };\n")); + writefile(strings.concat(bad, "/main.ww"), strings.concat( + "package main;\nimport _ missing.pkg;\n", + "fn main() i32 = { return 0; };\n")); + let raw: str = strings.concat(source, "/raw.ww"); + writefile(raw, "package main;\nfn main() i32 = { return 9; };\n"); + + let compilerwrapper: str = strings.concat(root, "/null-w6c.sh"); + let assemblerwrapper: str = strings.concat(root, "/null-w6a.sh"); + let linkerwrapper: str = strings.concat(root, "/null-w6l.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\n", + "printf '%s\\n' \"$*\" >> \"$WW_NULL_CTRACE\"\n", + "exec \"$WW_NULL_REAL_C\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\n", + "printf '%s\\n' \"$*\" >> \"$WW_NULL_ATRACE\"\n", + "exec \"$WW_NULL_REAL_A\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\n", + "printf '%s\\n' \"$*\" >> \"$WW_NULL_LTRACE\"\n", + "out= take=\n", + "for arg do\n", + " if test \"$take\" = yes; then out=$arg; take=; continue; fi\n", + " if test \"$arg\" = -o; then take=yes; fi\n", + "done\n", + "printf '%s' \"$out\" > \"$WW_NULL_LAST_OUTPUT\"\n", + "case \"$WW_NULL_LINK_MODE\" in\n", + " fail) printf 'injected null-output linker failure\\n' >&2; exit 97;;\n", + " signal) kill -TERM \"$$\";;\n", + "esac\n", + "\"$WW_NULL_REAL_L\" \"$@\"\n", + "rc=$?\n", + "if test \"$rc\" -eq 0 && test -n \"$WW_NULL_CAPTURE\"; then\n", + " cp \"$out\" \"$WW_NULL_CAPTURE\" || exit 98\n", + "fi\n", + "exit \"$rc\"\n")); + + let nullbefore: os.filestat; + match (os.stat(&nullbefore, "/dev/null")) { + case void => void; + case let e: os.oserror => abort("cannot stat /dev/null"); + }; + assert(((nullbefore.mode as u32) & 61440u32) + == (os.mode.CHR as u32)); + let stages: []str = ["ww", "ww_ww"]; + let compilers: []str = ["w6c", "w6c_ww"]; + let assemblers: []str = ["w6a", "w6a_ww"]; + let linkers: []str = ["w6l", "w6l_ww"]; + let tags: []str = ["c", "ww"]; + let coldref: str = ""; + let changedref: str = ""; + let rawref: str = ""; + let lookalikeref: str = ""; + let faildiagref: str = ""; + let signaldiagref: str = ""; + let missingdiagref: str = ""; + let emptydiagref: str = ""; + let rejectdiagref: str = ""; + let testcompileref: str = ""; + let testoutrefs: []str = ["", "", ""]; + let testerrrefs: []str = ["", "", ""]; + let baseenv: []str = os.getenvs(); + let suffixes: []str = [".unit.ww", ".wwi", ".s", ".o", ".a"]; + let out: commandout; + let si: i32 = 0; + for (si < stages.len) { + rewritefile(alphafile, alphaoriginal); + let work: str = strings.concat(root, "/work-", tags[si]); + mkdirall(work); + let ctrace: str = strings.concat(root, "/", tags[si], "-c.trace"); + let atrace: str = strings.concat(root, "/", tags[si], "-a.trace"); + let ltrace: str = strings.concat(root, "/", tags[si], "-l.trace"); + let lastout: str = strings.concat(root, "/", tags[si], "-last.out"); + let capture: str = strings.concat(root, "/", tags[si], "-capture"); + writefile(ctrace, ""); writefile(atrace, ""); + writefile(ltrace, ""); writefile(lastout, ""); + let env: []str = alloc([], (baseenv.len + 12): u64)!; + let ei: i32 = 0; + for (ei < baseenv.len) { + if (!strings.hasprefix(baseenv[ei], "WW_W6C=") + && !strings.hasprefix(baseenv[ei], "WW_W6A=") + && !strings.hasprefix(baseenv[ei], "WW_W6L=") + && !strings.hasprefix(baseenv[ei], "WW_NULL_")) { + append(env, baseenv[ei]); + }; + ei += 1; + }; + append(env, strings.concat("WW_W6C=", compilerwrapper)); + append(env, strings.concat("WW_W6A=", assemblerwrapper)); + append(env, strings.concat("WW_W6L=", linkerwrapper)); + append(env, strings.concat("WW_NULL_CTRACE=", ctrace)); + append(env, strings.concat("WW_NULL_ATRACE=", atrace)); + append(env, strings.concat("WW_NULL_LTRACE=", ltrace)); + append(env, strings.concat("WW_NULL_REAL_C=", + driver(compilers[si]))); + append(env, strings.concat("WW_NULL_REAL_A=", + driver(assemblers[si]))); + append(env, strings.concat("WW_NULL_REAL_L=", + driver(linkers[si]))); + let linkmodeindex: i32 = env.len; + append(env, "WW_NULL_LINK_MODE="); + let captureindex: i32 = env.len; + append(env, strings.concat("WW_NULL_CAPTURE=", capture)); + append(env, strings.concat("WW_NULL_LAST_OUTPUT=", lastout)); + + // Exact null is no-install policy. Loading, dependency production, + // assembly, archiving, and the command link still occur normally. + let coldav: []str = [driver(stages[si]), "build", "-w", work, + "-I", source, "-o", "/dev/null", alpha]; + runcommandenvdir(root, strings.concat("null-cold-", tags[si]), coldav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(occurrences(readfile(ctrace), "\n") == 2); + assert(occurrences(readfile(atrace), "\n") == 3); + assert(occurrences(readfile(ltrace), "\n") == 1); + assert(has(readfile(ctrace), "/base.unit.new")); + assert(has(readfile(ctrace), "/alpha.unit.new")); + discardedlinkcleaned(lastout); + let coldbytes: str = readfile(capture); + if (si == 0) { coldref = strings.dup(coldbytes); } + else { assert(same(coldref, coldbytes)); }; + let runav: []str = [capture]; + runcommand(root, strings.concat("null-cold-run-", tags[si]), runav, + time.second, &out); + expectexit(&out, 4); + let originalunit: str = readfile(strings.concat(work, + "/alpha.unit.ww")); + + // Unchanged actions are warm-reused, but the command links again into + // fresh private storage because null output is not a result cache. + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + rewritefile(capture, ""); + runcommandenvdir(root, strings.concat("null-warm-", tags[si]), coldav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0); + assert(occurrences(readfile(ltrace), "\n") == 1); + assert(same(coldbytes, readfile(capture))); + discardedlinkcleaned(lastout); + + // Source change invalidates the owning action and linked bytes while + // preserving the same action names and persistent representation. + rewritefile(alphafile, alphachanged); + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + rewritefile(capture, ""); + runcommandenvdir(root, strings.concat("null-change-", tags[si]), coldav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(ctrace), "\n") == 1); + assert(occurrences(readfile(atrace), "\n") == 1); + assert(occurrences(readfile(ltrace), "\n") == 1); + assert(!same(originalunit, readfile(strings.concat(work, + "/alpha.unit.ww")))); + let changedbytes: str = readfile(capture); + assert(!same(coldbytes, changedbytes)); + if (si == 0) { changedref = strings.dup(changedbytes); } + else { assert(same(changedref, changedbytes)); }; + discardedlinkcleaned(lastout); + let kept: []str = alloc([], suffixes.len: u64)!; + let xi: i32 = 0; + for (xi < suffixes.len) { + append(kept, strings.dup(readfile(strings.concat(work, + "/alpha", suffixes[xi])))); + xi += 1; + }; + + // A late producer failure and a producer signal cannot publish and + // cannot commit an invalidated persistent generation. + rewritefile(alphafile, alphafailing); + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + env[linkmodeindex] = "WW_NULL_LINK_MODE=fail"; + runcommandenvdir(root, strings.concat("null-fail-", tags[si]), coldav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "injected null-output linker failure\n")); + assert(has(out.stderr, "ww: w6l failed\n")); + assert(occurrences(readfile(ltrace), "\n") == 1); + discardedlinkcleaned(lastout); + xi = 0; + for (xi < suffixes.len) { + assert(same(kept[xi], readfile(strings.concat(work, + "/alpha", suffixes[xi])))); + xi += 1; + }; + assert(!directoryhasnew(work)); + if (si == 0) { faildiagref = strings.dup(out.stderr); } + else { assert(same(faildiagref, out.stderr)); }; + + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + env[linkmodeindex] = "WW_NULL_LINK_MODE=signal"; + runcommandenvdir(root, strings.concat("null-signal-", tags[si]), coldav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "ww: w6l failed\n")); + discardedlinkcleaned(lastout); + xi = 0; + for (xi < suffixes.len) { + assert(same(kept[xi], readfile(strings.concat(work, + "/alpha", suffixes[xi])))); + xi += 1; + }; + assert(!directoryhasnew(work)); + if (si == 0) { signaldiagref = strings.dup(out.stderr); } + else { assert(same(signaldiagref, out.stderr)); }; + env[linkmodeindex] = "WW_NULL_LINK_MODE="; + + // Import loading and empty-pattern handling precede output planning. + // The exact null exception changes neither diagnostic nor tool order. + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + let badav: []str = [driver(stages[si]), "build", "-I", source, + "-o", "/dev/null", bad]; + runcommandenvdir(root, strings.concat("null-import-", tags[si]), badav, + env, root, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "cannot find package missing.pkg\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0 && readfile(lastout).len == 0); + if (si == 0) { missingdiagref = strings.dup(out.stderr); } + else { assert(same(missingdiagref, out.stderr)); }; + + let emptyroot: str = strings.concat(source, "/absent/..."); + let emptyav: []str = [driver(stages[si]), "build", "-o", + "/dev/null", emptyroot]; + runcommandenvdir(root, strings.concat("null-empty-", tags[si]), emptyav, + env, root, time.second, &out); + expectexit(&out, 0); + assert(has(out.stderr, "matched no packages\n")); + assert(readfile(ctrace).len == 0 && readfile(ltrace).len == 0); + if (si == 0) { emptydiagref = strings.dup(out.stderr); } + else { assert(same(emptydiagref, out.stderr)); }; + + // One discard request may contain commands and a non-main library. + // Every action runs, only commands link, and no basename is published. + rewritefile(alphafile, alphaoriginal); + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + let multiav: []str = [driver(stages[si]), "build", "-I", source, + "-o", "/dev/null", alpha, beta, library]; + runcommandenvdir(root, strings.concat("null-multi-", tags[si]), multiav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(occurrences(readfile(ctrace), "\n") == 4); + assert(occurrences(readfile(atrace), "\n") == 6); + assert(occurrences(readfile(ltrace), "\n") == 2); + assert(!os.exists(strings.concat(root, "/alpha"))); + assert(!os.exists(strings.concat(root, "/beta"))); + assert(!os.exists(strings.concat(root, "/library"))); + assert(!os.exists("/dev/null.new")); + assert(!os.exists("/dev/null.sepwork")); + + // Assembly-only keeps the same loading/action exception, has no link, + // and needs no persistent workdir solely to make discarded files visible. + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + let asmav: []str = [driver(stages[si]), "build", "-S", "-I", source, + "-o", "/dev/null", alpha, beta, library]; + runcommandenvdir(root, strings.concat("null-asm-", tags[si]), asmav, + env, root, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(ctrace), "\n") == 4); + assert(readfile(atrace).len == 0 && readfile(ltrace).len == 0); + + // The direct raw-file route links privately too. A wrapper capture proves + // the linked bytes are runnable before the driver's cleanup. + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + rewritefile(capture, ""); + let rawav: []str = [driver(stages[si]), "build", "-o", + "/dev/null", raw]; + runcommandenvdir(root, strings.concat("null-raw-", tags[si]), rawav, + env, root, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(occurrences(readfile(ltrace), "\n") == 1); + discardedlinkcleaned(lastout); + let rawbytes: str = readfile(capture); + if (si == 0) { rawref = strings.dup(rawbytes); } + else { assert(same(rawref, rawbytes)); }; + let rawrun: []str = [capture]; + runcommand(root, strings.concat("null-raw-run-", tags[si]), rawrun, + time.second, &out); + expectexit(&out, 9); + + // Null recognition is exact. A regular lookalike is retained with its + // established adjacent scratch; a multi-root lookalike still rejects. + let lookalike: str = strings.concat(root, "/null-like-", tags[si]); + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + env[captureindex] = "WW_NULL_CAPTURE="; + let lookav: []str = [driver(stages[si]), "build", "-o", + lookalike, raw]; + runcommandenvdir(root, strings.concat("null-look-", tags[si]), lookav, + env, root, (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(os.exists(lookalike)); + assert(os.exists(strings.concat(lookalike, ".sepwork"))); + let lookbytes: str = readfile(lookalike); + if (si == 0) { lookalikeref = strings.dup(lookbytes); } + else { assert(same(lookalikeref, lookbytes)); }; + let rejectout: str = strings.concat(root, "/multi-like-", tags[si]); + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); + let rejectav: []str = [driver(stages[si]), "build", "-I", source, + "-o", rejectout, alpha, beta]; + runcommandenvdir(root, strings.concat("null-reject-", tags[si]), + rejectav, env, root, time.second, &out); + expectexit(&out, 2); + assert(has(out.stderr, "cannot use -o with multiple packages\n")); + assert(readfile(ctrace).len == 0 && readfile(atrace).len == 0 + && readfile(ltrace).len == 0); + if (si == 0) { rejectdiagref = strings.dup(out.stderr); } + else { assert(same(rejectdiagref, out.stderr)); }; + env[captureindex] = strings.concat("WW_NULL_CAPTURE=", capture); + + // Raw test compilation has the same private-output rule. Running forms + // preserve ordinary success, assertion, and signal outcomes. + let compileav: []str = [driver(stages[si]), "test", "-c", "-o", + "/dev/null", runtimepath("success_test.ww")]; + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + rewritefile(capture, ""); + runcommandenvdir(root, strings.concat("null-test-c-", tags[si]), + compileav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stdout.len == 0 && out.stderr.len == 0); + assert(occurrences(readfile(ltrace), "\n") == 1); + discardedlinkcleaned(lastout); + let testbytes: str = readfile(capture); + if (si == 0) { testcompileref = strings.dup(testbytes); } + else { assert(same(testcompileref, testbytes)); }; + + let runtargets: []str = ["success_test.ww", "fail_test.ww", + "signal_test.ww"]; + let exitcodes: []i32 = [0, 1, 1]; + let needles: []str = ["alpha_pass ... ok\n", + "assertion_failure ... FAIL (exit 1)\n", + "signal_is_not_exit ... FAIL (signal 15)\n"]; + let ri: i32 = 0; + for (ri < runtargets.len) { + let testav: []str = [driver(stages[si]), "test"]; + append(testav, "-o"); append(testav, "/dev/null"); + append(testav, runtimepath(runtargets[ri])); + rewritefile(ctrace, ""); rewritefile(atrace, ""); + rewritefile(ltrace, ""); rewritefile(lastout, ""); + runcommandenvdir(root, strings.concat("null-test-run-", tags[si], + "-", boundarypkgname(ri)), testav, env, root, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, exitcodes[ri]); + assert(has(out.stdout, needles[ri])); + assert(occurrences(readfile(ltrace), "\n") == 1); + discardedlinkcleaned(lastout); + if (si == 0) { + testoutrefs[ri] = strings.dup(out.stdout); + testerrrefs[ri] = strings.dup(out.stderr); + } else { + assert(same(testoutrefs[ri], out.stdout)); + assert(same(testerrrefs[ri], out.stderr)); + }; + ri += 1; + }; + assert(!os.exists("/dev/null.new")); + assert(!os.exists("/dev/null.sepwork")); + si += 1; + }; + + // Persisted semantic artifacts remain byte-identical across stages. Output + // disposition is request metadata and does not enter these identities. + let actions: []str = ["base", "alpha"]; + let ai: i32 = 0; + for (ai < actions.len) { + let xi: i32 = 0; + for (xi < suffixes.len) { + assert(same(readfile(strings.concat(root, "/work-c/", + actions[ai], suffixes[xi])), readfile(strings.concat(root, + "/work-ww/", actions[ai], suffixes[xi])))); + xi += 1; + }; + ai += 1; + }; + + // Independent Cstage and WWstage discard requests can execute together. + // Their private products and persistent generations remain isolated. + let pcwork: str = strings.concat(root, "/parallel-c-work"); + let pwwork: str = strings.concat(root, "/parallel-ww-work"); + mkdirall(pcwork); mkdirall(pwwork); + let pcav: []str = [driver("ww"), "build", "-w", pcwork, + "-I", source, "-o", "/dev/null", alpha, beta, library]; + let pwav: []str = [driver("ww_ww"), "build", "-w", pwwork, + "-I", source, "-o", "/dev/null", alpha, beta, library]; + let pc: exec.command; + pc.path = pcav[0]; pc.argv = pcav; pc.env = os.getenvs(); pc.dir = root; + pc.stdoutpath = strings.concat(root, "/parallel-c.stdout"); + pc.stderrpath = strings.concat(root, "/parallel-c.stderr"); + pc.deadline = time.add(time.now(time.clock.monotonic), + (120i64 * (time.second: i64)): time.duration); + pc.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pw: exec.command; + pw.path = pwav[0]; pw.argv = pwav; pw.env = os.getenvs(); pw.dir = root; + pw.stdoutpath = strings.concat(root, "/parallel-ww.stdout"); + pw.stderrpath = strings.concat(root, "/parallel-ww.stderr"); + pw.deadline = time.add(time.now(time.clock.monotonic), + (120i64 * (time.second: i64)): time.duration); + pw.grace = (100i64 * (time.millisecond: i64)): time.duration; + let pcp: exec.process; + let pwp: exec.process; + exec.start(&pcp, &pc); exec.start(&pwp, &pw); + let pcdone: bool = false; + let pwdone: bool = false; + for (!pcdone || !pwdone) { + if (!pcdone) { pcdone = exec.poll(&pcp); }; + if (!pwdone) { pwdone = exec.poll(&pwp); }; + if (!pcdone || !pwdone) { + time.sleep(time.millisecond, time.clock.monotonic); + }; + }; + assert(pcp.result.errno == 0 && pcp.result.cleanuperrno == 0); + assert(pwp.result.errno == 0 && pwp.result.cleanuperrno == 0); + assert(pcp.result.termination == exec.termination.EXIT + && pcp.result.code == 0); + assert(pwp.result.termination == exec.termination.EXIT + && pwp.result.code == 0); + assert(readfile(pc.stdoutpath).len == 0 && readfile(pc.stderrpath).len == 0); + assert(readfile(pw.stdoutpath).len == 0 && readfile(pw.stderrpath).len == 0); + assert(!directoryhasnew(pcwork) && !directoryhasnew(pwwork)); + ai = 0; + let parallel: []str = ["base", "alpha", "beta", "library"]; + for (ai < parallel.len) { + let xi: i32 = 0; + for (xi < suffixes.len) { + assert(same(readfile(strings.concat(pcwork, "/", parallel[ai], + suffixes[xi])), readfile(strings.concat(pwwork, "/", + parallel[ai], suffixes[xi])))); + xi += 1; + }; + ai += 1; + }; + + let nullafter: os.filestat; + match (os.stat(&nullafter, "/dev/null")) { + case void => void; + case let e: os.oserror => abort("cannot restat /dev/null"); + }; + assert((nullafter.mode as u32) == (nullbefore.mode as u32)); + assert(nullafter.inode == nullbefore.inode); + assert(nullafter.sz == nullbefore.sz); + assert(!os.exists("/dev/null.new")); + assert(!os.exists("/dev/null.sepwork")); + clean(root); +};