From 351f4a25bc7462a7e9810f35f7aa37acff02f1b1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 14 Aug 2026 05:12:45 +0900 Subject: [PATCH] ww: invalidate partial package commits --- cmd/ww/main.c | 51 +++++++++-- docs/build-system.md | 12 ++- docs/test-system-v2.md | 7 +- selfhost/cmd/ww/main.ww | 64 ++++++++++++-- test/package/package_test.ww | 165 ++++++++++++++++++++++++++++++++++- 5 files changed, 282 insertions(+), 17 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 22e4ac4e..95405dc0 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -4220,6 +4220,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, return 1; } int any_failed = 0; + int commit_open = 0; + int commit_integrity_failed = 0; for (int i = 0; i < nproducts; i++) if (g->pkg[products[i].root].failed) any_failed = 1; for (int oi = 0; oi < norder; oi++) { @@ -4255,6 +4257,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *ca = warm ? anew : apath; if (sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew) < 0) { + if (warm && unlink(unitf) != 0 && errno != ENOENT) + commit_integrity_failed = 1; g->pkg[pi].failed = 1; any_failed = 1; continue; @@ -4262,6 +4266,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (sep_compose_unit(g, pi, cu) < 0) { (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); + if (warm && unlink(unitf) != 0 && errno != ENOENT) + commit_integrity_failed = 1; g->pkg[pi].failed = 1; any_failed = 1; continue; @@ -4271,6 +4277,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (g->pkg[g->pkg[pi].deps[k]].export_changed) deps_changed = 1; if (warm && !stale_all && !deps_changed + && !commit_integrity_failed && file_equal(unitnew, unitf) && file_is_reg(asmf) && file_is_reg(wwi) @@ -4284,6 +4291,20 @@ build_one_sep_impl(const char *src, int entry_is_dir, } continue; } + /* Once an action is known not to be reusable, its old unit must no + * longer vouch for artifacts if any later producer or commit step + * fails. This is especially important when a dependency already + * committed a changed export during the same request. */ + if (warm && unlink(unitf) != 0 && errno != ENOENT) { + fprintf(stderr, "ww: cannot invalidate package unit %s\n", + unitf); + (void)sep_discard_action_staging(warm, unitnew, wwinew, + asmnew, objnew, anew); + g->pkg[pi].failed = 1; + any_failed = 1; + commit_integrity_failed = 1; + continue; + } int nmaps = 0; for (int k = 0; k < g->pkg[pi].bindings.n; k++) { if (!sep_binding_first_map(g, &g->pkg[pi].bindings, k)) @@ -4418,8 +4439,22 @@ build_one_sep_impl(const char *src, int entry_is_dir, } } /* Commit order: artifacts before the unit that vouches for - * them, unit strictly last. */ + * them, unit strictly last. Remove the workdir identity before the + * first artifact rename; failure to do so is a pre-commit rejection + * that leaves all committed artifacts untouched. */ if (warm) { + if (!commit_open) { + if (unlink(stampf) != 0 && errno != ENOENT) { + fprintf(stderr, + "ww: cannot invalidate package workdir\n"); + g->pkg[pi].failed = 1; + any_failed = 1; + (void)sep_discard_action_staging(warm, unitnew, + wwinew, asmnew, objnew, anew); + continue; + } + commit_open = 1; + } if (rename(wwinew, wwi) != 0 || rename(asmnew, asmf) != 0 || (!emit_asm && rename(objnew, obj) != 0) @@ -4429,6 +4464,12 @@ build_one_sep_impl(const char *src, int entry_is_dir, g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); g->pkg[pi].failed = 1; any_failed = 1; + /* A failed rename sequence may already have replaced the + * interface or another artifact. The stamp is already absent; + * also invalidate every unit voucher and force later actions in + * this invocation through their producers. */ + commit_integrity_failed = 1; + (void)invalidate_workdir_units(scratch); (void)sep_discard_action_staging(warm, unitnew, wwinew, asmnew, objnew, anew); continue; @@ -4437,9 +4478,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, } /* Stale passes removed every old unit voucher before compiling. Current * successful units are therefore safe to vouch for even when a sibling - * root failed; a killed pass leaves the old identity and forces another - * invalidating pass, never false reuse. */ - if (warm) { + * compiler rejects. A partial artifact commit invalidates all vouchers + * and suppresses the workdir identity so the next pass starts stale. */ + if (warm && !commit_integrity_failed) { if (!file_equal(toolw, self_path) && copy_file_atomic(self_path, toolw) != 0) { fprintf(stderr, "ww: cannot record %s\n", toolw); @@ -4455,7 +4496,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, fprintf(stderr, "ww: cannot record %s\n", toola); free(order); return 1; } - if (!stampok) { + if (!stampok || commit_open) { char stampnew[PATH_MAX]; int sn = snprintf(stampnew, sizeof stampnew, "%s.new", stampf); if (sn < 0 || (size_t)sn >= sizeof stampnew) { diff --git a/docs/build-system.md b/docs/build-system.md index 9f3a9f95..74b1100b 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -4558,7 +4558,17 @@ loader-owned deterministic error before producers. Compiler-owned scope or use errors may invoke the compiler, but the driver removes that action's staged `.new` unit/export/assembly/object/archive files instead of renaming them. No completion/status marker is written, and no publication occurs after a failed -compile. +compile. If an artifact rename itself fails after an earlier rename has +published part of a new generation, the driver removes every committed unit +voucher as well as the remaining staged files. Before the first artifact +rename, it must successfully remove the workdir identity stamp; failure is a +pre-commit rejection that leaves the prior artifact generation untouched. The +stamp remains absent after any partial commit, and later actions in that +invocation cannot take the freshness shortcut. Each non-reusable action also +loses its old unit voucher before its producers run. The next request must +therefore rebuild the incomplete action set and reconsider direct importers; a +partly replaced `.wwi` or a failed importer can never make an old importer +voucher look fresh. Package kind follows the declaration. `package main`, not a path component, marks a command. A path ending in `main` remains importable when it declares a diff --git a/docs/test-system-v2.md b/docs/test-system-v2.md index 42cf4b8a..efa12da8 100644 --- a/docs/test-system-v2.md +++ b/docs/test-system-v2.md @@ -236,7 +236,12 @@ union; command and production/internal/external/generated-main variants retain canonical action ownership; vendor expansion changes identity but not the declared qualifier; compiler argv contains only direct `.wwi` inputs; and its named rejected actions leave neither committed nor staged action artifacts or a -published binary. Together with the existing directory, recursive, vendor, +published binary. It also forces a staged multi-artifact commit to fail after +the interface rename, then proves that both stages invalidate old unit vouchers +and reconsider the importer rather than accepting a mixed warm generation. A +separate injected stamp-removal failure proves the pre-commit gate leaves all +previously committed artifacts byte-identical. +Together with the existing directory, recursive, vendor, exact-argv, command, and persistent-workdir observers, the package suite proves archive-only link argv and exact warm/rejection-state behavior without duplicating those broader mechanisms in this observer. diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index e4f4360a..ac2155f6 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -5093,6 +5093,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, return 1; }; let anyfailed: bool = false; + let commitopen: bool = false; + let commitintegrityfailed: bool = false; producti = 0; for (producti < nproducts) { if (g.pkg[products[producti].root].failed) { anyfailed = true; }; @@ -5139,6 +5141,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; if (sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew) < 0) { + if (warm) { + let unitrr: i32 = os.remove(pathstr(unitf)); + if (unitrr != 0 && unitrr != -2) { + commitintegrityfailed = true; + }; + }; g.pkg[pi].failed = true; anyfailed = true; oi += 1; @@ -5147,6 +5155,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (sepcomposeunit(g, pi, cu) < 0) { sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); + if (warm) { + let unitrr: i32 = os.remove(pathstr(unitf)); + if (unitrr != 0 && unitrr != -2) { + commitintegrityfailed = true; + }; + }; g.pkg[pi].failed = true; anyfailed = true; oi += 1; @@ -5162,7 +5176,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; let fresh: bool = false; if (warm) { - if (!staleall && !depschanged) { + if (!staleall && !depschanged && !commitintegrityfailed) { fresh = fileequal(unitnew, unitf); if (fresh) { fresh = fileisreg(asmf); }; if (fresh) { fresh = fileisreg(wwi); }; @@ -5190,6 +5204,22 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, oi += 1; continue; }; + // Once an action is not reusable, its old unit must not vouch for + // artifacts after a later producer or commit failure. This also makes a + // failed importer retry after a dependency committed a changed export. + if (warm) { + let unitrr: i32 = os.remove(pathstr(unitf)); + if (unitrr != 0 && unitrr != -2) { + cerrpath("ww: cannot invalidate package unit ", unitf, "\n"); + sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, + objnew, anew); + g.pkg[pi].failed = true; + anyfailed = true; + commitintegrityfailed = true; + oi += 1; + continue; + }; + }; { let rawtest: bool = (istest != 0) && g.pkg[pi].root && g.pkg[pi].isdir == 0; @@ -5378,9 +5408,23 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, continue; }; }; - // Commit order: artifacts before the unit that vouches for - // them, unit strictly last. + // Commit order: artifacts before the unit that vouches for them, unit + // strictly last. Remove the workdir identity before the first artifact + // rename; failure is a pre-commit rejection with old artifacts intact. if (warm) { + if (!commitopen) { + let stamprr: i32 = os.remove(pathstr(stampf)); + if (stamprr != 0 && stamprr != -2) { + cerr("ww: cannot invalidate package workdir\n"); + g.pkg[pi].failed = true; + anyfailed = true; + sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, + objnew, anew); + oi += 1; + continue; + }; + commitopen = true; + }; let bad: bool = false; if (os.rename(pathstr(wwinew), pathstr(wwi)) != 0) { bad = true; @@ -5418,6 +5462,11 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; g.pkg[pi].failed = true; anyfailed = true; + // A failed rename sequence may already have replaced the + // interface or another artifact. The stamp is already absent; + // invalidate all vouchers and force later actions through tools. + commitintegrityfailed = true; + invalidateworkdirunits(scratch); sepdiscardactionstaging(warm, unitnew, wwinew, asmnew, objnew, anew); oi += 1; @@ -5427,9 +5476,10 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, oi += 1; }; // A stale pass removed every old unit voucher before compiling. Current - // successful units remain safe to vouch for when a sibling root fails; - // a killed pass retains the old identity and invalidates again next time. - if (warm) { + // successful units remain safe when a sibling compiler rejects. A partial + // artifact commit invalidates all vouchers and suppresses the workdir + // identity so the next pass starts stale. + if (warm && !commitintegrityfailed) { let sametool: bool = fileequal(toolw, selfpath); if (sepfatalallocation) { return 1; }; if (!sametool) { @@ -5456,7 +5506,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; }; }; - if (!stampok) { + if (!stampok || commitopen) { if (writestampatomic(stampf, stampwant) != 0) { cerrpath("ww: cannot record ", stampf, "\n"); return 1; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index caeca83d..7ae4e6d0 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -7879,6 +7879,8 @@ fn runtimepath(relative: str) str = { let scopetwo: str = strings.concat(source, "/scope/two"); let scoped: str = strings.concat(source, "/cmd/scoped"); let repeated: str = strings.concat(source, "/cmd/repeated"); + let commitcodec: str = strings.concat(source, "/commit/codec"); + let commitapp: str = strings.concat(source, "/cmd/commit"); let leafbad: str = strings.concat(source, "/cmd/leafbad"); let leak: str = strings.concat(source, "/cmd/leak"); let duplicate: str = strings.concat(source, "/cmd/duplicate"); @@ -7895,7 +7897,8 @@ fn runtimepath(relative: str) str = { "/vend/vendor/short/codec"); let vendorclient: str = strings.concat(source, "/vend/client"); let dirs: []str = [source, tools, codec, bridge, direct, app, scopeone, - scopetwo, scoped, repeated, leafbad, leak, duplicate, collision, + scopetwo, scoped, repeated, commitcodec, commitapp, leafbad, leak, + duplicate, collision, crosscollision, conflict, leafmain, program, leafmainuser, programuser, testcodec, testhelper, vendored, vendorclient]; let di: i32 = 0; @@ -7909,6 +7912,12 @@ fn runtimepath(relative: str) str = { "export fn value() i32 = { return 42; };\n"); let codecfile: str = strings.concat(codec, "/codec.ww"); writefile(codecfile, wirebody); + let commitcodecfile: str = strings.concat(commitcodec, "/codec.ww"); + let commitappfile: str = strings.concat(commitapp, "/main.ww"); + writefile(commitcodecfile, wirebody); + writefile(commitappfile, strings.concat( + "package main;\nimport commit.codec;\n", + "fn main() i32 = { return wire.value(); };\n")); writefile(strings.concat(bridge, "/bridge.ww"), strings.concat( "package bridge;\nimport acme.codec;\n", // WW retains its direct-import bare-declaration convenience. It is @@ -8014,7 +8023,15 @@ fn runtimepath(relative: str) str = { "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", "\"$WW_NAME_COMPILER_TRACE\"; done\n", "printf '\\n' >> \"$WW_NAME_COMPILER_TRACE\"\n", - "exec \"$WW_NAME_REAL_COMPILER\" \"$@\"\n")); + "\"$WW_NAME_REAL_COMPILER\" \"$@\"\n", + "status=$?\n", + "if [ \"$status\" -eq 0 ] && ", + "[ -n \"$WW_COMMIT_BLOCK_STAMP\" ]; then\n", + " mv -- \"$WW_COMMIT_BLOCK_STAMP\" ", + "\"$WW_COMMIT_BLOCK_STAMP.saved\" || exit 97\n", + " mkdir -- \"$WW_COMMIT_BLOCK_STAMP\" || exit 98\n", + "fi\n", + "exit \"$status\"\n")); let stages: []str = ["ww", "ww_ww"]; let compilers: []str = ["w6c", "w6c_ww"]; @@ -8032,6 +8049,16 @@ fn runtimepath(relative: str) str = { strings.concat(root, "/ww-scoped")]; let repeatedbins: []str = [strings.concat(root, "/c-repeated"), strings.concat(root, "/ww-repeated")]; + let commitworks: []str = [strings.concat(root, "/c-commit-work"), + strings.concat(root, "/ww-commit-work")]; + let commitcoldbins: []str = [strings.concat(root, "/c-commit-cold"), + strings.concat(root, "/ww-commit-cold")]; + let commitblockbins: []str = [strings.concat(root, "/c-commit-block"), + strings.concat(root, "/ww-commit-block")]; + let commitrejectbins: []str = [strings.concat(root, "/c-commit-reject"), + strings.concat(root, "/ww-commit-reject")]; + let commitfixedbins: []str = [strings.concat(root, "/c-commit-fixed"), + strings.concat(root, "/ww-commit-fixed")]; let leafmainbins: []str = [strings.concat(root, "/c-leafmain"), strings.concat(root, "/ww-leafmain")]; let vendorbins: []str = [strings.concat(root, "/c-vendor"), @@ -8053,13 +8080,22 @@ fn runtimepath(relative: str) str = { let scopedtraceref: str = ""; let repeatedtraceref: str = ""; let changedtraceref: str = ""; + let commitfailref: str = ""; + let commitpreflightref: str = ""; + let commitbindingref: str = ""; + let commitbinref: str = ""; let initialwwiref: str = ""; let testoutref: str = ""; let baseenv: []str = os.getenvs(); let si: i32 = 0; for (si < stages.len) { rewritefile(codecfile, wirebody); + rewritefile(commitcodecfile, wirebody); + rewritefile(commitappfile, strings.concat( + "package main;\nimport commit.codec;\n", + "fn main() i32 = { return wire.value(); };\n")); assert(os.mkdir(works[si], 448i32) == 0); + assert(os.mkdir(commitworks[si], 448i32) == 0); writefile(traces[si], ""); let env: []str = alloc([], (baseenv.len + 7): u64)!; let ei: i32 = 0; @@ -8071,7 +8107,9 @@ fn runtimepath(relative: str) str = { && !strings.hasprefix(baseenv[ei], "WW_NAME_COMPILER_TRACE=") && !strings.hasprefix(baseenv[ei], - "WW_NAME_REAL_COMPILER=")) { + "WW_NAME_REAL_COMPILER=") + && !strings.hasprefix(baseenv[ei], + "WW_COMMIT_BLOCK_STAMP=")) { append(env, baseenv[ei]); }; ei += 1; @@ -8122,6 +8160,127 @@ fn runtimepath(relative: str) str = { (30i64 * (time.second: i64)): time.duration, &out); expectexit(&out, 42); + // A failed multi-file artifact rename may have published a new + // interface before a later rename fails. It invalidates every old unit + // voucher and the workdir identity, so the next request must reconsider + // an importer even when the regenerated dependency export now compares + // equal to that partially published interface. + let commitcoldav: []str = [driver(stages[si]), "build", "-w", + commitworks[si], "-I", source, "-o", commitcoldbins[si], commitapp]; + runcommandenv(root, strings.concat("name-commit-cold-", stages[si]), + commitcoldav, env, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let commitrun: []str = [commitcoldbins[si]]; + runcommand(root, strings.concat("name-commit-cold-run-", stages[si]), + commitrun, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 42); + rewritefile(commitcodecfile, cablebody); + let commitstamp: str = strings.concat(commitworks[si], + "/.wwtool.stamp"); + let savedstamp: str = strings.concat(commitstamp, ".saved"); + let oldcommitwwi: str = readfile(strings.concat(commitworks[si], + "/commit.codec.wwi")); + let oldcommits: str = readfile(strings.concat(commitworks[si], + "/commit.codec.s")); + let oldcommito: str = readfile(strings.concat(commitworks[si], + "/commit.codec.o")); + let oldcommita: str = readfile(strings.concat(commitworks[si], + "/commit.codec.a")); + let blockenv: []str = alloc([], (env.len + 1): u64)!; + let bei: i32 = 0; + for (bei < env.len) { append(blockenv, env[bei]); bei += 1; }; + append(blockenv, strings.concat("WW_COMMIT_BLOCK_STAMP=", commitstamp)); + let commitblockav: []str = [driver(stages[si]), "build", "-w", + commitworks[si], "-I", source, "-o", commitblockbins[si], + commitapp]; + runcommandenv(root, strings.concat("name-commit-preflight-", stages[si]), + commitblockav, blockenv, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "ww: cannot invalidate package workdir\n")); + if (si == 0) { commitpreflightref = strings.dup(out.stderr); } + else { assert(same(commitpreflightref, out.stderr)); }; + assert(same(oldcommitwwi, readfile(strings.concat(commitworks[si], + "/commit.codec.wwi")))); + assert(same(oldcommits, readfile(strings.concat(commitworks[si], + "/commit.codec.s")))); + assert(same(oldcommito, readfile(strings.concat(commitworks[si], + "/commit.codec.o")))); + assert(same(oldcommita, readfile(strings.concat(commitworks[si], + "/commit.codec.a")))); + assert(!os.exists(strings.concat(commitworks[si], + "/commit.codec.unit.ww"))); + assert(!os.exists(commitblockbins[si])); + assert(os.rmdir(commitstamp) == 0); + assert(os.rename(savedstamp, commitstamp) == 0); + + // With the stamp preflight restored, force a later artifact rename to + // fail after the changed interface has already been published. + let blockedasm: str = strings.concat(commitworks[si], + "/commit.codec.s"); + assert(os.remove(blockedasm) == 0); + assert(os.mkdir(blockedasm, 448i32) == 0); + let commitrejectav: []str = [driver(stages[si]), "build", "-w", + commitworks[si], "-I", source, "-o", commitrejectbins[si], + commitapp]; + runcommandenv(root, strings.concat("name-commit-partial-", stages[si]), + commitrejectav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "ww: cannot commit commit.codec\n")); + if (si == 0) { commitfailref = strings.dup(out.stderr); } + else { assert(same(commitfailref, out.stderr)); }; + assert(has(readfile(strings.concat(commitworks[si], + "/commit.codec.wwi")), + "//ww:module commit.codec\npackage cable;\n")); + assert(!os.exists(strings.concat(commitworks[si], + "/commit.codec.unit.ww"))); + assert(!os.exists(strings.concat(commitworks[si], + "/cmd.commit.unit.ww"))); + assert(!os.exists(strings.concat(commitworks[si], "/.wwtool.stamp"))); + let partialsuffixes: []str = [".unit.new", ".wwi.new", ".s.new", + ".o.new", ".a.new"]; + let psi: i32 = 0; + for (psi < partialsuffixes.len) { + assert(!os.exists(strings.concat(commitworks[si], + "/commit.codec", partialsuffixes[psi]))); + psi += 1; + }; + assert(!os.exists(commitrejectbins[si])); + assert(os.rmdir(blockedasm) == 0); + rewritefile(traces[si], ""); + runcommandenv(root, strings.concat("name-commit-reconsider-", stages[si]), + commitrejectav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "undefined: wire")); + let ncommitbinding: str = normalizedtrace(out.stderr, + strings.concat(commitworks[si], "/"), commitrejectbins[si]); + if (si == 0) { commitbindingref = strings.dup(ncommitbinding); } + else { assert(same(commitbindingref, ncommitbinding)); }; + assert(has(readfile(traces[si]), "/cmd.commit.unit.new")); + assert(os.exists(strings.concat(commitworks[si], + "/commit.codec.unit.ww"))); + assert(!os.exists(strings.concat(commitworks[si], + "/cmd.commit.unit.ww"))); + assert(!os.exists(commitrejectbins[si])); + rewritefile(commitappfile, strings.concat( + "package main;\nimport commit.codec;\n", + "fn main() i32 = { return cable.value(); };\n")); + let commitfixedav: []str = [driver(stages[si]), "build", "-w", + commitworks[si], "-I", source, "-o", commitfixedbins[si], commitapp]; + runcommandenv(root, strings.concat("name-commit-fixed-", stages[si]), + commitfixedav, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + let commitfixedrun: []str = [commitfixedbins[si]]; + runcommand(root, strings.concat("name-commit-fixed-run-", stages[si]), + commitfixedrun, (30i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 42); + let commitbinbytes: str = readfile(commitfixedbins[si]); + if (si == 0) { commitbinref = strings.dup(commitbinbytes); } + else { assert(same(commitbinref, commitbinbytes)); }; + // The same declared qualifier belongs to each importing source file, // not to the package-wide namespace. rewritefile(traces[si], "");