ww build: honor Go output permission modes

This commit is contained in:
2026-08-20 22:03:09 +09:00
parent 77168fd891
commit 84f5e68709
8 changed files with 513 additions and 9 deletions

View File

@@ -62,6 +62,14 @@ fn readfile(path: str) str = {
return out;
};
fn permissionmode(path: str) u32 = {
let fi: os.filestat;
match (os.stat(&fi, path)) {
case void => return (fi.mode: u32) & 511u32;
case let e: os.oserror => abort("stat failed");
};
};
fn writefile(path: str, content: str) void = {
let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
@@ -12635,3 +12643,350 @@ fn runtimepath(relative: str) str = {
"/occupied/alpha.test.new")) == 0);
clean(root);
};
@test fn build_output_permissions_follow_umask() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let alpha: str = strings.concat(source, "/alpha");
let fan: str = strings.concat(source, "/fan");
let fanalpha: str = strings.concat(fan, "/alpha");
let fanbeta: str = strings.concat(fan, "/beta");
let fanlib: str = strings.concat(fan, "/library");
let library: str = strings.concat(source, "/library");
let check: str = strings.concat(source, "/check");
mkdirall(alpha); mkdirall(fanalpha); mkdirall(fanbeta);
mkdirall(fanlib); mkdirall(library); mkdirall(check);
let alphafile: str = strings.concat(alpha, "/main.ww");
let alphabase: str = strings.concat(
"package main;\n",
"fn main() i32 = { return 21; };\n");
let alphachanged: str = strings.concat(
"package main;\n",
"fn main() i32 = { return 22; };\n");
writefile(alphafile, alphabase);
writefile(strings.concat(fanalpha, "/main.ww"),
"package main;\nfn main() i32 = { return 31; };\n");
writefile(strings.concat(fanbeta, "/main.ww"),
"package main;\nfn main() i32 = { return 32; };\n");
writefile(strings.concat(fanlib, "/library.ww"),
"package library;\nexport fn value() i32 = { return 33; };\n");
writefile(strings.concat(library, "/library.ww"),
"package library;\nexport fn value() i32 = { return 41; };\n");
let raw: str = strings.concat(source, "/raw.ww");
writefile(raw, "package main;\nfn main() i32 = { return 23; };\n");
writefile(strings.concat(check, "/check.ww"),
"package check;\nfn value() i32 = { return 1; };\n");
writefile(strings.concat(check, "/check_test.ww"), strings.concat(
"package check;\n",
"@test fn retained() void = { assert(value() == 1); };\n"));
let launcher: str = driver("package-umaskexec");
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let linkers: []str = ["w6l", "w6l_ww"];
let coldref: str = "";
let changedref: str = "";
let rawref: str = "";
let defaultref: str = "";
let fanalpharef: str = "";
let fanbetaref: str = "";
let archiverefs: []str = ["", ""];
let testref: str = "";
let faildiagref: str = "";
let occupieddiagref: str = "";
let linkerwrapper: str = strings.concat(root, "/mode-w6l.sh");
writeexecutable(linkerwrapper, strings.concat(
"#!/bin/sh\n",
"for arg do case \"$arg\" in */beta.new)\n",
" printf 'injected build-mode linker failure\\n' >&2\n",
" exit 97;; esac; done\n",
"exec \"$WW_MODE_REAL_LINKER\" \"$@\"\n"));
let baseenv: []str = os.getenvs();
let si: i32 = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/work-", tags[si]);
mkdirall(work);
let binary: str = strings.concat(root, "/command-", tags[si]);
let coldav: []str = [launcher, "000", driver(stages[si]), "build",
"-w", work, "-I", source, "-o", binary, alpha];
let out: commandout;
runcommand(root, strings.concat("mode-cold-", tags[si]), coldav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
assert(permissionmode(binary) == 511u32);
assert(!os.exists(strings.concat(binary, ".new")));
assert(!os.exists(strings.concat(binary, "-go-tmp-umask")));
let coldbytes: str = readfile(binary);
let coldarchive: str = readfile(strings.concat(work, "/alpha.a"));
if (si == 0) { coldref = strings.dup(coldbytes); }
else { assert(same(coldref, coldbytes)); };
let runav: []str = [binary];
runcommand(root, strings.concat("mode-cold-run-", tags[si]), runav,
time.second, &out);
expectexit(&out, 21);
// Umask is request-local publication metadata: the warm graph stays
// byte-stable while replacing the same output with the current mode.
let warmav: []str = [launcher, "077", driver(stages[si]), "build",
"-w", work, "-I", source, "-o", binary, alpha];
runcommand(root, strings.concat("mode-warm-", tags[si]), warmav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
assert(permissionmode(binary) == 448u32);
assert(same(coldbytes, readfile(binary)));
assert(same(coldarchive, readfile(strings.concat(work, "/alpha.a"))));
assert(!directoryhasnew(work));
// Source invalidation changes the command bytes, not the creation rule.
rewritefile(alphafile, alphachanged);
let changedav: []str = [launcher, "027", driver(stages[si]), "build",
"-w", work, "-I", source, "-o", binary, alpha];
runcommand(root, strings.concat("mode-changed-", tags[si]), changedav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(binary) == 488u32);
let changedbytes: str = readfile(binary);
assert(!same(coldbytes, changedbytes));
if (si == 0) { changedref = strings.dup(changedbytes); }
else { assert(same(changedref, changedbytes)); };
let changedrun: []str = [binary];
runcommand(root, strings.concat("mode-changed-run-", tags[si]),
changedrun, time.second, &out);
expectexit(&out, 22);
rewritefile(alphafile, alphabase);
// A default command output and an explicit raw-file output use the same
// link-install permission, independent of their naming route.
let defaultdir: str = strings.concat(root, "/default-", tags[si]);
let defaultwork: str = strings.concat(root, "/default-work-", tags[si]);
mkdirall(defaultdir); mkdirall(defaultwork);
let defaultav: []str = [launcher, "000", driver(stages[si]),
"build", "-w", defaultwork, "-I", source, alpha];
runcommanddir(root, strings.concat("mode-default-", tags[si]),
defaultdir, defaultav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let defaultbin: str = strings.concat(defaultdir, "/alpha");
assert(permissionmode(defaultbin) == 511u32);
let defaultbytes: str = readfile(defaultbin);
if (si == 0) { defaultref = strings.dup(defaultbytes); }
else { assert(same(defaultref, defaultbytes)); };
let rawbin: str = strings.concat(root, "/raw-", tags[si]);
let rawav: []str = [launcher, "007", driver(stages[si]), "build",
"-o", rawbin, raw];
runcommand(root, strings.concat("mode-raw-", tags[si]), rawav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(rawbin) == 504u32);
let rawbytes: str = readfile(rawbin);
if (si == 0) { rawref = strings.dup(rawbytes); }
else { assert(same(rawref, rawbytes)); };
// Directory fan-out creates each command independently, skips the
// library product, and leaves no request-private publication residue.
let fanout: str = strings.concat(root, "/fanout-", tags[si]);
let fanwork: str = strings.concat(root, "/fanwork-", tags[si]);
mkdirall(fanout); mkdirall(fanwork);
let fanav: []str = [launcher, "027", driver(stages[si]), "build",
"-w", fanwork, "-I", source, "-o", fanout,
strings.concat(fan, "/...")];
runcommand(root, strings.concat("mode-fan-", tags[si]), fanav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let fanalphabin: str = strings.concat(fanout, "/alpha");
let fanbetabin: str = strings.concat(fanout, "/beta");
assert(permissionmode(fanalphabin) == 488u32);
assert(permissionmode(fanbetabin) == 488u32);
assert(!os.exists(strings.concat(fanout, "/library")));
assert(!directoryhasnew(fanout) && !directoryhasnew(fanwork));
let fanalphabytes: str = readfile(fanalphabin);
let fanbetabytes: str = readfile(fanbetabin);
if (si == 0) {
fanalpharef = strings.dup(fanalphabytes);
fanbetaref = strings.dup(fanbetabytes);
} else {
assert(same(fanalpharef, fanalphabytes));
assert(same(fanbetaref, fanbetabytes));
};
// The non-link branch of BuildInstallFunc uses 0666. WW's required
// adjacent interface follows the archive's data-file permission.
let libwork: str = strings.concat(root, "/libwork-", tags[si]);
mkdirall(libwork);
let archive: str = strings.concat(root, "/library-", tags[si], ".a");
let libav: []str = [launcher, "000", driver(stages[si]), "build",
"-w", libwork, "-I", source, "-o", archive, library];
runcommand(root, strings.concat("mode-library-", tags[si]), libav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(archive) == 438u32);
assert(permissionmode(strings.concat(archive, ".wwi")) == 438u32);
archiverefs[si] = strings.dup(readfile(archive));
let libwarm: []str = [launcher, "077", driver(stages[si]), "build",
"-w", libwork, "-I", source, "-o", archive, library];
runcommand(root, strings.concat("mode-library-warm-", tags[si]), libwarm,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(archive) == 384u32);
assert(permissionmode(strings.concat(archive, ".wwi")) == 384u32);
assert(same(archiverefs[si], readfile(archive)));
assert(!os.exists(strings.concat(archive, ".new")));
assert(!os.exists(strings.concat(archive, ".wwi.new")));
// Test-binary retention is a separate publication branch, but the
// ordinary linked-executable rule remains identical and byte-stable.
let testbin: str = strings.concat(root, "/check-", tags[si], ".test");
let testav: []str = [launcher, "000", driver(stages[si]), "test",
"-c", "-I", source, "-o", testbin, check];
runcommand(root, strings.concat("mode-test-", tags[si]), testav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(testbin) == 511u32);
let testbytes: str = readfile(testbin);
if (si == 0) { testref = strings.dup(testbytes); }
else { assert(same(testref, testbytes)); };
let testwarm: []str = [launcher, "077", driver(stages[si]), "test",
"-c", "-I", source, "-o", testbin, check];
runcommand(root, strings.concat("mode-test-warm-", tags[si]), testwarm,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(testbin) == 448u32);
assert(same(testbytes, readfile(testbin)));
assert(!os.exists(strings.concat(testbin, ".new")));
// Assembly-only builds do not publish an inode to which the permission
// contract could apply.
let asmdir: str = strings.concat(root, "/asm-dir-", tags[si]);
let asmwork: str = strings.concat(root, "/asm-work-", tags[si]);
mkdirall(asmdir); mkdirall(asmwork);
let asmav: []str = [launcher, "000", driver(stages[si]), "build",
"-S", "-w", asmwork, "-I", source, alpha];
runcommanddir(root, strings.concat("mode-asm-", tags[si]), asmdir,
asmav, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(!os.exists(strings.concat(asmdir, "/alpha")));
// A later link failure rolls back already staged siblings. Existing
// destination bytes and modes survive; neither transaction nor action
// stages remain.
let failout: str = strings.concat(root, "/failout-", tags[si]);
let failwork: str = strings.concat(root, "/failwork-", tags[si]);
mkdirall(failout); mkdirall(failwork);
writefile(strings.concat(failout, "/alpha"), "old-alpha\n");
writefile(strings.concat(failout, "/beta"), "old-beta\n");
let failenv: []str = alloc([], (baseenv.len + 2): u64)!;
let ei: i32 = 0;
for (ei < baseenv.len) {
if (!strings.hasprefix(baseenv[ei], "WW_W6L=")
&& !strings.hasprefix(baseenv[ei], "WW_MODE_REAL_LINKER=")) {
append(failenv, baseenv[ei]);
};
ei += 1;
};
append(failenv, strings.concat("WW_W6L=", linkerwrapper));
append(failenv, strings.concat("WW_MODE_REAL_LINKER=",
driver(linkers[si])));
let failav: []str = [launcher, "000", driver(stages[si]), "build",
"-w", failwork, "-I", source, "-o", failout,
strings.concat(fan, "/...")];
runcommandenv(root, strings.concat("mode-failure-", tags[si]), failav,
failenv, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "injected build-mode linker failure\n"));
assert(has(out.stderr, "ww: w6l failed\n"));
assert(same(readfile(strings.concat(failout, "/alpha")),
"old-alpha\n"));
assert(same(readfile(strings.concat(failout, "/beta")),
"old-beta\n"));
assert(permissionmode(strings.concat(failout, "/alpha")) == 384u32);
assert(permissionmode(strings.concat(failout, "/beta")) == 384u32);
assert(!directoryhasnew(failout) && !directoryhasnew(failwork));
let faildiag: str = normalizedtrace(out.stderr,
strings.concat(failwork, "/"), failout);
if (si == 0) { faildiagref = strings.dup(faildiag); }
else { assert(same(faildiagref, faildiag)); };
// An occupied caller-visible stage is rejected before production and
// leaves both the prior output and the occupied inode untouched.
let occupied: str = strings.concat(root, "/occupied");
let occupiedstage: str = strings.concat(occupied, ".new");
if (si == 0) {
writefile(occupied, "old-output\n");
writefile(occupiedstage, "occupied-stage\n");
};
let occupiedav: []str = [launcher, "000", driver(stages[si]),
"build", "-I", source, "-o", occupied, alpha];
runcommand(root, strings.concat("mode-occupied-", tags[si]), occupiedav,
time.second, &out);
expectexit(&out, 1);
assert(same(readfile(occupied), "old-output\n"));
assert(same(readfile(occupiedstage), "occupied-stage\n"));
assert(permissionmode(occupied) == 384u32);
assert(permissionmode(occupiedstage) == 384u32);
if (si == 0) { occupieddiagref = strings.dup(out.stderr); }
else { assert(same(occupieddiagref, out.stderr)); };
si += 1;
};
assert(same(archiverefs[0], archiverefs[1]));
let artifacts: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
".init.unit.ww", ".init.s", ".init.o"];
let ai: i32 = 0;
for (ai < artifacts.len) {
assert(same(readfile(strings.concat(root, "/work-c/alpha",
artifacts[ai])), readfile(strings.concat(root,
"/work-ww/alpha", artifacts[ai]))));
ai += 1;
};
// Two independent drivers execute simultaneously with different umasks.
// Their process-local masks cannot leak across stages, works, or outputs.
let parallelcwork: str = strings.concat(root, "/parallel-c-work");
let parallelwwwork: str = strings.concat(root, "/parallel-ww-work");
mkdirall(parallelcwork); mkdirall(parallelwwwork);
let parallelcbin: str = strings.concat(root, "/parallel-c");
let parallelwwbin: str = strings.concat(root, "/parallel-ww");
let cav: []str = [launcher, "002", driver("ww"), "build", "-w",
parallelcwork, "-I", source, "-o", parallelcbin, fanalpha];
let wav: []str = [launcher, "077", driver("ww_ww"), "build", "-w",
parallelwwwork, "-I", source, "-o", parallelwwbin, fanbeta];
let cc: exec.command;
cc.path = launcher; cc.argv = cav; cc.env = os.getenvs(); cc.dir = repo();
cc.stdoutpath = strings.concat(root, "/parallel-c.stdout");
cc.stderrpath = strings.concat(root, "/parallel-c.stderr");
cc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
cc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let wc: exec.command;
wc.path = launcher; wc.argv = wav; wc.env = os.getenvs(); wc.dir = repo();
wc.stdoutpath = strings.concat(root, "/parallel-ww.stdout");
wc.stderrpath = strings.concat(root, "/parallel-ww.stderr");
wc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
wc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let cp: exec.process;
let wp: exec.process;
exec.start(&cp, &cc); exec.start(&wp, &wc);
let cdone: bool = false;
let wdone: bool = false;
for (!cdone || !wdone) {
if (!cdone) { cdone = exec.poll(&cp); };
if (!wdone) { wdone = exec.poll(&wp); };
if (!cdone || !wdone) {
time.sleep(time.millisecond, time.clock.monotonic);
};
};
assert(cp.result.errno == 0 && cp.result.cleanuperrno == 0);
assert(wp.result.errno == 0 && wp.result.cleanuperrno == 0);
assert(cp.result.termination == exec.termination.EXIT && cp.result.code == 0);
assert(wp.result.termination == exec.termination.EXIT && wp.result.code == 0);
assert(readfile(cc.stdoutpath).len == 0 && readfile(cc.stderrpath).len == 0);
assert(readfile(wc.stdoutpath).len == 0 && readfile(wc.stderrpath).len == 0);
assert(permissionmode(parallelcbin) == 509u32);
assert(permissionmode(parallelwwbin) == 448u32);
assert(!directoryhasnew(parallelcwork));
assert(!directoryhasnew(parallelwwwork));
clean(root);
};