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;