ww build: honor output directories

This commit is contained in:
2026-08-21 02:32:56 +09:00
parent 71a9da2058
commit ce91f6662c
7 changed files with 1768 additions and 107 deletions

View File

@@ -1465,6 +1465,7 @@ type seppkg = struct {
generatedtargets: []i32,
ngeneratedtargets: i32,
failed: bool,
action: bool, // reached by this request's semantic action list
testsupport: bool,
loaded: bool,
exportchanged: bool,
@@ -1513,6 +1514,7 @@ type sepproduct = struct {
variant: i32,
directoryproduct: bool,
notests: bool,
buildaction: bool, // loaded product retained in the action list
context: i32,
root: i32,
variantroot: i32,
@@ -2973,6 +2975,7 @@ fn sepfindoraddvariant(g: *sepgraph, path: *u8, entry: *u8,
g.pkg[g.n].generatedtargets = emptytargets;
g.pkg[g.n].ngeneratedtargets = 0;
g.pkg[g.n].failed = false;
g.pkg[g.n].action = false;
g.pkg[g.n].testsupport = false;
g.pkg[g.n].loaded = false;
g.pkg[g.n].exportchanged = false;
@@ -3537,7 +3540,9 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = {
fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = {
let i: i32 = 0;
for (i < g.n) {
if (g.pkg[i].failed || !g.pkg[i].loaded) { i += 1; continue; };
if (g.pkg[i].failed || !g.pkg[i].loaded || !g.pkg[i].action) {
i += 1; continue;
};
if (g.pkg[i].storage == nil
&& sepassignstorage(&g.pkg[i], scratch) < 0) { return -1; };
i += 1;
@@ -3547,10 +3552,12 @@ fn sepvalidateartifactpaths(g: *sepgraph, scratch: *u8) i32 = {
changed = false;
i = 0;
for (i < g.n && !changed) {
if (g.pkg[i].failed || !g.pkg[i].loaded) { i += 1; continue; };
if (g.pkg[i].failed || !g.pkg[i].loaded || !g.pkg[i].action) {
i += 1; continue;
};
let j: i32 = i + 1;
for (j < g.n) {
if (g.pkg[j].failed || !g.pkg[j].loaded
if (g.pkg[j].failed || !g.pkg[j].loaded || !g.pkg[j].action
|| !cstreq(g.pkg[i].storage, g.pkg[j].storage)) {
j += 1; continue;
};
@@ -4350,6 +4357,7 @@ fn sepaddgeneratedmain(g: *sepgraph, product: *sepproduct, ordinal: i32,
ti += 1;
};
p.failed = false;
p.action = false;
p.testsupport = false;
p.loaded = true;
p.exportchanged = false;
@@ -4473,6 +4481,7 @@ fn sepclonefortest(g: *sepgraph, original: i32, owner: *u8,
p.generatedtargets = emptytargets;
p.ngeneratedtargets = 0;
p.failed = src.failed;
p.action = false;
p.testsupport = src.testsupport;
p.loaded = src.loaded;
p.exportchanged = false;
@@ -4501,6 +4510,15 @@ fn sepclonefortest(g: *sepgraph, original: i32, owner: *u8,
seppkgfreeowned(&p);
return -1;
};
// A test copy is product-scoped even when its replaced source node is not
// in the final action closure. Preserve the complete-action locator without
// relying on a storage collision with that inactive source node.
p.storage = sepstoragedigest(&p);
p.storagehashed = true;
if (p.storage == nil) {
seppkgfreeowned(&p);
return -1;
};
if (src.nsources > 0) {
let sources: []*u8;
@@ -5856,7 +5874,7 @@ fn sepvalidateunitowner(g: *sepgraph, pi: i32, scratch: *u8) i32 = {
fn sepvalidateworkdirowners(g: *sepgraph, scratch: *u8) i32 = {
let i: i32 = 0;
for (i < g.n) {
if (!g.pkg[i].failed && g.pkg[i].loaded
if (!g.pkg[i].failed && g.pkg[i].loaded && g.pkg[i].action
&& sepvalidateunitowner(g, i, scratch) < 0) { return -1; };
i += 1;
};
@@ -6035,7 +6053,7 @@ fn sepvalidaterequeststaging(g: *sepgraph, scratch: *u8, warm: bool,
".a.new", ".init.unit.new", ".init.s.new", ".init.o.new"];
let pi: i32 = 0;
for (pi < g.n) {
if (!g.pkg[pi].failed && g.pkg[pi].loaded) {
if (!g.pkg[pi].failed && g.pkg[pi].loaded && g.pkg[pi].action) {
let si: i32 = 0;
for (si < suffix.len) {
let path: *u8 = sepfname(g, pi, scratch, suffix[si]);
@@ -6437,6 +6455,7 @@ fn sepdiscardrequeststaging(g: *sepgraph, scratch: *u8, warm: bool,
let rc: i32 = 0;
let pi: i32 = 0;
for (pi < g.n) {
if (!g.pkg[pi].action) { pi += 1; continue; };
let si: i32 = 0;
for (si < suffix.len) {
let path: *u8 = sepfname(g, pi, scratch, suffix[si]);
@@ -6941,10 +6960,12 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8,
};
fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
out: *u8, objstem: *u8, incs: *u8, lf: *lflags,
publishpackage: i32, requirecommand: i32, istest: i32,
products: *sepproduct, nproducts: i32, emitasm: i32,
workdir: *u8, createworkdir: bool, createoutputdir: *u8,
out: *u8, objstem: *u8, incs: *u8, lf: *lflags,
publishpackage: i32, requirecommand: i32, istest: i32,
products: *sepproduct, nproducts: i32, emitasm: i32,
workdir: *u8, createworkdir: bool, createoutputdir: *u8,
defaultoutputdir: *u8, outputpatherror: bool,
outputcollisionbase: *u8, outputcollisiondir: *u8,
scratchout: **u8,
graphout: **sepgraph) i32 = {
sepfatalallocation = false;
@@ -7511,9 +7532,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let rootpackage: bool = istest == 0 && nproducts == 1
&& !g.pkg[products[0].root].failed
&& !seprootiscommand(&g.pkg[products[0].root]);
if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; };
if (warm && workdirexists
&& sepvalidateworkdirowners(g, scratch) < 0) { return 1; };
let ci: i32 = 0;
let order: []i32;
let stack: []i32;
@@ -7572,6 +7590,22 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
return 1;
};
};
if (istest == 0 && createoutputdir != nil) {
let actions: i32 = 0;
producti = 0;
for (producti < nproducts) {
let root: i32 = products[producti].root;
if (g.pkg[root].failed) { return 1; };
products[producti].buildaction =
seprootiscommand(&g.pkg[root]);
if (products[producti].buildaction) { actions += 1; };
producti += 1;
};
if (actions == 0) {
cerr("ww: no main packages to build\n");
return 1;
};
};
if (!g.pkg[products[0].root].failed
&& rootpackage && publishpackage != 0 && emitasm == 0
&& validatepackageoutputpath(out) < 0) { return 1; };
@@ -7580,12 +7614,25 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
producti = 0;
for (producti < nproducts) {
let root: i32 = products[producti].root;
if (!g.pkg[root].failed) {
if (products[producti].buildaction && !g.pkg[root].failed) {
if (septopovisit(g, root, order,
&norder, stack, 0) < 0) { return 1; };
};
producti += 1;
};
let actionpi: i32 = 0;
for (actionpi < g.n) {
g.pkg[actionpi].action = false;
actionpi += 1;
};
let actionoi: i32 = 0;
for (actionoi < norder) {
g.pkg[order[actionoi]].action = true;
actionoi += 1;
};
if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; };
if (warm && workdirexists
&& sepvalidateworkdirowners(g, scratch) < 0) { return 1; };
// Propagate already-known package-load failures before scratch or status
// acquisition. Good sibling roots may remain viable for deterministic
// staging/diagnosis, but any failure rejects publication; an entirely failed
@@ -7605,12 +7652,31 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let viableproduct: bool = false;
producti = 0;
for (producti < nproducts) {
if (!g.pkg[products[producti].root].failed) {
if (products[producti].buildaction
&& !g.pkg[products[producti].root].failed) {
viableproduct = true;
};
producti += 1;
};
if (!viableproduct) { return 1; };
if (istest == 0 && emitasm == 0 && outputpatherror) {
cerr("ww: command output path is too long\n");
return 1;
};
if (istest == 0 && emitasm == 0 && outputcollisionbase != nil) {
cerr("ww: multiple commands produce output basename ");
sepputquoted(outputcollisionbase);
cerr(" in directory ");
sepputquoted(outputcollisiondir);
cerr("\n");
return 1;
};
if (istest == 0 && emitasm == 0 && defaultoutputdir != nil && nproducts == 1
&& seprootiscommand(&g.pkg[products[0].root])) {
cerrpath("ww: build output \"", defaultoutputdir,
"\" already exists and is a directory\n");
return 1;
};
if (sepvalidaterequeststaging(g, scratch, warm, products, nproducts,
rootpackage, publishpackage, emitasm, istest) < 0) {
sepfreeproductstaging(products, nproducts);
@@ -7622,10 +7688,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32,
let createdwork: sepcreateddirs;
createdoutput.n = 0;
createdwork.n = 0;
let outputmode: i32 = 448;
if (istest != 0) { outputmode = 511; };
if (createoutputdir != nil
&& sepmkdirsrecord(createoutputdir, outputmode, &createdoutput) != 0) {
if (emitasm == 0 && createoutputdir != nil
&& sepmkdirsrecord(createoutputdir, 511, &createdoutput) != 0) {
if (istest != 0) {
cerrpath("ww: cannot create test output directory ",
createoutputdir, "\n");
@@ -8149,7 +8213,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32,
requirecommand: i32, istest: i32,
rootvariant: i32, testpackage: *u8, emitasm: i32,
keepscratch: i32, workdir: *u8) i32 = {
keepscratch: i32, workdir: *u8, createoutputdir: *u8,
defaultoutputdir: *u8, outputpatherror: bool) i32 = {
let scratch: *u8 = nil;
let g: *sepgraph = nil;
let product: sepproduct;
@@ -8169,12 +8234,15 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
product.variant = rootvariant;
product.directoryproduct = false;
product.notests = false;
product.buildaction = true;
product.root = -1;
product.variantroot = -1;
product.support = -1;
let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem,
incs, lf, publishpackage, requirecommand, istest, &product, 1,
emitasm, workdir, false, nil, &scratch, &g);
emitasm, workdir, false, createoutputdir, defaultoutputdir,
outputpatherror, nil, nil,
&scratch, &g);
sepgraphfree(g);
if (keepscratch == 0 && scratch != nil) {
if (cstrendswithlit(scratch, ".sepwork")) {
@@ -8205,7 +8273,9 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
fn buildpackagetests(selfdir: *u8, src: *u8, rootidentity: *u8,
incs: *u8, workdir: *u8, products: *sepproduct, nproducts: i32,
istest: i32, publishpackage: i32, lf: *lflags, emitasm: i32,
createworkdir: bool, createoutputdir: *u8) i32 = {
createworkdir: bool, createoutputdir: *u8, defaultoutputdir: *u8,
outputpatherror: bool, outputcollisionbase: *u8,
outputcollisiondir: *u8) i32 = {
let scratch: *u8 = nil;
let g: *sepgraph = nil;
let i: i32 = 0;
@@ -8217,7 +8287,8 @@ fn buildpackagetests(selfdir: *u8, src: *u8, rootidentity: *u8,
products[0].out, products[0].out, incs, lf,
publishpackage, 0, istest,
products, nproducts, emitasm, workdir, createworkdir,
createoutputdir, &scratch, &g);
createoutputdir, defaultoutputdir, outputpatherror,
outputcollisionbase, outputcollisiondir, &scratch, &g);
sepgraphfree(g);
return r;
};
@@ -8313,7 +8384,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = {
};
fn writeusage(fd: i32) void = {
let s: str = "usage: ww [-V] <subcommand> [args...]\n -V print version and exit\n build [-S] [-w DIR] [-I DIR] [-o FILE] [path ...] build local package graphs\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -o publishes a non-main archive FILE + FILE.wwi\n lib/... every eligible package under lib, recursively\n . build the cwd's <basename>.ww\n";
let s: str = "usage: ww [-V] <subcommand> [args...]\n -V print version and exit\n build [-S] [-w DIR] [-I DIR] [-o FILE|DIR] [path ...] build local package graphs\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -o FILE publishes a non-main archive FILE + FILE.wwi\n -o DIR publishes each selected command beneath DIR\n lib/... every eligible package under lib, recursively\n . build the cwd's <basename>.ww\n";
os.write(fd, s.ptr, s.len: u64);
};
@@ -8352,6 +8423,54 @@ fn defaultoutpath(src: *u8) *u8 = {
return out.ptr;
};
// Go's build -o directory branch follows an existing destination through
// stat, and a trailing platform separator declares a directory which the
// request may need to create. WW's platform separator is '/'.
fn buildoutputdir(path: *u8) bool = {
if (cstrendswithlit(path, "/")) { return true; };
let fi: os.filestat;
match (os.stat(&fi, pathstr(path))) {
case void => {
let typ: u32 = (fi.mode: u32) & 61440u32;
return typ == os.mode.DIR: u32;
};
case let e: os.oserror => return false;
};
};
fn buildoutputpath(dir: *u8, src: *u8) *u8 = {
let base: *u8 = defaultoutpath(src);
let need: u64 = 0u64;
if (!sepaddbytes(&need, cstrlen(dir))
|| (!cstrendswithlit(dir, "/") && !sepaddbytes(&need, 1u64))
|| !sepaddbytes(&need, cstrlen(base))
|| !sepaddbytes(&need, 1u64)
|| need > os.PATH_MAX: u64) {
return nil;
};
if (cstrendswithlit(dir, "/")) {
return sepappendlit(dir, pathstr(base));
};
return sepjoinpath(dir, base);
};
fn defaultimportoutpath(identity: *u8) *u8 = {
let n: u64 = cstrlen(identity);
let start: u64 = 0u64;
let i: u64 = 0u64;
for (i < n) {
if (identity[i] == 46u8) { start = i + 1u64; };
i += 1u64;
};
let out: []u8 = alloc([], (os.PATH_MAX: u64))!;
out.len = os.PATH_MAX;
let off: u64 = 0u64;
i = start;
for (i < n) { out[off] = identity[i]; off += 1u64; i += 1u64; };
cstrseal(out.ptr, off);
return out.ptr;
};
fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let src: *u8 = nil;
let srcindex: i32 = -1;
@@ -8504,11 +8623,34 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let objstem: *u8 = nil;
let discardoutput: bool = outflag != nil
&& cstreqlit(outflag, "/dev/null");
let outputdir: bool = outflag != nil && outflag[0u64] != 0u8
&& !discardoutput && buildoutputdir(outflag);
let rootidentity: *u8 = nil;
if (!requestedliteral && isdir != 0) { rootidentity = src; };
if (outputdir && isdir != 0) {
let coordinatortarget: *u8 = resolved;
if (srcindex < 0) { coordinatortarget = nil; };
return execpackagetests(selfdir, argv, argc, start, srcindex,
coordinatortarget, rootidentity, srcindex < 0, true);
};
let createoutputdir: *u8 = nil;
let outputpatherror: bool = false;
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;
objstem = outflag;
if (outputdir) {
out = buildoutputpath(outflag, resolved);
if (out == nil) {
outputpatherror = true;
out = defaultoutpath(resolved);
};
createoutputdir = outflag;
} else {
out = outflag;
};
objstem = out;
} else { if (isdir != 0 && rootidentity != nil) {
out = defaultimportoutpath(rootidentity);
} else { if (isdir != 0) {
let rlen: u64 = cstrlen(resolved);
for (rlen > 1u64) {
@@ -8525,14 +8667,16 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
cstrseal(out, off);
} else {
out = defaultoutpath(resolved);
}; };
}; }; };
let defaultoutputdir: *u8 = nil;
if ((outflag == nil || outflag[0u64] == 0u8) && buildoutputdir(out)) {
defaultoutputdir = out;
};
let lf: lflags;
lf.libdirs = &libdirs[0];
lf.nlibdirs = nlibdirs;
lf.libs = &libs[0];
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;
@@ -8547,7 +8691,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let rc: i32 = buildonesep(selfdir, resolved, isdir, rootidentity,
outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, 0i32, workdir);
emitasm, 0i32, workdir, nil, nil, false);
let cleanbad: bool = false;
let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) {
@@ -8566,7 +8710,8 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
return buildonesep(selfdir, resolved, isdir, rootidentity,
out, objstem, incs.ptr, &lf,
publishpackage, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, 1i32, workdir);
emitasm, 1i32, workdir, createoutputdir, defaultoutputdir,
outputpatherror);
};
// Format the owned driver workspace /tmp/<prefix><pid> into buf. Pid is
@@ -8748,7 +8893,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (buildonesep(selfdir, resolved, isdir, rootidentity,
outp, outp, incs.ptr, &lf,
0i32, 1i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
0i32, 0i32, nil) != 0) {
0i32, 0i32, nil, nil, nil, false) != 0) {
let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n");
@@ -8841,7 +8986,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
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);
emitasm, keep, workdir, nil, nil, false);
if (bres != 0) {
if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp));
@@ -8942,6 +9087,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let packagepublish: bool = false;
let packagecreateworkdir: bool = false;
let packagecreateoutputdir: *u8 = nil;
let packageoutputpatherror: bool = false;
let packagedefaultoutputdir: *u8 = nil;
let packageoutputcollisionbase: *u8 = nil;
let packageoutputcollisiondir: *u8 = nil;
let maxpackagelflags: i32 = 32;
let packagelibdirs: [32]*u8;
let packagelibs: [32]*u8;
@@ -9008,6 +9157,37 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
i += 2;
continue;
};
if (cstreqlit(p, "--ww-command-output-path-error")) {
if (packageoutputpatherror) {
cerr("ww test: invalid --ww-command-output-path-error\n");
return 2;
};
packageoutputpatherror = true;
i += 1;
continue;
};
if (cstreqlit(p, "--ww-default-output-dir")) {
if (i + 1 >= argc || packagedefaultoutputdir != nil
|| argv[i + 1][0u64] == 0u8) {
cerr("ww test: invalid --ww-default-output-dir\n");
return 2;
};
packagedefaultoutputdir = argv[i + 1];
i += 2;
continue;
};
if (cstreqlit(p, "--ww-command-output-collision")) {
if (i + 2 >= argc || packageoutputcollisionbase != nil
|| argv[i + 1][0u64] == 0u8
|| argv[i + 2][0u64] == 0u8) {
cerr("ww test: invalid --ww-command-output-collision\n");
return 2;
};
packageoutputcollisionbase = argv[i + 1];
packageoutputcollisiondir = argv[i + 2];
i += 3;
continue;
};
if (cstreqlit(p, "--ww-package-test")) {
if (i + 9 >= argc) {
cerr("ww test: --ww-package-test needs kind, package, production, internal, external, directory, output, publication, and status\n");
@@ -9067,6 +9247,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (buildproduct) { product.variant = SEP_VARIANT_PRODUCTION; };
product.directoryproduct = true;
product.notests = testproduct && !hasinternal && !hasexternal;
product.buildaction = true;
product.root = -1;
product.variantroot = -1;
product.productionroot = -1;
@@ -9243,6 +9424,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
cerr("ww test: invalid private directory creation\n");
return 2;
};
if ((packageoutputpatherror || packagedefaultoutputdir != nil
|| packageoutputcollisionbase != nil)
&& (!packagebuild || products.len == 0)) {
cerr("ww test: invalid private output preflight\n");
return 2;
};
if (packagecreateworkdir && products.len == 0) {
cerr("ww test: invalid private directory creation\n");
return 2;
@@ -9388,7 +9575,9 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
incs.ptr, workdir,
products.ptr, products.len, packagemode, publishmode,
&packagelinks, emitasm, packagecreateworkdir,
packagecreateoutputdir);
packagecreateoutputdir, packagedefaultoutputdir,
packageoutputpatherror, packageoutputcollisionbase,
packageoutputcollisiondir);
};
let replacement: *u8 = nil;
if (resolved != target) { replacement = resolved; };