diff --git a/Makefile b/Makefile index b83ad644..6ea54164 100644 --- a/Makefile +++ b/Makefile @@ -851,11 +851,11 @@ clean: # (it takes no -o/-I and writes asm to stdout), so the chainable sep # frontend w6c is the bootstrapped target (test 994 pins w6c == # wwdump -c). -# - The wwstage DRIVER (`ww_ww`) is NOT yet in this loop: it cannot -# swap its frontend (it hardcodes /w6c_ww and honors no -# WW_W6C override). Closing that — so `ww_ww` itself carries the -# glue — is task #66. Until then cstage `ww` orchestrates and this -# bootstrap does NOT claim a fully wwstage-driven self-host. +# - The wwstage DRIVER (`ww_ww`) can now swap all three tools through +# exact-path WW_W6C/WW_W6A/WW_W6L overrides. This loop deliberately +# still keeps cstage `ww` as its fixed orchestrator, so the driver is +# outside the compared artifacts and this target does NOT yet claim a +# fully wwstage-driven self-host. # # Why a fourth stage? Stage 3 proves stage-1 w6c_ww and ww2 (which differ # in how they were built) emit the same machine code. Stage 4 proves ww3 is @@ -940,7 +940,7 @@ nocc: @rm -rf $(NOCC_OUT) @mkdir -p $(NOCC_BIN) $(NOCC_LIB) $(NOCC_OBJ) @# Stage 0: drop bootstrap binaries under both natural names and - @# the _ww-suffixed names the driver expects when shelling out. + @# the _ww-suffixed names used by the driver's default sibling lookup. @for f in ww w6c w6a w6l; do \ cp $(STAGE0)/$$f $(NOCC_BIN)/$$f; \ cp $(STAGE0)/$$f $(NOCC_BIN)/$${f}_ww; \ diff --git a/cmd/w6l/main.c b/cmd/w6l/main.c index ad964604..343e5a12 100644 --- a/cmd/w6l/main.c +++ b/cmd/w6l/main.c @@ -9,6 +9,7 @@ #include #include #include +#include /* `path` is acceptable iff it's either an archive ("!\n") or * an ELF file ("\x7fELF"). Distros often ship lib.so as a GNU @@ -128,10 +129,9 @@ main(int argc, char **argv) } int rc = l_emit_elf(&l, f, base, base + 0x1000 + entry->val); fclose(f); - if (rc == 0) { - char cmd[1024]; - snprintf(cmd, sizeof cmd, "chmod +x %s", out); - (void)system(cmd); + if (rc == 0 && chmod(out, 0755) != 0) { + fprintf(stderr, "w6l: cannot make %s executable\n", out); + rc = 1; } free(inputs); return rc; diff --git a/docs/build-system.md b/docs/build-system.md index a0851602..9d819e58 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -2651,11 +2651,13 @@ Other accidental constraints include fixed 256/1024-byte name/path buffers. The compiler parser silently truncates dotted full imports beyond 255 bytes, while the C driver scanner can stop advancing and hang on an import identifier at that limit; the dynamically sized WWstage scanner differs. There is no regression -test for this stage divergence. Other defects include unquoted Cstage `system()` command -construction, and dependence on `/bin/sh`. Cstage honors `WW_W6C`, `WW_W6A`, -`WW_W6L`, `WW_LIB`, and source-library fallbacks; the self-hosted driver instead -hardcodes sibling tools and relative libraries. The two implementations are -therefore similar algorithms, not one protocol implementation. +test for this stage divergence. Compiler, assembler, and linker launches now use +structured argument vectors in both stages. Both drivers honor exact executable +paths in `WW_W6C`, `WW_W6A`, and `WW_W6L` and otherwise select their +stage-specific sibling tools. Cstage additionally honors `WW_LIB` and +source-library fallbacks; the self-hosted driver still uses relative source and +runtime libraries. The two implementations remain parallel production +algorithms rather than one protocol implementation. Build and test disagree about source symlinks. External package tests are built from a generated single-file root plus `-I`; an external import of a multi-file @@ -2689,8 +2691,8 @@ The observed invalidation rules are: content is not rehashed. Hidden or incompletely modeled inputs include `CC`, `AR`, `PATH`, `ccache`, Make -flags, shell quoting/behavior, compiler built-ins, assembler/linker defaults, -inferred `argv[0]` library locations, current working directory, `chmod`, runtime +flags, compiler built-ins, assembler/linker defaults, +inferred `argv[0]` library locations, current working directory, file mode, runtime archive, linker binary, native-library resolution, host libc/CRT/loader, SDK, CPU, target, and environment. Make does not invalidate existing C objects when the host compiler or C flags change. Workdir reuse also omits the driver binary/ @@ -2736,9 +2738,10 @@ holds for measured WW artifacts, not for the complete build. Cstage and WWstage drivers building the same eight-package graph produced a byte-identical final executable and all 38 non-tool artifacts, but took 0.138 s -and 0.356 s respectively. Under `env -i PATH=/nonexistent`, Cstage returned -success after ambient `chmod` failed and left mode 0644; WWstage returned success -with mode 0755. This exposes implementation asymmetry and an undeclared host tool. +and 0.356 s respectively. The audit also found that Cstage formerly returned +success after an ambient `chmod` lookup failed and left mode 0644, while WWstage +created mode 0755 directly. Cstage now calls `chmod(2)` on the exact output path +and reports failure, removing that host-tool and path-splitting asymmetry. ### 11.5 What survives and what is deleted @@ -2985,6 +2988,42 @@ boundary in with compiler and linker import configurations emitted separately in [`cmd/go/internal/work/exec.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go). +### 11.8 Implemented exact package-tool invocation slice + +The local package builder now launches the compiler, assembler, and linker as +an executable plus an argument vector in both Cstage and WWstage. No package +source path, work-directory artifact path, output path, test-support qualifier, +or link-closure member is flattened into a shell command. Paths containing +spaces therefore retain one argument boundary from the package coordinator +through compilation, assembly, and final executable linking. + +`WW_W6C`, `WW_W6A`, and `WW_W6L` each name one exact executable path. They are +not shell fragments and are not searched through `PATH`. With no override, +Cstage keeps its `w6c`/`w6a`/`w6l` siblings and WWstage keeps its +`w6c_ww`/`w6a_ww`/`w6l_ww` siblings. The coordinator preserves these variables +when it starts the one command-scoped package build, so the same contract covers +ordinary directory builds, same-package tests, external tests, recursive test +requests, and persistent-workdir tool identity. A failed overridden compiler or +assembler remains attributed to its owning package in both stages. The Cstage +linker also sets executable mode with `chmod(2)` on the exact output path rather +than invoking an ambient command. + +This changes only process invocation and publication. Source imports still own +the graph, each directory is still one production package, compiler actions +still consume direct dependency export data through `.unit.ww`, and links still +receive the complete per-root `.a` closure. Repository-native coverage wraps +all three real stage tools at executable paths containing spaces, records every +argument boundary, inspects `.unit.ww`, `.wwi`, `.a`, and root object placement, +runs the published test binary, compares Cstage/WWstage artifacts and traces, +and injects a compiler failure to compare package attribution. + +Go 1.26.5 keeps the same responsibility boundary: its work executor passes the +selected compiler or linker tool and a constructed argument slice to the +builder, while package loading and action construction remain separate +([`cmd/go/internal/work/exec.go`](https://go.googlesource.com/go/+/refs/tags/go1.26.5/src/cmd/go/internal/work/exec.go)). +WW adopts that exact-tool boundary without adding a command schema, generalized +action graph, scheduler, manifest, cache protocol, or package-manager behavior. + ## 12. Candidate architectures and hard-gate decision Five candidates were developed as coherent systems, not as feature bins. diff --git a/test/package/package_test.ww b/test/package/package_test.ww index fd9e85c6..20b43588 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -1110,10 +1110,9 @@ fn workescape(s: str) str = { append(traceenv, strings.concat("WW_PACKAGE_LINKER_TRACE=", linkertrace)); append(traceenv, strings.concat("WW_PACKAGE_W6C=", driver("w6c"))); append(traceenv, strings.concat("WW_PACKAGE_W6L=", driver("w6l"))); - // The WW driver intentionally takes sibling tools rather than WW_W6C - // overrides. Run an exact copied driver beside narrow tracing wrappers so - // its real compiler/linker actions are observable without changing that - // production contract. + // Exercise default sibling discovery independently of the exact-path + // override contract covered below: the copied driver sits beside narrow + // tracing wrappers for its real compiler and linker actions. let wwtracebin: str = strings.concat(repo(), "/out/package-trace-", workescape(root)); assert(os.mkdir(wwtracebin, 448i32) == 0); @@ -2143,7 +2142,8 @@ fn workescape(s: str) str = { let trace: str = strings.concat(root, "/compiler.trace"); let wrapper: str = strings.concat(root, "/trace-w6c.sh"); writefile(trace, ""); - writefile(wrapper, strings.concat( + writeexecutable(wrapper, strings.concat( + "#!/bin/sh\n", "printf '%s\\n' \"$*\" >> \"$WW_PACKAGE_TRACE\"\n", "exec \"$WW_PACKAGE_W6C\" \"$@\"\n")); let baseenv: []str = os.getenvs(); @@ -2157,7 +2157,7 @@ fn workescape(s: str) str = { }; ei += 1; }; - append(env, strings.concat("WW_W6C=/bin/sh ", wrapper)); + append(env, strings.concat("WW_W6C=", wrapper)); append(env, strings.concat("WW_PACKAGE_TRACE=", trace)); append(env, strings.concat("WW_PACKAGE_W6C=", driver("w6c"))); let bin: str = strings.concat(root, "/diamond.test"); @@ -2187,6 +2187,245 @@ fn workescape(s: str) str = { clean(root); }; +@test fn exact_package_tool_argv_stage_parity() void = { + let root: str = fresh(); + let source: str = strings.concat(root, "/source tree"); + let leaf: str = strings.concat(source, "/leaf"); + let dep: str = strings.concat(source, "/dep"); + let target: str = strings.concat(source, "/target"); + let tools: str = strings.concat(root, "/tool wrappers"); + assert(os.mkdir(source, 448i32) == 0); + assert(os.mkdir(leaf, 448i32) == 0); + assert(os.mkdir(dep, 448i32) == 0); + assert(os.mkdir(target, 448i32) == 0); + assert(os.mkdir(tools, 448i32) == 0); + writefile(strings.concat(leaf, "/leaf.ww"), strings.concat( + "package leaf;\n", + "export fn value() i32 = { return 40; };\n")); + writefile(strings.concat(dep, "/dep.ww"), strings.concat( + "package dep;\nimport leaf;\n", + "export fn value() i32 = { return leaf.value() + 2; };\n")); + writefile(strings.concat(target, "/target.ww"), strings.concat( + "package target;\nimport dep;\n", + "fn value() i32 = { return dep.value(); };\n")); + writefile(strings.concat(target, "/target_test.ww"), strings.concat( + "package target;\n", + "@test fn exact_argv() void = { assert(value() == 42); };\n")); + + let compilerwrapper: str = strings.concat(tools, "/w6c wrapper.sh"); + let assemblerwrapper: str = strings.concat(tools, "/w6a wrapper.sh"); + let linkerwrapper: str = strings.concat(tools, "/w6l wrapper.sh"); + let failurewrapper: str = strings.concat(tools, "/failing w6c wrapper.sh"); + writeexecutable(compilerwrapper, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ARGV_COMPILER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ARGV_COMPILER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ARGV_COMPILER_TRACE\"\n", + "exec \"$WW_ARGV_REAL_COMPILER\" \"$@\"\n")); + writeexecutable(assemblerwrapper, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ARGV_ASSEMBLER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ARGV_ASSEMBLER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ARGV_ASSEMBLER_TRACE\"\n", + "exec \"$WW_ARGV_REAL_ASSEMBLER\" \"$@\"\n")); + writeexecutable(linkerwrapper, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ARGV_LINKER_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ARGV_LINKER_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ARGV_LINKER_TRACE\"\n", + "exec \"$WW_ARGV_REAL_LINKER\" \"$@\"\n")); + writeexecutable(failurewrapper, strings.concat( + "#!/bin/sh\n", + "printf 'BEGIN' >> \"$WW_ARGV_FAILURE_TRACE\"\n", + "for arg in \"$@\"; do printf '<%s>' \"$arg\" >> ", + "\"$WW_ARGV_FAILURE_TRACE\"; done\n", + "printf '\\n' >> \"$WW_ARGV_FAILURE_TRACE\"\n", + "exit 23\n")); + + 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 bin: str = strings.concat(root, "/published test binary"); + let workroot: str = strings.concat(bin, ".sepwork"); + let work: str = strings.concat(workroot, "/"); + let referenceunit: str = ""; + let referencewwi: str = ""; + let referencearchive: str = ""; + let referencebin: str = ""; + let referencecompiler: str = ""; + let referenceassembler: str = ""; + let referencelinker: str = ""; + let referenceout: str = ""; + let referenceerr: str = ""; + let referencefailure: str = ""; + let baseenv: []str = os.getenvs(); + let si: i32 = 0; + for (si < stages.len) { + let compilertrace: str = strings.concat(root, "/", tags[si], + " compiler trace"); + let assemblertrace: str = strings.concat(root, "/", tags[si], + " assembler trace"); + let linkertrace: str = strings.concat(root, "/", tags[si], + " linker trace"); + let failuretrace: str = strings.concat(root, "/", tags[si], + " failure trace"); + writefile(compilertrace, ""); + writefile(assemblertrace, ""); + writefile(linkertrace, ""); + writefile(failuretrace, ""); + let env: []str = alloc([], (baseenv.len + 9): 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_ARGV_COMPILER_TRACE=") + && !strings.hasprefix(baseenv[ei], + "WW_ARGV_ASSEMBLER_TRACE=") + && !strings.hasprefix(baseenv[ei], + "WW_ARGV_LINKER_TRACE=") + && !strings.hasprefix(baseenv[ei], + "WW_ARGV_REAL_COMPILER=") + && !strings.hasprefix(baseenv[ei], + "WW_ARGV_REAL_ASSEMBLER=") + && !strings.hasprefix(baseenv[ei], + "WW_ARGV_REAL_LINKER=")) { + 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_ARGV_COMPILER_TRACE=", compilertrace)); + append(env, strings.concat("WW_ARGV_ASSEMBLER_TRACE=", assemblertrace)); + append(env, strings.concat("WW_ARGV_LINKER_TRACE=", linkertrace)); + append(env, strings.concat("WW_ARGV_REAL_COMPILER=", + driver(compilers[si]))); + append(env, strings.concat("WW_ARGV_REAL_ASSEMBLER=", + driver(assemblers[si]))); + append(env, strings.concat("WW_ARGV_REAL_LINKER=", + driver(linkers[si]))); + + let av: []str = [driver(stages[si]), "test", "-c", "-o", bin, + "-I", source, target]; + let out: commandout; + runcommandenv(root, strings.concat("exact-argv-", tags[si]), av, env, + (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(out.stderr.len == 0); + let rootunit: str = readfile(strings.concat(work, + "__ww-test-000-same.unit.ww")); + let depunit: str = readfile(strings.concat(work, "dep.unit.ww")); + assert(has(rootunit, "//ww:module dep\n")); + assert(!has(rootunit, "//ww:module leaf\n")); + assert(has(depunit, "//ww:module leaf\n")); + assert(os.exists(strings.concat(work, "leaf.wwi"))); + assert(os.exists(strings.concat(work, "leaf.a"))); + assert(os.exists(strings.concat(work, "dep.wwi"))); + assert(os.exists(strings.concat(work, "dep.a"))); + assert(!os.exists(strings.concat(work, + "__ww-test-000-same.wwi"))); + assert(!os.exists(strings.concat(work, + "__ww-test-000-same.a"))); + assert(os.exists(bin)); + + let ctrace: str = readfile(compilertrace); + let atrace: str = readfile(assemblertrace); + let ltrace: str = readfile(linkertrace); + assert(occurrences(ctrace, strings.concat("<", work, + "leaf.unit.ww>")) == 1); + assert(occurrences(ctrace, strings.concat("<", work, + "dep.unit.ww>")) == 1); + assert(occurrences(ctrace, strings.concat("<", work, + "__ww-test-000-same.unit.ww>")) == 1); + assert(pos(ctrace, strings.concat("<", work, "leaf.unit.ww>")) + < pos(ctrace, strings.concat("<", work, "dep.unit.ww>"))); + assert(pos(ctrace, strings.concat("<", work, "dep.unit.ww>")) + < pos(ctrace, strings.concat("<", work, + "__ww-test-000-same.unit.ww>"))); + assert(has(ctrace, strings.concat("BEGIN<-c><-I><", work, + "dep.wwi><-o><", work, "dep.s><", work, "dep.unit.ww>"))); + assert(has(ctrace, strings.concat( + "BEGIN<-T><--test-support-module><-c><-o><", work, + "__ww-test-000-same.s><", work, + "__ww-test-000-same.unit.ww>"))); + assert(has(atrace, strings.concat("BEGIN<-o><", work, + "dep.o><", work, "dep.s>"))); + assert(occurrences(ltrace, "\n") == 1); + assert(has(ltrace, strings.concat("BEGIN<-o><", bin, "><", work, + "__ww-test-000-same.o>"))); + assert(has(ltrace, strings.concat("<", work, "dep.a>"))); + assert(has(ltrace, strings.concat("<", work, "leaf.a>"))); + assert(has(ltrace, strings.concat("<", work, "test.a>"))); + assert(!has(ltrace, ".wwi>")); + + let runav: []str = [bin]; + runcommand(root, strings.concat("exact-argv-run-", tags[si]), runav, + (60i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 0); + assert(has(out.stdout, "exact_argv ... ok\n")); + if (si == 0) { + referenceunit = strings.dup(rootunit); + referencewwi = strings.dup(readfile(strings.concat(work, + "dep.wwi"))); + referencearchive = strings.dup(readfile(strings.concat(work, + "dep.a"))); + referencebin = strings.dup(readfile(bin)); + referencecompiler = strings.dup(ctrace); + referenceassembler = strings.dup(atrace); + referencelinker = strings.dup(ltrace); + referenceout = strings.dup(out.stdout); + referenceerr = strings.dup(out.stderr); + } else { + assert(same(referenceunit, rootunit)); + assert(same(referencewwi, + readfile(strings.concat(work, "dep.wwi")))); + assert(same(referencearchive, + readfile(strings.concat(work, "dep.a")))); + assert(same(referencebin, readfile(bin))); + assert(same(referencecompiler, ctrace)); + assert(same(referenceassembler, atrace)); + assert(same(referencelinker, ltrace)); + assert(same(referenceout, out.stdout)); + assert(same(referenceerr, out.stderr)); + }; + clean(workroot); + clean(bin); + + let failenv: []str = alloc([], (env.len + 2): u64)!; + ei = 0; + for (ei < env.len) { + if (!strings.hasprefix(env[ei], "WW_W6C=") + && !strings.hasprefix(env[ei], + "WW_ARGV_FAILURE_TRACE=")) { + append(failenv, env[ei]); + }; + ei += 1; + }; + append(failenv, strings.concat("WW_W6C=", failurewrapper)); + append(failenv, strings.concat("WW_ARGV_FAILURE_TRACE=", failuretrace)); + runcommandenv(root, strings.concat("exact-argv-fail-", tags[si]), av, + failenv, (120i64 * (time.second: i64)): time.duration, &out); + expectexit(&out, 1); + assert(has(out.stderr, "ww: w6c failed for leaf\n")); + assert(has(readfile(failuretrace), strings.concat("<-I><", work, + "leaf.wwi>"))); + if (si == 0) { referencefailure = strings.dup(out.stderr); } + else { assert(same(referencefailure, out.stderr)); }; + clean(workroot); + if (os.exists(bin)) { clean(bin); }; + si += 1; + }; + clean(root); +}; + @test fn package_graph_diagnostics_are_stable() void = { let root: str = fresh(); let missing: str = strings.concat(root, "/missing"); @@ -2640,9 +2879,9 @@ fn runtimepath(relative: str) str = { let ctraced: str = readfile(linktrace); assert(has(ctraced, strings.concat(" -L ", root, " -l extra"))); - // WWstage intentionally locates sibling tools. Copy the real driver beside - // a tracing linker and exact compiler/assembler siblings to inspect its - // joined native-linker flag contract. + // Keep this row on default sibling discovery: a copied driver beside a + // tracing linker and exact compiler/assembler siblings exposes WWstage's + // joined native-linker flag contract independently of tool overrides. let tracedir: str = strings.concat(repo(), "/out/link-flag-trace-", workescape(root)); assert(os.mkdir(tracedir, 448i32) == 0); diff --git a/test/sep/localbuild_test.ww b/test/sep/localbuild_test.ww index 12c97244..93241bfd 100644 --- a/test/sep/localbuild_test.ww +++ b/test/sep/localbuild_test.ww @@ -421,7 +421,8 @@ fn writediamond(td: str, reverse: bool) str = { let trace: str = strings.concat(td, "/compiler.trace"); let wrapper: str = strings.concat(td, "/trace-w6c.sh"); testenv.writefile(trace, ""); - testenv.writefile(wrapper, strings.concat( + testenv.writeexecutable(wrapper, strings.concat( + "#!/bin/sh\n", "printf '%s\\n' \"$*\" >> \"$WW_LOCALBUILD_TRACE\"\n", "exec \"$WW_LOCALBUILD_W6C\" \"$@\"\n")); let baseenv: []str = os.getenvs(); @@ -435,7 +436,7 @@ fn writediamond(td: str, reverse: bool) str = { }; ei += 1; }; - append(env, strings.concat("WW_W6C=/bin/sh ", wrapper)); + append(env, strings.concat("WW_W6C=", wrapper)); append(env, strings.concat("WW_LOCALBUILD_TRACE=", trace)); append(env, strings.concat("WW_LOCALBUILD_W6C=", testenv.driver("w6c"))); diff --git a/test/testenv/testenv.ww b/test/testenv/testenv.ww index 6134a441..7bd727f5 100644 --- a/test/testenv/testenv.ww +++ b/test/testenv/testenv.ww @@ -76,6 +76,17 @@ export fn writefile(path: str, content: str) void = { assert(os.close(fd) == 0); }; +export fn writeexecutable(path: str, content: str) void = { + let fd: i32 = os.open(path, + os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 448i32); + assert(fd >= 0); + match (os.writeall(fd, content.ptr, content.len: u64)) { + case let n: i64 => assert(n == content.len: i64); + case let e: os.oserror => abort("write failed"); + }; + assert(os.close(fd) == 0); +}; + export fn exists(path: str) bool = { return os.access(path, 0i32) == 0; };