ww build: honor Go output permission modes
This commit is contained in:
@@ -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);
|
||||
};
|
||||
|
||||
27
test/package/umaskexec.c
Normal file
27
test/package/umaskexec.c
Normal file
@@ -0,0 +1,27 @@
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
char *end;
|
||||
unsigned long mask;
|
||||
|
||||
if (argc < 3) {
|
||||
fputs("usage: package-umaskexec MASK PROGRAM [ARG ...]\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
errno = 0;
|
||||
mask = strtoul(argv[1], &end, 8);
|
||||
if (errno != 0 || end == argv[1] || *end != '\0' || mask > 0777) {
|
||||
fputs("package-umaskexec: invalid mask\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
umask((mode_t)mask);
|
||||
execv(argv[2], &argv[2]);
|
||||
fputs("package-umaskexec: exec failed\n", stderr);
|
||||
return 127;
|
||||
}
|
||||
Reference in New Issue
Block a user