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

@@ -25,7 +25,7 @@
static const char *usage =
"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"
" 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"
@@ -34,7 +34,8 @@ static const char *usage =
" 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"
" -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";
@@ -1256,6 +1257,7 @@ struct seppkg {
int ngenerated_targets;
int generated_targetcap;
int failed; /* discovery/compile failure reaches this action */
int action; /* reached by this request's semantic action list */
int test_support; /* compiler-generated -T support package */
int loaded; /* directory membership/name loaded exactly once */
int export_changed; /* staged export differs from committed export */
@@ -1306,6 +1308,7 @@ struct sepproduct {
int variant;
int directory_product;
int no_tests;
int build_action; /* loaded product retained in the action list */
int context;
int root;
int variant_root; /* retained single-unit root outside directory products */
@@ -2847,7 +2850,8 @@ static int
sep_validate_artifact_paths(struct sepgraph *g, const char *scratch)
{
for (int i = 0; i < g->n; i++) {
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
if (g->pkg[i].failed || !g->pkg[i].loaded || !g->pkg[i].action)
continue;
if (g->pkg[i].storage == NULL
&& sep_assign_storage(&g->pkg[i], scratch) < 0) return -1;
}
@@ -2855,9 +2859,11 @@ sep_validate_artifact_paths(struct sepgraph *g, const char *scratch)
do {
changed = 0;
for (int i = 0; i < g->n && !changed; i++) {
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
if (g->pkg[i].failed || !g->pkg[i].loaded
|| !g->pkg[i].action) continue;
for (int j = i + 1; j < g->n; j++) {
if (g->pkg[j].failed || !g->pkg[j].loaded
|| !g->pkg[j].action
|| strcmp(g->pkg[i].storage,
g->pkg[j].storage) != 0)
continue;
@@ -2932,7 +2938,8 @@ sep_validate_workdir_owners(const struct sepgraph *g, const char *scratch)
{
char unit[SEP_ARTIFACT_MAX];
for (int i = 0; i < g->n; i++) {
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
if (g->pkg[i].failed || !g->pkg[i].loaded || !g->pkg[i].action)
continue;
if (sep_fname(g, i, scratch, ".unit.ww", unit, sizeof unit) < 0)
return -1;
if (sep_validate_unit_owner(unit, &g->pkg[i]) < 0) return -1;
@@ -3833,6 +3840,13 @@ sep_clone_for_test(struct sepgraph *g, int original, const char *owner,
p->is_dir = src->is_dir;
p->variant = SEP_VARIANT_TEST_COPY;
p->role = src->role;
/* A test copy is a product-scoped action even when its replaced source
* node is not in the final action closure. Preserve its established
* complete-action locator without relying on a collision with that
* inactive source node during artifact validation. */
p->storage = sep_storage_digest(p);
p->storage_hashed = 1;
if (p->storage == NULL) goto fail;
p->loaded = src->loaded;
p->failed = src->failed;
p->test_support = src->test_support;
@@ -5222,7 +5236,8 @@ sep_validate_request_staging(struct sepgraph *g, const char *scratch, int warm,
".o.new", ".a.new", ".init.unit.new", ".init.s.new",
".init.o.new" };
for (int pi = 0; pi < g->n; pi++) {
if (g->pkg[pi].failed || !g->pkg[pi].loaded) continue;
if (g->pkg[pi].failed || !g->pkg[pi].loaded
|| !g->pkg[pi].action) continue;
for (size_t si = 0; si < nelem(suffix); si++) {
char path[SEP_ARTIFACT_MAX];
if (sep_fname(g, pi, scratch, suffix[si], path,
@@ -5365,14 +5380,16 @@ sep_discard_request_staging(struct sepgraph *g, const char *scratch, int warm,
".init.unit.ww", ".init.s", ".init.o" };
const char **suffix = warm ? warm_suffix : cold_suffix;
int rc = 0;
for (int pi = 0; pi < g->n; pi++)
for (int pi = 0; pi < g->n; pi++) {
if (!g->pkg[pi].action) continue;
for (size_t i = 0; i < nelem(warm_suffix); i++) {
char path[SEP_ARTIFACT_MAX];
if (sep_fname(g, pi, scratch, suffix[i], path,
sizeof path) < 0
|| (unlink(path) != 0 && errno != ENOENT))
rc = -1;
rc = -1;
}
}
for (int i = 0; i < nproducts; i++) {
const char *path[] = { products[i].stage_out,
products[i].stage_publish, products[i].stage_iface,
@@ -5474,6 +5491,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
int require_command, int is_test,
struct sepproduct *products, int nproducts, int emit_asm,
const char *workdir, int create_workdir, const char *create_output_dir,
const char *default_output_dir, int output_path_error,
const char *output_collision_base, const char *output_collision_dir,
char *scratchout, size_t scratchoutsz,
struct sepgraph **graphout)
{
@@ -5927,10 +5946,6 @@ build_one_sep_impl(const char *src, int entry_is_dir,
int root_package = !is_test && nproducts == 1
&& !g->pkg[products[0].root].failed
&& !sep_root_is_command(&g->pkg[products[0].root]);
if (sep_validate_artifact_paths(g, scratch) < 0)
return 1;
if (warm && workdir_exists && sep_validate_workdir_owners(g, scratch) < 0)
return 1;
int *order = calloc((size_t)g->n, sizeof *order);
int *stack = calloc((size_t)g->n, sizeof *stack);
int norder = 0;
@@ -5980,6 +5995,24 @@ build_one_sep_impl(const char *src, int entry_is_dir,
return 1;
}
}
if (!is_test && create_output_dir != NULL) {
int actions = 0;
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
if (g->pkg[root].failed) {
free(stack); free(order);
return 1;
}
products[i].build_action =
sep_root_is_command(&g->pkg[root]);
if (products[i].build_action) actions++;
}
if (actions == 0) {
fputs("ww: no main packages to build\n", stderr);
free(stack); free(order);
return 1;
}
}
if (!g->pkg[products[0].root].failed
&& root_package && publish_package && !emit_asm
&& validate_package_output_path(out) < 0) {
@@ -5989,12 +6022,23 @@ build_one_sep_impl(const char *src, int entry_is_dir,
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
for (int i = 0; i < nproducts; i++) {
int root = products[i].root;
if (!g->pkg[root].failed
if (products[i].build_action && !g->pkg[root].failed
&& sep_topo_visit(g, root, order, &norder, stack, 0) < 0) {
free(stack); free(order); return 1;
}
}
free(stack);
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].action = 0;
for (int oi = 0; oi < norder; oi++) g->pkg[order[oi]].action = 1;
if (sep_validate_artifact_paths(g, scratch) < 0) {
free(order);
return 1;
}
if (warm && workdir_exists
&& sep_validate_workdir_owners(g, scratch) < 0) {
free(order);
return 1;
}
/* Propagate already-known package-load failures through the union before
* acquiring scratch or completion state. Independent sibling roots may
* remain viable for deterministic staging/diagnosis, but any failed product
@@ -6007,11 +6051,34 @@ build_one_sep_impl(const char *src, int entry_is_dir,
}
int viable_product = 0;
for (int i = 0; i < nproducts; i++)
if (!g->pkg[products[i].root].failed) viable_product = 1;
if (products[i].build_action
&& !g->pkg[products[i].root].failed) viable_product = 1;
if (!viable_product) {
free(order);
return 1;
}
if (!is_test && !emit_asm && output_path_error) {
fputs("ww: command output path is too long\n", stderr);
free(order);
return 1;
}
if (!is_test && !emit_asm && output_collision_base != NULL) {
fputs("ww: multiple commands produce output basename ", stderr);
sep_put_quoted(output_collision_base);
fputs(" in directory ", stderr);
sep_put_quoted(output_collision_dir);
fputc('\n', stderr);
free(order);
return 1;
}
if (!is_test && !emit_asm && default_output_dir != NULL && nproducts == 1
&& sep_root_is_command(&g->pkg[products[0].root])) {
fprintf(stderr,
"ww: build output \"%s\" already exists and is a directory\n",
default_output_dir);
free(order);
return 1;
}
if (sep_validate_request_staging(g, scratch, warm, products, nproducts,
root_package, publish_package, emit_asm, is_test) < 0) {
sep_free_product_staging(products, nproducts);
@@ -6023,8 +6090,8 @@ build_one_sep_impl(const char *src, int entry_is_dir,
* output paths, and action closures have passed their pre-tool checks. */
struct sep_created_dirs created_output = {0};
struct sep_created_dirs created_work = {0};
if (create_output_dir != NULL && create_output_dir[0] != '\0'
&& sep_mkdirs(create_output_dir, is_test ? 0777 : 0700,
if (!emit_asm && create_output_dir != NULL && create_output_dir[0] != '\0'
&& sep_mkdirs(create_output_dir, 0777,
&created_output) != 0) {
fprintf(stderr, is_test
? "ww: cannot create test output directory %s\n"
@@ -6704,7 +6771,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
const struct seplinkflags *linkflags, int publish_package,
int require_command, int is_test,
int root_variant, const char *test_package, int emit_asm,
int keepscratch, const char *workdir)
int keepscratch, const char *workdir, const char *create_output_dir,
const char *default_output_dir, int output_path_error)
{
char scratch[PATH_MAX] = {0};
struct sepgraph *g = NULL;
@@ -6717,6 +6785,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
.publish = NULL,
.artifact = NULL,
.variant = root_variant,
.build_action = 1,
.root = -1,
.variant_root = -1,
.support = -1,
@@ -6725,7 +6794,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
product.artifact = "__root";
int r = build_one_sep_impl(src, entry_is_dir, out, objstem,
extra_includes, linkflags, publish_package, require_command, is_test,
&product, 1, emit_asm, workdir, 0, NULL, scratch,
&product, 1, emit_asm, workdir, 0, create_output_dir,
default_output_dir, output_path_error, NULL, NULL, scratch,
sizeof scratch, &g);
sep_graph_free(g);
if (!keepscratch && scratch[0]) {
@@ -6761,7 +6831,9 @@ build_package_tests(const char *src, const char *root_identity,
const char *extra_includes, const char *workdir,
struct sepproduct *products, int nproducts, int is_test,
int publish_package, const struct seplinkflags *linkflags, int emit_asm,
int create_workdir, const char *create_output_dir)
int create_workdir, const char *create_output_dir,
const char *default_output_dir, int output_path_error,
const char *output_collision_base, const char *output_collision_dir)
{
char scratch[PATH_MAX] = {0};
struct sepgraph *g = NULL;
@@ -6770,7 +6842,9 @@ build_package_tests(const char *src, const char *root_identity,
int r = build_one_sep_impl(src, 1, products[0].out,
products[0].out, extra_includes, linkflags, publish_package, 0, is_test,
products, nproducts, emit_asm, workdir, create_workdir,
create_output_dir, scratch, sizeof scratch, &g);
create_output_dir, default_output_dir, output_path_error,
output_collision_base, output_collision_dir,
scratch, sizeof scratch, &g);
sep_graph_free(g);
return r;
}
@@ -6818,6 +6892,39 @@ basename_no_ext(const char *path, char *out, size_t outsz)
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
}
/* 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 '/'. */
static int
build_output_dir(const char *path)
{
struct stat st;
size_t n = strlen(path);
if (n != 0 && path[n - 1] == '/') return 1;
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}
static int
build_output_path(const char *dir, const char *src, char *out, size_t outsz)
{
char base[PATH_MAX];
size_t n = strlen(dir);
int written;
basename_no_ext(src, base, sizeof base);
written = snprintf(out, outsz, "%s%s%s", dir,
n != 0 && dir[n - 1] == '/' ? "" : "/", base);
if (written < 0 || (size_t)written >= outsz) return -1;
return 0;
}
static void
build_import_leaf(const char *identity, char *out, size_t outsz)
{
const char *leaf = strrchr(identity, '.');
leaf = leaf ? leaf + 1 : identity;
snprintf(out, outsz, "%s", leaf);
}
static int
resolve_module(const char *name, const char *incs, char *out, size_t outsz,
int *is_dir)
@@ -7043,6 +7150,7 @@ do_build(int argc, char **argv)
free(incs);
return 2;
}
int add_dot = src == NULL;
if (src == NULL) src = ".";
if (strstr(src, "...") != NULL || next < argc || saw_terminator) {
free(incs);
@@ -7060,11 +7168,32 @@ do_build(int argc, char **argv)
char out[PATH_MAX];
const char *objstem = NULL;
int discard_output = strcmp(outflag, "/dev/null") == 0;
int output_dir = outflag[0] && !discard_output
&& build_output_dir(outflag);
const char *root_identity = !literal && is_dir ? src : NULL;
if (output_dir && is_dir) {
free(incs);
return exec_package_command(argc, argv, src,
add_dot ? NULL : resolved, root_identity, add_dot, 1);
}
const char *create_output_dir = NULL;
int output_path_error = 0;
if (outflag[0] && !discard_output) {
/* -o sets both the binary path and the intermediate stem so
* artifacts land beside the requested output (T3). */
memcpy(out, outflag, strlen(outflag) + 1);
if (output_dir) {
if (build_output_path(outflag, resolved, out,
sizeof out) < 0) {
output_path_error = 1;
basename_no_ext(resolved, out, sizeof out);
}
create_output_dir = outflag;
} else {
memcpy(out, outflag, strlen(outflag) + 1);
}
objstem = out;
} else if (is_dir && root_identity != NULL) {
build_import_leaf(root_identity, out, sizeof out);
} else if (is_dir) {
char tmp[PATH_MAX];
memcpy(tmp, resolved, strlen(resolved) + 1);
@@ -7075,7 +7204,8 @@ do_build(int argc, char **argv)
} else {
basename_no_ext(resolved, out, sizeof out);
}
const char *root_identity = !literal && is_dir ? src : NULL;
const char *default_output_dir = !outflag[0] && build_output_dir(out)
? out : NULL;
if (discard_output) {
char tmpdir[PATH_MAX], tmp[PATH_MAX];
int dn = snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_build_%d", getpid());
@@ -7095,7 +7225,7 @@ do_build(int argc, char **argv)
* library compilation still need request-private product paths. */
int rc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs,
&linkflags, 0, 0, 0, SEP_VARIANT_PRODUCTION, NULL,
emit_asm, 0, workdir);
emit_asm, 0, workdir, NULL, NULL, 0);
int cleanfail = 0;
if (unlink(tmp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr);
@@ -7111,7 +7241,8 @@ do_build(int argc, char **argv)
}
int rc = build_one_sep(resolved, is_dir, root_identity, out, objstem, incs,
&linkflags, outflag[0] != '\0', 0, 0, SEP_VARIANT_PRODUCTION, NULL,
emit_asm, 1, workdir);
emit_asm, 1, workdir, create_output_dir, default_output_dir,
output_path_error);
free(incs);
return rc;
}
@@ -7156,7 +7287,7 @@ do_run(int argc, char **argv)
const char *root_identity = !literal && is_dir ? src : NULL;
int buildrc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs,
&linkflags,
0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL);
0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL, NULL, NULL, 0);
free(incs);
if (buildrc != 0) {
if (unlink(tmp) != 0 && errno != ENOENT)
@@ -7226,6 +7357,10 @@ do_test(int argc, char **argv)
int package_publish = 0;
int package_create_workdir = 0;
const char *package_create_output_dir = NULL;
int package_output_path_error = 0;
const char *package_default_output_dir = NULL;
const char *package_output_collision_base = NULL;
const char *package_output_collision_dir = NULL;
struct seplinkflags package_linkflags = {0};
int afterdash = 0;
const char *request_identity = NULL;
@@ -7296,6 +7431,34 @@ do_test(int argc, char **argv)
return 2;
}
package_create_output_dir = argv[++i];
} else if (strcmp(argv[i],
"--ww-command-output-path-error") == 0) {
if (package_output_path_error) {
fprintf(stderr,
"ww test: invalid --ww-command-output-path-error\n");
return 2;
}
package_output_path_error = 1;
} else if (strcmp(argv[i], "--ww-default-output-dir") == 0) {
if (i + 1 >= argc || package_default_output_dir != NULL
|| argv[i + 1][0] == '\0') {
fprintf(stderr,
"ww test: invalid --ww-default-output-dir\n");
return 2;
}
package_default_output_dir = argv[++i];
} else if (strcmp(argv[i],
"--ww-command-output-collision") == 0) {
if (i + 2 >= argc
|| package_output_collision_base != NULL
|| argv[i + 1][0] == '\0'
|| argv[i + 2][0] == '\0') {
fprintf(stderr,
"ww test: invalid --ww-command-output-collision\n");
return 2;
}
package_output_collision_base = argv[++i];
package_output_collision_dir = argv[++i];
} else if (strcmp(argv[i], "--ww-package-test") == 0) {
if (i + 9 >= argc) {
fprintf(stderr,
@@ -7363,6 +7526,7 @@ do_test(int argc, char **argv)
products[nproducts].directory_product = 1;
products[nproducts].no_tests = test_product
&& !has_internal && !has_external;
products[nproducts].build_action = 1;
products[nproducts].root = -1;
products[nproducts].variant_root = -1;
products[nproducts].production_root = -1;
@@ -7494,6 +7658,12 @@ do_test(int argc, char **argv)
fprintf(stderr, "ww test: invalid private directory creation\n");
return 2;
}
if ((package_output_path_error || package_default_output_dir != NULL
|| package_output_collision_base != NULL)
&& (!package_build || nproducts == 0)) {
fprintf(stderr, "ww test: invalid private output preflight\n");
return 2;
}
if (package_create_workdir && nproducts == 0) {
fprintf(stderr, "ww test: invalid private directory creation\n");
return 2;
@@ -7595,7 +7765,10 @@ do_test(int argc, char **argv)
incs, workdir, products, nproducts,
package_build ? 0 : 1, package_publish,
package_build ? &package_linkflags : NULL, emit_asm,
package_create_workdir, package_create_output_dir);
package_create_workdir, package_create_output_dir,
package_default_output_dir, package_output_path_error,
package_output_collision_base,
package_output_collision_dir);
free(products);
free(incs);
return r;
@@ -7643,7 +7816,7 @@ do_test(int argc, char **argv)
outstem[0] && !discard_output ? outstem : tmp,
incs, NULL, 0, 0, 1,
SEP_VARIANT_PRODUCTION, NULL, emit_asm,
outstem[0] && !discard_output ? 1 : 0, workdir);
outstem[0] && !discard_output ? 1 : 0, workdir, NULL, NULL, 0);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
@@ -7711,7 +7884,7 @@ do_test(int argc, char **argv)
int br = build_one_sep(target, 0, NULL, outp,
outstem[0] && !discard_output ? outstem : tmp,
incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm,
outstem[0] && !discard_output ? 1 : 0, workdir);
outstem[0] && !discard_output ? 1 : 0, workdir, NULL, NULL, 0);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
@@ -7759,7 +7932,9 @@ do_test(int argc, char **argv)
int r = build_package_tests(target, request_identity, incs, workdir,
products, nproducts, package_build ? 0 : 1,
package_publish, package_build ? &package_linkflags : NULL,
emit_asm, package_create_workdir, package_create_output_dir);
emit_asm, package_create_workdir, package_create_output_dir,
package_default_output_dir, package_output_path_error,
package_output_collision_base, package_output_collision_dir);
free(products);
free(incs);
return r;

View File

@@ -6533,6 +6533,233 @@ runtime cwd/environment/stdin. Build runtime behavior is inapplicable, and
caller-output rollback, occupied caller stages, and output permissions are
inapplicable to the exact discard branch because it creates no public inode.
### 11.30 Implemented single-root build output directories
An explicit `ww build -o OUT` now treats `OUT` as a directory when ordinary
`stat` reports an existing directory or the spelling ends in `/`. The rule is
independent of package count. A selected command directory publishes
`OUT/<requested-import-leaf>` (falling back to the selected local directory
leaf when no contextual identity exists); a raw command-line source publishes
`OUT/<source-basename-without-.ww>`. A missing trailing-slash hierarchy is
created from `0777`, filtered by caller umask, through the existing checked
directory ledger. All roots load first, but independently selected non-main
roots are omitted from the directory branch's action list. A selection
containing no command—including a raw non-main root—rejects as
`ww: no main packages to build` without running a producer or changing the
output directory. After a lone command's default basename is synthesized, an
existing directory at that
basename instead rejects as
`ww: build output "<name>" already exists and is a directory`; a non-main
package has no default public output and is unaffected.
#### Pinned Go evidence and fact classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `runBuild` completes package loading and package-error checking before output
handling at
[`cmd/go/internal/work/build.go`, lines 459471](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L471).
After synthesizing a lone `main` package's default output at lines 473478,
its output branch classifies an existing directory through
`os.Stat`, or a spelling ending in `/` or the host path separator, at
[lines 508518](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L508-L518).
It then creates install actions only for packages named `main`, targets each
at the output directory joined with `DefaultExecName`, rejects an empty
command action list as `no main packages to build`, and executes that graph
at
[lines 519535](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L519-L535).
The non-directory single-output branch is separate at
[lines 537548](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L537-L548).
- `DefaultExecName` uses the final import-path element for a directory package
and the source basename for command-line files
([`cmd/go/internal/load/pkg.go`, lines 17271769](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/load/pkg.go#L1727-L1769)).
- `BuildInstallFunc` creates the target parent before installation, and
`Shell.Mkdir` implements that operation as `os.MkdirAll(dir, 0777)`
([`cmd/go/internal/work/exec.go`, lines 19752000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1975-L2000),
[`cmd/go/internal/work/shell.go`, lines 283301](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L283-L301)).
- Official
[`build_output.txt`, lines 1029 and 4145](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output.txt#L10-L45)
asserts a raw source's default basename, a missing trailing-slash directory,
and an existing directory destination.
- Official
[`build_multi_main.txt`, lines 116](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_multi_main.txt#L1-L16)
asserts command fan-out beneath one directory, no-main rejection, the
implicit-default-existing-directory error, and raw command-line-source
placement beneath an existing output directory.
Load-before-output ordering, `stat`/separator classification, command-only
installation, import-leaf naming, destination joining, no-main rejection, and
the `0777` parent-creation request are directly implemented by the pinned
source. Raw-source basename and existing/missing directory behavior are
directly asserted by official testdata. The resulting parent mode after umask
is derived from the pinned `MkdirAll` call. Application to
exactly one directory package is derived from `runBuild`: the directory branch
tests output form, not package cardinality, while the cardinality check exists
only in the later non-directory branch. These facts apply to WW's local
manifest-free command and raw-source products without importing Go's module,
cache, registry, distribution, or network behavior. The installed host Go
version is not authority.
#### Direct pre-fix measurements
Fresh public Cstage (`out/bin/ww`) and WWstage (`out/bin/ww_ww`) probes measured
the same divergence before production edits:
- one command directory plus an existing output directory spelled without `/`
returned 0, renamed the caller's directory to a PID-bearing transaction
backup, installed a 4,268-byte ELF executable at the directory pathname, and
diagnosed failure to remove the nonempty backup;
- the same existing directory spelled with `/` returned 1 with
`ww: cannot preserve transaction destination .../`;
- a missing nested spelling ending in `/` returned 1 while trying to acquire
`<OUT>/.sepwork` before the hierarchy existed;
- a raw `main.ww` plus an existing trailing-slash destination failed through
the same transaction-destination path instead of producing `OUT/main`; and
- a non-main directory plus an existing directory spelled without `/` returned
0, replaced the directory pathname by an archive, wrote its `.wwi` beside
that pathname, and stranded the former directory as a transaction backup;
and
- for both a command directory and a raw command-line source, an existing
directory at the synthesized default basename was renamed to a transaction
backup and replaced by the executable; both stages returned 0 and diagnosed
inability to remove the deliberately nonempty backup.
Completion review measured four additional stage-identical pre-completion
behaviors before their production edits. A mixed command/non-main request
compiled the independent non-main root in both stages; a raw non-main root
returned 0 and published an archive plus `.wwi`; a contextual `alias` symlink
to physical directory `physical` published `OUT/physical`; and an exactly
arranged umask `000` produced newly created output parents with mode `0700`.
Pinned Go instead loads then omits the independent non-main action, rejects the
raw no-command selection, uses the requested import leaf, and requests parent
mode `0777`. The preserved traces and stat results are in the session evidence
ledger.
Final review then measured four stage-identical load-precedence leaks before
the completion edit. A non-main root with a missing import reported only
`no main packages`; a command with a missing import and an overlong derived
directory destination reported only the path error; two colliding command
basenames, one with a missing import, reported only the duplicate-destination
error; and a recursive command with a missing import plus an implicit default
directory collision reported only the collision. Pinned `runBuild` lines
470471 load and check all selected packages before any output handling at
lines 473548. Missing-package diagnostics therefore precede no-main,
derived-path, duplicate-destination, and implicit-default checks. Extending
that boundary to WW's transactional duplicate guard is derived from the pinned
ordering because the guard is WW-local output preflight. The exact probes and
outputs are preserved in the evidence ledger.
Those are directly measured WW facts. Both drivers interpreted every non-null
single-root `-o` as one file and object stem. The compiler, assembler, linker,
archive writer, and shared package coordinator were not the cause.
#### Ownership, actions, publication, and identity
`internal/wwpackage.packagecommand` remains the shared package-request output
owner. It classifies output directories, discovers and groups the complete
selection, and passes every selected root plus output-preflight metadata into
the shared separate-build executor. Only after that executor has loaded all
packages and imports does it retain command roots, derive their complete action
closure, and evaluate no-main, path-length, duplicate-destination, and
implicit-default checks. A separate presentation field carries the requested
import leaf (or local-path fallback) through collision preflight and
publication; canonical physical directory metadata remains loader metadata.
The coordinator commits the retained products through one request transaction.
The dispatch part of the gap belonged to the early single-root compatibility
choice in `cmd/ww.do_build` and `selfhost/cmd/ww.dobuild`; completion review
also closed the coordinator's independent-non-main action,
physical-leaf-presentation, and load-precedence leaks.
After ordinary argument parsing and root resolution, both drivers now apply the
same exact Unix classification. A directory root with a directory output enters
the existing package coordinator. An explicit logical root carries its
unchanged logical root identity while the corresponding argument is replaced
by the already-resolved loader route; a default invocation inserts exactly one
`.`; a literal directory remains literal. Thus the shared loader, grouping,
graph, action, output-preflight, and publication rules operate exactly as they
do for a larger request.
The raw-file compatibility route remains driver-owned. It derives the joined
command path, uses that path as the existing direct action's product and stem,
and passes the classified directory to the shared transaction's checked
creation ledger. A bounds failure is carried as preflight metadata so source
and import errors, and raw no-main rejection, retain pinned load-first
precedence before the path diagnostic. It does not manufacture a package
request or change raw-source graph identity. `stat` follows a symlinked output
directory; lexical publication remains beneath the requested symlink spelling.
Exact `/dev/null` is classified first by the completed discard rule and never
enters this directory branch.
For an implicit default, the drivers pass the existing-directory collision as
preflight metadata to the common separate-build executor. The executor waits
until package/import loading, contextual checks, and graph-cycle validation
have established the root action kind. It rejects a command before scratch,
workdir, tool, stage, or destination acquisition, but lets a non-main package
perform its unchanged no-public-output build. This keeps raw and directory
compatibility routes on the same diagnostic-precedence rule without deriving
kind from a path, filename, declared-name guess, or driver-side source scan.
Loading and source/import rejection precede every output-derived rejection,
including no-main, derived-path length, duplicate destination, and implicit
default collision; all precede output creation. A selection with no command
rejects after full package/import loading and graph validation but before
compiler, assembler, linker, or directory mutation. In a mixed request,
independently selected non-main roots have no action; a non-main package
reachable as a command dependency still performs its ordinary semantic action.
Repeated exact roots retain canonical graph/action deduplication. Successful
commands compile, assemble, archive, and link normally; `-S -w` remains
action-only and publishes no command. Directory form still selects only
commands and therefore retains no-main rejection, but destination length,
duplicate publication names, implicit destination collision, and output-parent
creation belong to the install action that `-S` never reaches. They are
inapplicable to that assembly-only request. An explicit external workdir
prevents an adjacent `<command>.sepwork`; without `-w`, that established WW
scratch tree remains an ordinary retained build artifact beneath the output
directory.
Existing destination contents and symlink targets survive successful
publication. Compiler failure, linker failure, or linker signal preserves the
prior command and every committed persistent artifact, removes `.new` and
transaction stages, and rolls back only directory prefixes created for the
failed request. Every caller-output prefix is requested as `0777` and filtered
once by the caller umask; persistent/private work directories keep their
separate modes. The raw route has the same missing-directory rollback through
the shared creation ledger. Independent Cstage and WWstage processes use
disjoint output, work, stage, and process ownership and may complete
concurrently. `ww build` itself has no runtime action; direct execution of the
published command verifies its ordinary program exit result, while runtime
failure/timeout policy remains owned by `ww run` and `ww test`.
This is output disposition and dispatch only. Dotted package/import identity,
declared package name, physical source directory metadata, file-local import
bindings, graph edges, action and storage keys, symbols, `.wwi`, compiler,
assembler, archive and linker semantic inputs, artifact bytes, invalidation,
and public-file output-mode formula are unchanged. Correcting caller-output
parent creation metadata does not enter any semantic identity. There is no
persisted-byte contract change: build workdir format remains `18`, test
workdir format remains `19`, and semantic storage remains `3`.
The WW-native owner `single_root_build_output_directory` covers literal,
logical, default-dot, symlinked, raw-file, explicit existing/missing, and
implicit-default existing-directory forms in both stages; basename selection
and directory-content preservation; logical alias versus physical-leaf
separation; command rejection, raw no-main rejection, non-main no-output
behavior, long raw-output and load-error precedence; mixed command-only and
repeated roots; skipped-root import rejection; and no-main, directory-derived
path, duplicate-destination, and recursive implicit-collision precedence with
zero tool activity; exact `0777` missing-parent creation under umask `000`;
the already-aligned retained-test control; `-S -w` command selection with no
install-only preflight or output-directory creation; cold, warm,
and invalidated persistence; exact compiler/assembler/linker action traces;
successful program exit results; compiler and linker failure; linker signal;
prior-state and newly-created-directory rollback; `.new` and transaction
cleanup; concurrent stage isolation; executable and semantic-artifact bytes;
and exact diagnostic parity. Existing owners continue to cover public-file
output umasks, occupied stages, generalized multi-product transactions, test
runtime failure and timeout, null discard, and broader package/import graph
matrices.
## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -310,6 +310,35 @@ ImportPath = ident { "." ident } .
`fn main`; path and directory spelling do not classify commands. An ordinary
import of a package declared `main` is rejected, except for the toolchain's
colocated external-test wiring.
- For `ww build`, an explicit `-o` names an output directory when ordinary
`stat` reports an existing directory (following symlinks) or its spelling
ends in `/`. This classification is independent of whether one or many
package roots were requested. Each selected command is published beneath
that directory using the final component of its requested contextual import
name, with the selected local directory leaf as the fallback when no such
identity exists. Independently selected non-main roots load but do not enter
the action list; non-main command dependencies retain ordinary actions, and
a selection containing no command rejects before tools or output creation.
A raw command-line `.ww` source uses its source basename without `.ww`, and
a raw non-main source is the same no-command rejection. Missing
trailing-slash hierarchies are created transactionally from `0777`, filtered
by the caller's umask.
Loading and package/import diagnostics precede no-main, derived-path,
duplicate-destination, implicit-default, and other output preflight; all of
those checks precede creation. Producer failure or interruption preserves
existing directory contents and removes only request-created prefixes and
stages. A non-directory output retains the single-product file/archive rule.
If a lone
command's synthesized default basename already names a directory, loading
and graph validation complete and the build rejects before tools without
changing that directory; a non-main package synthesizes no default public
output. Output paths and directory metadata never become package, import,
graph, action,
symbol, artifact, `.wwi`, or persistence identity.
Assembly-only `-S` retains the directory form's command-action selection and
no-main rejection, but it reaches no install action: destination length,
duplicate publication names, implicit destination collision, and output
parent creation are therefore inapplicable.
- A newly published `ww build` command is created with permission `0777`
filtered by the invoking process's umask. A newly published non-command
archive, and the adjacent WW interface required to consume it, use `0666`

View File

@@ -234,6 +234,28 @@ package with no selected test source validates production
but publishes nothing and does not create an otherwise unneeded output
directory. Successful `-c` output is silent apart from no-test reporting.
The package suite also pins the adjacent `ww build` output-directory branch.
After normal loading, an existing directory (including a symlink to one) or an
explicit spelling ending in `/` receives each command under its requested
import-leaf name (or local directory-leaf fallback) even for exactly one
selected root; a raw `.ww` command uses its source basename. A missing
trailing-slash hierarchy is created from `0777`, filtered by umask, by the
request transaction. A directory or raw no-main-only selection and an import
failure run no tools and do not create output. Independently selected non-main
siblings in a mixed request load but have no action; command dependencies still
build. Package/import rejection precedes no-main, derived destination length,
duplicate command basename, and implicit default-directory preflight. If the
synthesized default command basename already exists as a directory,
load/graph validation precedes a zero-tool rejection and preserves that
directory; a non-main package has no corresponding default output.
Persistent work reuse, invalidation, failures, interruption, and
concurrent drivers retain the same action and rollback rules; output form does
not enter package/import/action identity. This build branch starts no runtime
process, while direct execution of its published binary remains an artifact
check rather than part of `ww build`. Assembly-only `-S` still selects command
actions and rejects a no-command directory request, but it performs no
install-only destination validation or output-directory creation.
The retained executable is byte-identical to the temporary runnable and has
executable mode `0777` filtered by the caller's umask, but it is never the path
executed by the coordinator. Publication participates in the driver's one
@@ -312,6 +334,24 @@ precede those coordinator-owned source-validation parses.
Separate compilation is the only driver build path; no compatibility mode
switch remains.
`test/package/package_test.ww` owns the focused dual-stage
`single_root_build_output_directory` observer. It covers existing and missing
directory spellings; trailing and non-trailing forms; literal, logical,
default-dot, symlink, raw-file, and implicit-default routes; build/test
adjacency; mixed and repeated roots; command-only publication, no-main and
implicit-collision rejection, non-main default behavior; import
diagnostic precedence over no-main, derived-path, duplicate-destination, and
recursive implicit-collision checks; `-S -w` command selection with long,
duplicate, implicit, and missing-directory publication preflight omitted;
cold, warm, and invalidated persistence;
compiler, assembler, and linker action traces; compiler/linker failure and
linker signal; prior-state and directory-creation rollback; concurrent
isolation; executable modes and runtime exit values; `.new`/transaction
cleanup; and Cstage/WWstage diagnostic, binary, assembly, and persistent
semantic-artifact byte parity. General output permissions, occupied stages,
multi-product transactions, null discard, and test runtime timeout remain with
their existing observers.
`test/package/package_test.ww` also owns the dual-stage
`declared_name_identity_and_file_import_scope` and
`explicit_import_alias_binding_modes` observers. They generate temporary

View File

@@ -24,6 +24,7 @@ type pkgfolder = struct {
type pkggroup = struct {
dir: str,
pkg: str,
basename: str,
testname: str,
publish: str,
prodpkg: str,
@@ -58,6 +59,11 @@ type pkgplan = struct {
buildonly: bool,
publish: bool,
emitasm: bool,
outputpatherror: bool,
defaultoutputdir: str,
outputcollisionbase: str,
outputcollisiondir: str,
suppressbuildreports: bool,
};
def PKG_COUNT_MAX: i32 = 2147483647;
@@ -1368,8 +1374,9 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32,
return false;
};
if (buildonly) {
if (outputdir && strings.compare(g.pkg, "main") == 0) {
if (!pkgjoinpath(outname, pkgbase(g.dir), &g.bin)) { return false; };
if (outputdir && strings.compare(g.pkg, "main") == 0
&& !p.outputpatherror && !p.emitasm) {
if (!pkgjoinpath(outname, g.basename, &g.bin)) { return false; };
} else if (outname.len != 0 && !outputdir) { g.bin = outname; }
else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; };
} else {
@@ -1416,7 +1423,7 @@ fn pkgreportcommand(fd: i32, kind: str, g: *pkggroup,
fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
libdirs: []str, libs: []str, h: *exec.process) bool = {
let nproducts: i32 = p.end - p.start;
let capacity: i32 = 16;
let capacity: i32 = 24;
if (nproducts < 0 || nproducts > (PKG_COUNT_MAX - capacity) / 10) {
pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large");
return false;
@@ -1457,6 +1464,18 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
append(ba, "--ww-create-output-dir");
append(ba, p.outputdir);
};
if (p.outputpatherror) {
append(ba, "--ww-command-output-path-error");
};
if (p.defaultoutputdir.len != 0) {
append(ba, "--ww-default-output-dir");
append(ba, p.defaultoutputdir);
};
if (p.outputcollisionbase.len != 0) {
append(ba, "--ww-command-output-collision");
append(ba, p.outputcollisionbase);
append(ba, p.outputcollisiondir);
};
if (p.identity.len != 0) {
append(ba, "--ww-root-identity");
append(ba, p.identity);
@@ -1627,8 +1646,10 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup,
let i: i32 = p.start;
for (i < p.end) {
if (!pkgproductbuilt(&groups[i], buildonly)) {
pkgreportcommand(os.STDERR_FILENO, "build", &groups[i],
&p.buildres);
if (!p.suppressbuildreports) {
pkgreportcommand(os.STDERR_FILENO, "build", &groups[i],
&p.buildres);
};
failed += 1;
} else if (buildonly) {
void;
@@ -2165,6 +2186,7 @@ export fn packagecommand(args: []str) int = {
g.dir = f.path;
g.pkg = family;
if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; };
g.basename = "";
g.testname = "";
g.publish = "";
g.prodpkg = f.prodpkg;
@@ -2193,57 +2215,48 @@ export fn packagecommand(args: []str) int = {
return 1;
};
pkgsortgroups(groups);
i = 0;
for (i < groups.len) {
groups[i].basename = pkgbase(groups[i].dir);
if (roots.len == 1 && !anyrecurse && requestidentity.len != 0) {
groups[i].basename = pkgimportbase(requestidentity);
};
i += 1;
};
let defaultout: bool = false;
if (buildonly && !buildnull && outname.len == 0 && folders.len == 1
&& groups.len == 1 && strings.compare(groups[0].pkg, "main") == 0) {
outname = pkgbase(groups[0].dir);
outname = groups[0].basename;
defaultout = true;
};
let defaultoutputdir: str = "";
if (defaultout && pkgstatdir(outname)) {
pkgput(os.STDERR_FILENO, "ww: build output \"");
pkgput(os.STDERR_FILENO, outname);
pkgputln(os.STDERR_FILENO, "\" already exists and is a directory");
return 1;
defaultoutputdir = outname;
};
let outputdir: bool = buildonly && explicitout && !buildnull
&& pkgoutputdir(outname);
let outputpatherror: bool = false;
let outputcollisionbase: str = "";
let outputcollisiondir: str = "";
if (outputdir) {
let mainpackages: i32 = 0;
i = 0;
for (i < groups.len) {
if (strings.compare(groups[i].pkg, "main") == 0) {
mainpackages += 1;
};
i += 1;
};
if (mainpackages == 0) {
pkgputln(os.STDERR_FILENO, "ww: no main packages to build");
return 1;
};
i = 0;
for (i < groups.len) {
if (strings.compare(groups[i].pkg, "main") == 0) {
let slash: i64 = 1i64;
if (strings.hassuffix(outname, "/")) { slash = 0i64; };
let outputlen: i64 = outname.len: i64 + slash
+ pkgbase(groups[i].dir).len: i64;
+ groups[i].basename.len: i64;
if (outputlen + 1i64 > os.PATH_MAX: i64) {
pkgputln(os.STDERR_FILENO,
"ww: command output path is too long");
return 1;
outputpatherror = true;
};
let j: i32 = 0;
for (j < i) {
if (strings.compare(groups[j].pkg, "main") == 0
&& strings.compare(pkgbase(groups[j].dir),
pkgbase(groups[i].dir)) == 0) {
pkgput(os.STDERR_FILENO,
"ww: multiple commands produce output basename ");
pkgputquoted(os.STDERR_FILENO, pkgbase(groups[i].dir));
pkgput(os.STDERR_FILENO, " in directory ");
pkgputquoted(os.STDERR_FILENO, outname);
pkgputln(os.STDERR_FILENO, "");
return 1;
&& strings.compare(groups[j].basename,
groups[i].basename) == 0
&& outputcollisionbase.len == 0) {
outputcollisionbase = groups[i].basename;
outputcollisiondir = outname;
};
j += 1;
};
@@ -2273,11 +2286,9 @@ export fn packagecommand(args: []str) int = {
};
i = 0;
for (i < groups.len) {
let leaf: str = pkgbase(groups[i].dir);
if (roots.len == 1 && !anyrecurse && requestidentity.len != 0) {
leaf = pkgimportbase(requestidentity);
if (!pkgstring(&groups[i].testname, groups[i].basename, ".test")) {
return 1;
};
if (!pkgstring(&groups[i].testname, leaf, ".test")) { return 1; };
groups[i].publish = "";
if (testretain && !groups[i].notests && !testnull) {
if (!explicitout) {
@@ -2368,6 +2379,12 @@ export fn packagecommand(args: []str) int = {
&& groups.len == 1
&& strings.compare(groups[0].pkg, "main") != 0;
plan.emitasm = emitasm;
plan.outputpatherror = outputpatherror;
plan.defaultoutputdir = defaultoutputdir;
plan.outputcollisionbase = outputcollisionbase;
plan.outputcollisiondir = outputcollisiondir;
plan.suppressbuildreports = buildonly
&& (outputdir || defaultoutputdir.len != 0);
append(plans, plan);
let createdir: str = "";
if (buildonly && outputdir) {

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; };

File diff suppressed because it is too large Load Diff