ww: protect public install outputs

This commit is contained in:
2026-08-21 04:11:01 +09:00
parent ce91f6662c
commit 1b71250ad5
7 changed files with 1064 additions and 90 deletions

View File

@@ -1309,6 +1309,7 @@ struct sepproduct {
int directory_product; int directory_product;
int no_tests; int no_tests;
int build_action; /* loaded product retained in the action list */ int build_action; /* loaded product retained in the action list */
int public_out;
int context; int context;
int root; int root;
int variant_root; /* retained single-unit root outside directory products */ int variant_root; /* retained single-unit root outside directory products */
@@ -4983,6 +4984,7 @@ struct septxnentry {
char *backup; char *backup;
int had_old; int had_old;
int installed; int installed;
int public_output;
}; };
struct septxn { struct septxn {
@@ -4991,7 +4993,8 @@ struct septxn {
}; };
static int static int
sep_txn_add(struct septxn *tx, const char *stage, const char *dst) sep_txn_add_mode(struct septxn *tx, const char *stage, const char *dst,
int public_output)
{ {
if (strcmp(stage, dst) == 0) { if (strcmp(stage, dst) == 0) {
fprintf(stderr, "ww: transaction path collision: %s\n", dst); fprintf(stderr, "ww: transaction path collision: %s\n", dst);
@@ -5013,6 +5016,7 @@ sep_txn_add(struct septxn *tx, const char *stage, const char *dst)
e->stage = strdup(stage); e->stage = strdup(stage);
e->dst = strdup(dst); e->dst = strdup(dst);
e->backup = sep_sprintf("%s.wwtxn.%ld.old", dst, (long)getpid()); e->backup = sep_sprintf("%s.wwtxn.%ld.old", dst, (long)getpid());
e->public_output = public_output;
if (e->stage == NULL || e->dst == NULL || e->backup == NULL) { if (e->stage == NULL || e->dst == NULL || e->backup == NULL) {
sep_fail_nomem(); sep_fail_nomem();
free(e->backup); free(e->dst); free(e->stage); free(e->backup); free(e->dst); free(e->stage);
@@ -5029,6 +5033,18 @@ sep_txn_add(struct septxn *tx, const char *stage, const char *dst)
return 0; return 0;
} }
static int
sep_txn_add(struct septxn *tx, const char *stage, const char *dst)
{
return sep_txn_add_mode(tx, stage, dst, 0);
}
static int
sep_txn_add_public(struct septxn *tx, const char *stage, const char *dst)
{
return sep_txn_add_mode(tx, stage, dst, 1);
}
static void static void
sep_txn_discard(struct septxn *tx) sep_txn_discard(struct septxn *tx)
{ {
@@ -5049,8 +5065,87 @@ sep_txn_free(struct septxn *tx)
memset(tx, 0, sizeof *tx); memset(tx, 0, sizeof *tx);
} }
struct sep_output_magic {
const unsigned char *bytes;
size_t n;
};
#define SEP_OUTPUT_MAGIC(s) { (const unsigned char *)(s), sizeof(s) - 1 }
/* Go 1.26.5 work.objectMagic. WW interfaces are an additional public output
* kind and always begin with the compiler-owned module directive. */
static const struct sep_output_magic sep_output_magic[] = {
SEP_OUTPUT_MAGIC("!<arch>\n"),
SEP_OUTPUT_MAGIC("<bigaf>\n"),
SEP_OUTPUT_MAGIC("\x7f" "ELF"),
SEP_OUTPUT_MAGIC("\xfe\xed\xfa\xce"),
SEP_OUTPUT_MAGIC("\xfe\xed\xfa\xcf"),
SEP_OUTPUT_MAGIC("\xce\xfa\xed\xfe"),
SEP_OUTPUT_MAGIC("\xcf\xfa\xed\xfe"),
SEP_OUTPUT_MAGIC("\x4d\x5a\x90\x00\x03\x00"),
SEP_OUTPUT_MAGIC("\x4d\x5a\x78\x00\x01\x00"),
SEP_OUTPUT_MAGIC("\x00\x00\x01\xeb"),
SEP_OUTPUT_MAGIC("\x00\x00\x8a\x97"),
SEP_OUTPUT_MAGIC("\x00\x00\x06\x47"),
SEP_OUTPUT_MAGIC("\x00\x61\x73\x6d"),
SEP_OUTPUT_MAGIC("\x01\xdf"),
SEP_OUTPUT_MAGIC("\x01\xf7"),
SEP_OUTPUT_MAGIC("//ww:module "),
};
#undef SEP_OUTPUT_MAGIC
static int
sep_is_object_output(const char *path)
{
unsigned char buf[64] = {0};
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
size_t got = 0;
int bad = 0;
while (got < sizeof buf) {
ssize_t n = read(fd, buf + got, sizeof buf - got);
if (n > 0) got += (size_t)n;
else if (n == 0) break;
else if (errno != EINTR) { bad = 1; break; }
}
(void)close(fd);
if (bad) return 0;
for (size_t i = 0; i < nelem(sep_output_magic); i++)
if (got >= sep_output_magic[i].n
&& memcmp(buf, sep_output_magic[i].bytes,
sep_output_magic[i].n) == 0)
return 1;
return 0;
}
/* Pinned Go's checkDstOverwrite follows symlinks with stat and protects only
* directories and nonempty regular non-object files. Other file kinds remain
* eligible for the install operation itself to replace. */
static int
sep_check_dst_overwrite(const char *dst)
{
struct stat st;
if (stat(dst, &st) != 0) return 0;
if (S_ISDIR(st.st_mode)) {
fputs("ww: build output ", stderr);
sep_put_quoted(dst);
fputs(" already exists and is a directory\n", stderr);
return -1;
}
if (S_ISREG(st.st_mode) && st.st_size != 0
&& !sep_is_object_output(dst)) {
fputs("ww: build output ", stderr);
sep_put_quoted(dst);
fputs(" already exists and is not an object file\n", stderr);
return -1;
}
return 0;
}
/* One request-wide rollback group: producers and linkers finish first; only /* One request-wide rollback group: producers and linkers finish first; only
* then are old destinations parked and all staged files installed. */ * then are public destinations checked, old destinations parked, and all
* staged files installed. */
static int static int
sep_txn_commit(struct septxn *tx) sep_txn_commit(struct septxn *tx)
{ {
@@ -5060,6 +5155,10 @@ sep_txn_commit(struct septxn *tx)
tx->v[i].stage); tx->v[i].stage);
goto rollback; goto rollback;
} }
for (int i = 0; i < tx->n; i++)
if (tx->v[i].public_output
&& sep_check_dst_overwrite(tx->v[i].dst) != 0)
goto rollback;
for (int i = 0; i < tx->n; i++) for (int i = 0; i < tx->n; i++)
if (path_exists_nofollow(tx->v[i].backup) != 0) { if (path_exists_nofollow(tx->v[i].backup) != 0) {
fprintf(stderr, "ww: transaction backup already exists: %s\n", fprintf(stderr, "ww: transaction backup already exists: %s\n",
@@ -5476,6 +5575,52 @@ sep_mkdirs(const char *path, mode_t mode, struct sep_created_dirs *created)
return 0; return 0;
} }
/* A retained running test is built and executed from request-private storage.
* Its Go-like install action runs only after that test succeeds, but reuses
* the same guarded publisher as build and compile-only test products. */
static int
sep_install_test_output(const char *stage, const char *dst)
{
char *install_stage = sep_sprintf("%s.install", stage);
if (install_stage == NULL) return 1;
if (strlen(install_stage) + 1 > (size_t)PATH_MAX
|| path_exists_nofollow(install_stage) != 0
|| copy_executable_stage(stage, install_stage) != 0) {
fputs("ww: cannot stage retained test output\n", stderr);
free(install_stage);
return 1;
}
char *parent = sep_parent_path(dst);
if (parent == NULL) {
(void)unlink(install_stage);
free(install_stage);
return 1;
}
struct sep_created_dirs created = {0};
if (sep_mkdirs(parent, 0777, &created) != 0) {
fprintf(stderr, "ww: cannot create test output directory %s\n",
parent);
free(parent);
(void)unlink(install_stage);
free(install_stage);
return 1;
}
free(parent);
struct septxn tx = {0};
if (sep_txn_add_public(&tx, install_stage, dst) != 0
|| sep_txn_commit(&tx) != 0) {
sep_txn_discard(&tx);
sep_txn_free(&tx);
sep_rollback_dirs(&created);
(void)unlink(install_stage);
free(install_stage);
return 1;
}
sep_txn_free(&tx);
free(install_stage);
return 0;
}
/* build_sep_plan — discover dependencies for every requested product in one /* build_sep_plan — discover dependencies for every requested product in one
* package universe, compile the dependency-first union once, then link each * package universe, compile the dependency-first union once, then link each
* root from its own complete reachable archive closure. The dependency-first * root from its own complete reachable archive closure. The dependency-first
@@ -6713,11 +6858,14 @@ prepare_transaction:
} }
for (int i = 0; i < nproducts; i++) { for (int i = 0; i < nproducts; i++) {
if (products[i].stage_out != NULL if (products[i].stage_out != NULL
&& sep_txn_add(&tx, products[i].stage_out, && (products[i].public_out
products[i].out) < 0) ? sep_txn_add_public(&tx, products[i].stage_out,
products[i].out)
: sep_txn_add(&tx, products[i].stage_out,
products[i].out)) < 0)
goto request_fail; goto request_fail;
if (products[i].stage_publish != NULL if (products[i].stage_publish != NULL
&& sep_txn_add(&tx, products[i].stage_publish, && sep_txn_add_public(&tx, products[i].stage_publish,
products[i].publish) < 0) products[i].publish) < 0)
goto request_fail; goto request_fail;
if (products[i].stage_iface != NULL) { if (products[i].stage_iface != NULL) {
@@ -6725,7 +6873,7 @@ prepare_transaction:
int on = snprintf(outiface, sizeof outiface, "%s.wwi", int on = snprintf(outiface, sizeof outiface, "%s.wwi",
products[i].out); products[i].out);
if (on < 0 || (size_t)on >= sizeof outiface if (on < 0 || (size_t)on >= sizeof outiface
|| sep_txn_add(&tx, products[i].stage_iface, || sep_txn_add_public(&tx, products[i].stage_iface,
outiface) < 0) outiface) < 0)
goto request_fail; goto request_fail;
} }
@@ -6771,7 +6919,8 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
const struct seplinkflags *linkflags, int publish_package, const struct seplinkflags *linkflags, int publish_package,
int require_command, int is_test, int require_command, int is_test,
int root_variant, const char *test_package, int emit_asm, int root_variant, const char *test_package, int emit_asm,
int keepscratch, const char *workdir, const char *create_output_dir, int public_output, int keepscratch, const char *workdir,
const char *create_output_dir,
const char *default_output_dir, int output_path_error) const char *default_output_dir, int output_path_error)
{ {
char scratch[PATH_MAX] = {0}; char scratch[PATH_MAX] = {0};
@@ -6786,6 +6935,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
.artifact = NULL, .artifact = NULL,
.variant = root_variant, .variant = root_variant,
.build_action = 1, .build_action = 1,
.public_out = public_output,
.root = -1, .root = -1,
.variant_root = -1, .variant_root = -1,
.support = -1, .support = -1,
@@ -7225,7 +7375,7 @@ do_build(int argc, char **argv)
* library compilation still need request-private product paths. */ * library compilation still need request-private product paths. */
int rc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, int rc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs,
&linkflags, 0, 0, 0, SEP_VARIANT_PRODUCTION, NULL, &linkflags, 0, 0, 0, SEP_VARIANT_PRODUCTION, NULL,
emit_asm, 0, workdir, NULL, NULL, 0); emit_asm, 0, 0, workdir, NULL, NULL, 0);
int cleanfail = 0; int cleanfail = 0;
if (unlink(tmp) != 0 && errno != ENOENT) { if (unlink(tmp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
@@ -7241,7 +7391,7 @@ do_build(int argc, char **argv)
} }
int rc = build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, int rc = build_one_sep(resolved, is_dir, root_identity, out, objstem, incs,
&linkflags, outflag[0] != '\0', 0, 0, SEP_VARIANT_PRODUCTION, NULL, &linkflags, outflag[0] != '\0', 0, 0, SEP_VARIANT_PRODUCTION, NULL,
emit_asm, 1, workdir, create_output_dir, default_output_dir, emit_asm, 1, 1, workdir, create_output_dir, default_output_dir,
output_path_error); output_path_error);
free(incs); free(incs);
return rc; return rc;
@@ -7287,7 +7437,7 @@ do_run(int argc, char **argv)
const char *root_identity = !literal && is_dir ? src : NULL; const char *root_identity = !literal && is_dir ? src : NULL;
int buildrc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs, int buildrc = build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs,
&linkflags, &linkflags,
0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL, NULL, NULL, 0); 0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, 0, NULL, NULL, NULL, 0);
free(incs); free(incs);
if (buildrc != 0) { if (buildrc != 0) {
if (unlink(tmp) != 0 && errno != ENOENT) if (unlink(tmp) != 0 && errno != ENOENT)
@@ -7334,6 +7484,9 @@ do_run(int argc, char **argv)
static int static int
do_test(int argc, char **argv) do_test(int argc, char **argv)
{ {
if (argc == 3
&& strcmp(argv[0], "--ww-install-test-output") == 0)
return sep_install_test_output(argv[1], argv[2]);
const char *src = NULL; const char *src = NULL;
struct sepproduct *products = NULL; struct sepproduct *products = NULL;
int nproducts = 0, productcap = 0; int nproducts = 0, productcap = 0;
@@ -7475,7 +7628,10 @@ do_test(int argc, char **argv)
const char *publish = argv[++i]; const char *publish = argv[++i];
const char *status = argv[++i]; const char *status = argv[++i];
size_t pn = strlen(name); size_t pn = strlen(name);
int build_product = strcmp(kind, "build") == 0; int build_product = strcmp(kind, "build") == 0
|| strcmp(kind, "build-public") == 0;
int public_build_product =
strcmp(kind, "build-public") == 0;
int test_product = strcmp(kind, "test") == 0; int test_product = strcmp(kind, "test") == 0;
int has_production = strcmp(production, "-") != 0; int has_production = strcmp(production, "-") != 0;
int has_internal = strcmp(internal, "-") != 0; int has_internal = strcmp(internal, "-") != 0;
@@ -7527,6 +7683,7 @@ do_test(int argc, char **argv)
products[nproducts].no_tests = test_product products[nproducts].no_tests = test_product
&& !has_internal && !has_external; && !has_internal && !has_external;
products[nproducts].build_action = 1; products[nproducts].build_action = 1;
products[nproducts].public_out = public_build_product;
products[nproducts].root = -1; products[nproducts].root = -1;
products[nproducts].variant_root = -1; products[nproducts].variant_root = -1;
products[nproducts].production_root = -1; products[nproducts].production_root = -1;
@@ -7788,9 +7945,12 @@ do_test(int argc, char **argv)
} }
char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX]; char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX];
const char *outp; const char *outp;
int owntmp = (!outstem[0] || discard_output) && !workdir[0]; int retain_output = outstem[0] && !discard_output;
if (outstem[0] && !discard_output) outp = outstem; int deferred_install = retain_output && !compileonly && !emit_asm;
else if (workdir[0]) { int owntmp = deferred_install
|| ((!outstem[0] || discard_output) && !workdir[0]);
if (retain_output && !deferred_install) outp = outstem;
else if (workdir[0] && !deferred_install) {
/* The workdir owns the persistent test binary the same /* The workdir owns the persistent test binary the same
* way it owns the package artifacts. */ * way it owns the package artifacts. */
int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir); int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir);
@@ -7816,7 +7976,8 @@ do_test(int argc, char **argv)
outstem[0] && !discard_output ? outstem : tmp, outstem[0] && !discard_output ? outstem : tmp,
incs, NULL, 0, 0, 1, incs, NULL, 0, 0, 1,
SEP_VARIANT_PRODUCTION, NULL, emit_asm, SEP_VARIANT_PRODUCTION, NULL, emit_asm,
outstem[0] && !discard_output ? 1 : 0, workdir, NULL, NULL, 0); retain_output && !deferred_install, retain_output ? 1 : 0,
workdir, NULL, NULL, 0);
if (br != 0) { if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT) if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
@@ -7837,6 +7998,9 @@ do_test(int argc, char **argv)
return cleanfail ? 1 : 0; return cleanfail ? 1 : 0;
} }
int rc = run_test_bin(outp, pattern); int rc = run_test_bin(outp, pattern);
if (rc == 0 && deferred_install
&& sep_install_test_output(outp, outstem) != 0)
rc = 1;
if (owntmp && unlink(outp) != 0 && errno != ENOENT) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1; if (rc == 0) rc = 1;
@@ -7860,9 +8024,12 @@ do_test(int argc, char **argv)
} }
char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX]; char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX];
const char *outp; const char *outp;
int owntmp = (!outstem[0] || discard_output) && !workdir[0]; int retain_output = outstem[0] && !discard_output;
if (outstem[0] && !discard_output) outp = outstem; int deferred_install = retain_output && !compileonly && !emit_asm;
else if (workdir[0]) { int owntmp = deferred_install
|| ((!outstem[0] || discard_output) && !workdir[0]);
if (retain_output && !deferred_install) outp = outstem;
else if (workdir[0] && !deferred_install) {
int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir); int tn = snprintf(tmp, sizeof tmp, "%s/main", workdir);
if (tn < 0 || (size_t)tn >= sizeof tmp) { if (tn < 0 || (size_t)tn >= sizeof tmp) {
fputs("ww: workdir path is too long\n", stderr); fputs("ww: workdir path is too long\n", stderr);
@@ -7884,7 +8051,8 @@ do_test(int argc, char **argv)
int br = build_one_sep(target, 0, NULL, outp, int br = build_one_sep(target, 0, NULL, outp,
outstem[0] && !discard_output ? outstem : tmp, outstem[0] && !discard_output ? outstem : tmp,
incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm, incs, NULL, 0, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm,
outstem[0] && !discard_output ? 1 : 0, workdir, NULL, NULL, 0); retain_output && !deferred_install, retain_output ? 1 : 0,
workdir, NULL, NULL, 0);
if (br != 0) { if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT) if (owntmp && unlink(outp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
@@ -7905,6 +8073,9 @@ do_test(int argc, char **argv)
return cleanfail ? 1 : 0; return cleanfail ? 1 : 0;
} }
int rc = run_test_bin(outp, pattern); int rc = run_test_bin(outp, pattern);
if (rc == 0 && deferred_install
&& sep_install_test_output(outp, outstem) != 0)
rc = 1;
if (owntmp && unlink(outp) != 0 && errno != ENOENT) { if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
fputs("ww: cannot remove temporary output\n", stderr); fputs("ww: cannot remove temporary output\n", stderr);
if (rc == 0) rc = 1; if (rc == 0) rc = 1;

View File

@@ -6214,11 +6214,10 @@ The authority is official Go 1.26.5 at commit
The separation between saved and executed paths is a conclusion derived from The separation between saved and executed paths is a conclusion derived from
the pinned action dependencies: the run consumes the temporary link action, the pinned action dependencies: the run consumes the temporary link action,
not the install action. Duplicate names are likewise a materialization not the install action. Duplicate names are likewise a materialization
collision, not package identity. Go's sources do not assert that a set of collision, not package identity. Section 11.31 completes the later install
retained binaries is installed as one rollback transaction; WW keeps its dependency: compile-only products retain the request transaction, while a
existing stronger request-wide transaction while matching the observable running retained product installs independently only after its successful run.
accepted, rejected, and preserved outputs. No installed host Go behavior was No installed host Go behavior was used as authority.
used as authority.
Before this slice, direct native measurements of both Cstage and WWstage showed Before this slice, direct native measurements of both Cstage and WWstage showed
that single-package `-c -o FILE` retained and did not run, but `-o FILE` that single-package `-c -o FILE` retained and did not run, but `-o FILE`
@@ -6268,23 +6267,24 @@ performs ordinary production validation, reports `[no tests]`, and creates no
binary or otherwise-unused output directory. Successful compile-only products binary or otherwise-unused output directory. Successful compile-only products
are silent, matching Go's no-op print action. are silent, matching Go's no-op print action.
After linking, the driver copies the private runnable bytes to a distinct For `-c`, the driver copies the private runnable bytes to a distinct `.new`
`.new` inode opened with executable mode `0777` subject to umask. Temporary inode opened with executable mode `0777` subject to umask. Temporary runnable,
runnable, retained copy, statuses, changed persistent actions, tool records, retained copy, statuses, changed persistent actions, tool records, and stamp
and stamp then enter the existing one-request transaction. All producers and then enter the existing one-request transaction. All producers and linkers
linkers complete before installation. Any load, compile, assemble, archive, complete before installation. Any load, compile, assemble, archive, link,
link, stage, or install failure preserves old retained binaries and persistent stage, or install failure preserves old retained binaries and persistent bytes,
bytes, discards all stages, removes cold scratch, and rolls back only output discards all stages, removes cold scratch, and rolls back only output prefixes
prefixes created by that request. Occupied or dangling `.new` paths reject created by that request. Occupied or dangling `.new` paths reject before tools
before tools and are never overwritten. and are never overwritten.
Execution begins only after publication commits. Assertion failure, signal, For running `-o`, the build transaction commits only the private runnable,
timeout, or child-setup failure therefore leaves an explicitly retained binary status, and semantic actions. The coordinator executes that runnable and, on a
while retaining the established stdout/stderr result routing and sibling successful result, invokes the selected driver stage's public install action.
isolation. Parallel products stage independent runnable/copy pairs; the shared Assertion failure, signal, timeout, interruption, or child-setup failure skips
transaction prevents partial sibling publication and canonical result emission that action and preserves any prior binary. Successful parallel products
order remains unchanged. Direct invocation of a retained binary continues to install independently after their runs; canonical result emission order stays
inherit caller cwd, environment, and separate standard descriptors. unchanged. Direct invocation of a retained binary continues to inherit caller
cwd, environment, and separate standard descriptors.
`-c` and `-o` can accompany `-w`: the workdir owns only semantic actions while `-c` and `-o` can accompany `-w`: the workdir owns only semantic actions while
the invocation/output path owns only the retained copy. Unchanged actions are the invocation/output path owns only the retained copy. Unchanged actions are
@@ -6302,9 +6302,9 @@ declared-name versus import-leaf naming; executable mode and direct execution;
temporary argv versus retained path; no-test omission; duplicate and temporary argv versus retained path; no-test omission; duplicate and
non-directory rejection; occupied stages; serial and parallel sibling non-directory rejection; occupied stages; serial and parallel sibling
publication; injected late-link rollback over old files and newly created publication; injected late-link rollback over old files and newly created
parents; runtime-failure retention; persistent cold/warm/invalidation behavior; parents; runtime-failure preservation; persistent cold/warm/invalidation
diagnostic equality; retained binary byte identity; and absence of `.new` behavior; diagnostic equality; retained binary byte identity; and absence of
residue. Existing package tests continue to own all action/test variants, `.new` residue. Existing package tests continue to own all action/test variants,
graph identity, output ordering, cwd/environment/stdin, timeout, and broader graph identity, output ordering, cwd/environment/stdin, timeout, and broader
transaction behavior. transaction behavior.
@@ -6760,6 +6760,150 @@ output umasks, occupied stages, generalized multi-product transactions, test
runtime failure and timeout, null discard, and broader package/import graph runtime failure and timeout, null discard, and broader package/import graph
matrices. matrices.
### 11.31 Implemented Go-like public-output overwrite safety
Every caller-visible build and retained-test install now protects an existing
destination at the same late boundary as Go 1.26.5. After applicable producers
finish, ordinary `stat` rejects a directory and rejects a nonempty regular file
whose leading bytes do not identify a toolchain output. Absent paths, empty
regular reservations, recognized outputs, and non-directory non-regular paths
remain replaceable. Exact `/dev/null` and assembly-only `-S` have no install
action and never enter this rule.
#### Pinned Go evidence and classification
The sole authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `Shell.moveOrCopyFile` and `Shell.CopyFile` call `checkDstOverwrite` before
replacing the destination
([`cmd/go/internal/work/shell.go`, lines 119235](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L119-L235)).
`checkDstOverwrite` uses `os.Stat`, rejects a directory, and—unless forced—
rejects a nonempty regular file for which `isObject` is false
([lines 248261](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L248-L261)).
- `BuildInstallFunc` creates the destination parent and reaches
`moveOrCopyFile(..., false)` only after its build producer
([`cmd/go/internal/work/exec.go`, lines 19042000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1904-L2000)).
`objectMagic` and `isObject` read the first 64 bytes and recognize archive,
ELF, Mach-O, PE, Plan 9, WASM, and XCOFF prefixes without consulting a file
extension
([lines 21182150](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L2118-L2150)).
- `runBuild` loads and checks every selected package before constructing the
output/install action
([`cmd/go/internal/work/build.go`, lines 459558](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L558)).
- `builderTest` makes `-c` depend directly on the install action. For a running
retained test, the run consumes the private build action and the install
action additionally depends on that run
([`cmd/go/internal/test/test.go`, lines 12571364](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/test/test.go#L1257-L1364)).
`Builder.Do` invokes an actor only when dependency failure has not propagated
(unless the action explicitly ignores failure), so a failed test run skips
`BuildInstallFunc`
([`cmd/go/internal/work/exec.go`, lines 72207](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L72-L207)).
- Official `build_output_overwrite.txt` requires refusal to replace a
nonempty source file and preservation of its contents
([lines 120](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output_overwrite.txt#L1-L20)).
Official `test_compile_tempfile.txt` requires an existing empty reservation
to be accepted and replaced
([lines 111](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/test_compile_tempfile.txt#L1-L11)).
Official `build_output.txt` separately pins executable command and archive
products
([lines 4757 and 6476](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output.txt#L47-L76)).
The destination predicate, complete magic table, load-before-install ordering,
producer-before-check ordering, and run-before-install dependency are directly
implemented by the pinned source. Non-overwrite and empty-file acceptance are
directly asserted by official testdata. Applying ELF and archive recognition
to WW's byte-identical output forms is derived from that implementation. Go
has no WW interface sidecar; recognizing only the compiler-owned
`//ww:module ` prefix is the derived local application that permits ordinary
repeat publication without letting arbitrary sidecar text be overwritten. No
installed host Go behavior was used as authority.
#### Fresh four-axis audit and pre-fix measurements
The bounded audit selected this one gap on the build axis and the shared
retained-test install axis. The package control selected the same canonical
command root twice and measured one deduplicated command action plus one
dependency action, with byte-identical Cstage/WWstage units, interfaces, and
archives. The import control placed a used alias in one source file and an
unused alias for the same dependency in a sibling; both stages emitted the
same file-local unused-import diagnostic and committed no work. Those package
and import candidates were aligned and were not changed.
Fresh public Cstage and WWstage probes directly measured the same pre-fix
behavior. Explicit command, raw command, output-directory child, library
archive/interface, compile-only test, and running retained-test destinations
containing arbitrary nonempty text were replaced successfully. A nonempty
directory at a build or test child destination was renamed to a PID-bearing
transaction backup, replaced by the executable, and left stranded because
backup cleanup could not unlink the directory. Empty reservations were already
accepted. Missing-import rejection already preceded destination handling. All
measured successful executables, archives, interfaces, diagnostics, runtime
results, and semantic artifacts were stage-identical. Those are directly
measured WW facts, not source inferences.
#### Ownership, action order, rollback, and identity
The Cstage `sep_txn_commit` and WWstage `septxncommit` publishers own the byte
predicate. Each transaction entry now explicitly distinguishes a public
install from internal status, tool-identity, stamp, and persistent-action
state. Only command/archive output, retained-copy, and published `.wwi` entries
are checked. Producers still finish before transaction commit; a rejected
destination discards all staged outputs and preserves every prior public and
persistent byte. A library archive and interface remain one rollback group, so
arbitrary text in either destination changes neither.
`internal/wwpackage.packagecommand` continues to own directory-test naming and
scheduling. A running retained descriptor withholds its public destination
from the build child. After a successful private run, the coordinator invokes
a private action in the selected driver, which stages an executable copy and
re-enters the same guarded publisher. A failed, signalled, timed-out,
interrupted, or unstartable run never invokes that action. Successful products
in a multi-package running request install independently; compile-only products
retain the established request transaction. The driver-owned raw single-file
route applies the same private build, run, and guarded-install sequence.
Package-build descriptors use `build-public` only for caller-visible command or
archive products. Private package-build placeholders, test runnables, `ww run`,
workdir-owned test binaries, null-discard products, and assembly-only products
remain internal entries. Destination path, file kind, magic, declared name,
requested alias, import leaf, physical directory, and publication order do not
enter package/import identity, graph edges, action keys, symbols, artifacts,
`.wwi` contents, or persistence keys.
The guard follows symlinks for classification, matching `os.Stat`; the existing
transaction still replaces the destination directory entry itself. It permits
FIFO and other non-directory non-regular destinations because the pinned guard
does. Diagnostics are exactly
`ww: build output "PATH" already exists and is a directory` and
`ww: build output "PATH" already exists and is not an object file` in both
stages. No guard is preflighted during loading: package/import errors still win,
and compiler, assembler, archive, or linker failure prevents the install action
from being reached.
Build runtime is inapplicable because `ww build` starts no program. Test
runtime is applicable and owns the post-run dependency above. Producer failure,
linker interruption, output-parent rollback, concurrency, occupied stages,
prior-state preservation, and residue cleanup remain governed by the existing
request/private-action transactions; the new check adds no process-global
state. Public artifact bytes and modes are unchanged on accepted installs.
There is no persisted-byte contract change: build workdir format remains `18`,
test workdir format remains `19`, and semantic storage format remains `3`.
The WW-native `public_output_overwrite_safety` observer covers both stages:
direct, default, raw, package-output-directory, library, compile-only test, and
running-test routes; late linker activity and load precedence; absent/empty,
ELF, archive, interface, arbitrary regular, directory, symlink, and FIFO
destinations; cold, warm, and invalidated persistent rollback; run-before-check
and failed-run no-install behavior; exact null and assembly-only exclusions;
runtime results; modes; diagnostic identity; public and semantic artifact-byte
identity; and `.new`, install-stage, and transaction-backup cleanup.
`test_binary_publication_transaction` pins the changed failed-run behavior and
the existing linker failure, output-parent rollback, multi-product, persistent,
and retained-binary contracts. Existing request-transaction, timeout,
interruption, and concurrent-driver owners continue to cover those unchanged
dimensions.
## 12. Candidate architectures and hard-gate decision ## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins. Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -335,6 +335,22 @@ ImportPath = ident { "." ident } .
output. Output paths and directory metadata never become package, import, output. Output paths and directory metadata never become package, import,
graph, action, graph, action,
symbol, artifact, `.wwi`, or persistence identity. symbol, artifact, `.wwi`, or persistence identity.
Every caller-visible build installation checks its destination after all
applicable compile, assemble, archive, and link producers finish. Ordinary
`stat` follows symlinks. An existing directory rejects as
`ww: build output "PATH" already exists and is a directory`; an existing
nonempty regular file rejects as `... is not an object file` unless its
leading bytes identify a Go 1.26.5 object/output form. The recognized table
is archive, ELF, Mach-O, PE, Plan 9, WASM, and XCOFF magic; WW additionally
recognizes its compiler-owned `//ww:module ` interface prefix. An absent
path, an empty regular reservation, or a non-directory non-regular path may
be replaced. A published non-main archive and its `.wwi` sidecar are checked
as one WW request transaction, so arbitrary caller text in either
destination preserves both old outputs and the committed persistent
generation. This safety check is output disposition only: it does not enter
package/import loading, graph or action identity, artifact bytes, or
invalidation.
Assembly-only `-S` retains the directory form's command-action selection and Assembly-only `-S` retains the directory form's command-action selection and
no-main rejection, but it reaches no install action: destination length, no-main rejection, but it reaches no install action: destination length,
duplicate publication names, implicit destination collision, and output duplicate publication names, implicit destination collision, and output
@@ -714,12 +730,17 @@ paths, and retained binary names remain presentation or loader metadata and do
not become canonical package or action identity. not become canonical package or action identity.
The retained file is an executable, byte-identical copy of the private The retained file is an executable, byte-identical copy of the private
runnable. It joins package artifacts and statuses in the request-wide atomic runnable. A compile-only retained binary joins package artifacts and statuses
publication transaction. Build, link, stage, or install failure preserves old in the request-wide atomic publication transaction. Build, link, stage, or
destinations and removes temporary stages and invocation-created output install failure preserves old destinations and removes temporary stages and
prefixes. Test execution starts only after that transaction commits, so a invocation-created output prefixes. For a running `-o` request, the private
runtime failure leaves an explicitly retained binary. A no-test product binary executes first. Only a successful run enters the guarded install
publishes no binary and does not create a directory solely for one. action; a failed, signalled, timed-out, interrupted, or unstartable run
publishes no new copy and preserves any prior destination. The post-run guard
uses the same directory/nonempty-regular/object-magic rule as `ww build`.
Successful products in a multi-package running request install independently;
their visible result order remains package order. A no-test product publishes
no binary and does not create a directory solely for one.
`-w` may persist the unchanged semantic actions for either `-c` or running `-w` may persist the unchanged semantic actions for either `-c` or running
retention without changing publication identity or introducing a test-result retention without changing publication identity or introducing a test-result
cache. cache.

View File

@@ -258,11 +258,25 @@ install-only destination validation or output-directory creation.
The retained executable is byte-identical to the temporary runnable and has 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 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 executed by the coordinator. Compile-only publication participates in the
request-wide transaction: producer, linker, staging, or installation failure driver's one request-wide transaction: producer, linker, staging, or
preserves every prior destination and removes stages and newly created output installation failure preserves every prior destination and removes stages and
prefixes. A runtime failure occurs after commit and therefore leaves the saved newly created output prefixes. A running retained request instead withholds
binary. The language runtime owns individual `@test` functions. the public path from that build transaction. The coordinator runs the private
binary, then invokes the selected stage driver's guarded install action only
after a successful process result. Failure, signal, timeout, interruption, or
child-start failure therefore preserves any prior retained binary and creates
no new one. Successful products install independently after their runs. The
language runtime owns individual `@test` functions.
Every build or retained-test public install follows Go 1.26.5's late
destination safety rule. After applicable producers (and, for running tests,
after the successful run), ordinary `stat` rejects a directory and rejects a
nonempty regular non-object file. Empty reservations, recognized prior
outputs, and non-directory non-regular paths remain replaceable. Recognition
uses Go's archive/ELF/Mach-O/PE/Plan 9/WASM/XCOFF magic plus WW's narrow
`//ww:module ` interface prefix. Package/import rejection keeps its earlier
diagnostic precedence; exact `/dev/null` and `-S` never enter the guard.
Every actually executed directory product gives its single generated binary Every actually executed directory product gives its single generated binary
the product's canonical absolute physical source directory as child cwd. A the product's canonical absolute physical source directory as child cwd. A
@@ -468,8 +482,9 @@ with one fresh `mkdir` and refuses an existing path; it never clears a
collision. A caller keeps only the exact artifacts it observes and removes collision. A caller keeps only the exact artifacts it observes and removes
that exact tree on every later success or failure. Directory-package test that exact tree on every later success or failure. Directory-package test
plans instead keep their cold semantic-action scratch and runnable binary plans instead keep their cold semantic-action scratch and runnable binary
inside the coordinator's temporary root; only the optional retained executable inside the coordinator's temporary root; a compile-only retained executable
escapes through the transaction above. `ww run` and no-output single-file escapes through the build transaction, while a running retained executable
uses the post-run install action above. `ww run` and no-output single-file
`ww test` use driver-owned scratch instead; both driver stages place that `ww test` use driver-owned scratch instead; both driver stages place that
scratch and their temporary executable beneath one freshly acquired directory, scratch and their temporary executable beneath one freshly acquired directory,
remove both after every build result, and make cleanup failure fail the remove both after every build result, and make cleanup failure fail the

View File

@@ -39,9 +39,13 @@ type pkggroup = struct {
bin: str, bin: str,
buildok: str, buildok: str,
runoutput: str, runoutput: str,
installoutput: str,
state: i32, state: i32,
runstartfailed: bool, runstartfailed: bool,
runres: exec.result, runres: exec.result,
publicbin: bool,
installattempted: bool,
installres: exec.result,
}; };
type pkgplan = struct { type pkgplan = struct {
@@ -64,6 +68,7 @@ type pkgplan = struct {
outputcollisionbase: str, outputcollisionbase: str,
outputcollisiondir: str, outputcollisiondir: str,
suppressbuildreports: bool, suppressbuildreports: bool,
deferpublish: bool,
}; };
def PKG_COUNT_MAX: i32 = 2147483647; def PKG_COUNT_MAX: i32 = 2147483647;
@@ -1373,16 +1378,23 @@ fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32,
pkgfailpath(g.root, "cannot create product temporary path"); pkgfailpath(g.root, "cannot create product temporary path");
return false; return false;
}; };
g.publicbin = false;
g.installattempted = false;
if (buildonly) { if (buildonly) {
if (outputdir && strings.compare(g.pkg, "main") == 0 if (outputdir && strings.compare(g.pkg, "main") == 0
&& !p.outputpatherror && !p.emitasm) { && !p.outputpatherror && !p.emitasm) {
if (!pkgjoinpath(outname, g.basename, &g.bin)) { return false; }; if (!pkgjoinpath(outname, g.basename, &g.bin)) { return false; };
} else if (outname.len != 0 && !outputdir) { g.bin = outname; } g.publicbin = true;
} else if (outname.len != 0 && !outputdir) {
g.bin = outname;
g.publicbin = true;
}
else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; }; else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; };
} else { } else {
if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; }; if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; };
}; };
if (!pkgstring(&g.runoutput, g.root, "/test.output") if (!pkgstring(&g.runoutput, g.root, "/test.output")
|| !pkgstring(&g.installoutput, g.root, "/install.output")
|| !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; }; || !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; };
i += 1; i += 1;
}; };
@@ -1484,7 +1496,8 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
for (i < p.end) { for (i < p.end) {
let g: *pkggroup = &groups[i]; let g: *pkggroup = &groups[i];
append(ba, "--ww-package-test"); append(ba, "--ww-package-test");
if (p.buildonly) { append(ba, "build"); } if (p.buildonly && g.publicbin) { append(ba, "build-public"); }
else if (p.buildonly) { append(ba, "build"); }
else { append(ba, "test"); }; else { append(ba, "test"); };
append(ba, g.pkg); append(ba, g.pkg);
if (g.prodpkg.len != 0) { append(ba, g.prodpkg); } if (g.prodpkg.len != 0) { append(ba, g.prodpkg); }
@@ -1495,7 +1508,7 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
else { append(ba, "-"); }; else { append(ba, "-"); };
append(ba, g.dir); append(ba, g.dir);
append(ba, g.bin); append(ba, g.bin);
if (g.publish.len != 0) { append(ba, g.publish); } if (g.publish.len != 0 && !p.deferpublish) { append(ba, g.publish); }
else { append(ba, "-"); }; else { append(ba, "-"); };
append(ba, g.buildok); append(ba, g.buildok);
i += 1; i += 1;
@@ -1591,6 +1604,31 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
return true; return true;
}; };
// Pinned builderTest orders a retained running test as build -> run ->
// BuildInstallFunc. Re-enter the selected stage driver for the final action so
// build and test share one overwrite predicate instead of copying object magic
// into the coordinator.
fn pkginstalloutput(g: *pkggroup, builder: str) void = {
let ia: []str = [builder, "test", "--ww-install-test-output",
g.bin, g.publish];
let env: []str = os.getenvs();
let icmd: exec.command;
icmd.path = builder;
icmd.argv = ia;
icmd.env = env;
icmd.dir = "";
icmd.stdoutpath = g.installoutput;
icmd.stderrpath = g.installoutput;
icmd.deadline.sec = 0i64;
icmd.deadline.nsec = 0i64;
icmd.grace = 0i64: time.duration;
let h: exec.process;
g.installattempted = true;
exec.start(&h, &icmd);
for (!exec.poll(&h)) { time.sleep(pkgpoll, time.clock.monotonic); };
g.installres = h.result;
};
fn pkgproductbuilt(g: *pkggroup, buildonly: bool) bool = { fn pkgproductbuilt(g: *pkggroup, buildonly: bool) bool = {
if (buildonly || g.notests) { return pkgisreg(g.buildok); }; if (buildonly || g.notests) { return pkgisreg(g.buildok); };
return pkgisreg(g.buildok) && pkgisreg(g.bin); return pkgisreg(g.buildok) && pkgisreg(g.bin);
@@ -1602,6 +1640,35 @@ fn pkgrunok(g: *pkggroup) bool = {
&& g.runres.code == 0; && g.runres.code == 0;
}; };
fn pkginstallok(g: *pkggroup) bool = {
return g.installres.errno == 0 && g.installres.cleanuperrno == 0
&& g.installres.termination == exec.termination.EXIT
&& g.installres.code == 0;
};
fn pkgemitinstall(g: *pkggroup) bool = {
if (!g.installattempted) { return true; };
let output: str;
if (!pkgread(g.installoutput, &output)) {
pkgfailpath(g.root, "cannot read install capture");
return false;
};
if (output.len != 0) {
pkgput(os.STDERR_FILENO, output);
if (output[output.len - 1] != '\n') {
pkgput(os.STDERR_FILENO, "\n");
};
};
if (!pkginstallok(g)) {
if (output.len == 0) {
pkgreportcommand(os.STDERR_FILENO, "install", g,
&g.installres);
};
return false;
};
return true;
};
fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = {
if (g.notests) { if (g.notests) {
pkgput(os.STDOUT_FILENO, "? "); pkgput(os.STDOUT_FILENO, "? ");
@@ -1653,8 +1720,9 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup,
failed += 1; failed += 1;
} else if (buildonly) { } else if (buildonly) {
void; void;
} else if (!pkgemitgroup(&groups[i], compileonly)) { } else {
failed += 1; if (!pkgemitgroup(&groups[i], compileonly)) { failed += 1; };
if (!compileonly && !pkgemitinstall(&groups[i])) { failed += 1; };
}; };
i += 1; i += 1;
}; };
@@ -2385,11 +2453,12 @@ export fn packagecommand(args: []str) int = {
plan.outputcollisiondir = outputcollisiondir; plan.outputcollisiondir = outputcollisiondir;
plan.suppressbuildreports = buildonly plan.suppressbuildreports = buildonly
&& (outputdir || defaultoutputdir.len != 0); && (outputdir || defaultoutputdir.len != 0);
plan.deferpublish = !buildonly && !compileonly && testretain && !testnull;
append(plans, plan); append(plans, plan);
let createdir: str = ""; let createdir: str = "";
if (buildonly && outputdir) { if (buildonly && outputdir) {
createdir = outname; createdir = outname;
} else if (!buildonly && testretain && !testnull) { } else if (!buildonly && compileonly && testretain && !testnull) {
i = 0; i = 0;
for (i < groups.len) { for (i < groups.len) {
if (groups[i].publish.len != 0) { if (groups[i].publish.len != 0) {
@@ -2550,6 +2619,22 @@ export fn packagecommand(args: []str) int = {
if (active > 0) { time.sleep(pkgpoll, time.clock.monotonic); }; if (active > 0) { time.sleep(pkgpoll, time.clock.monotonic); };
}; };
// A failed, signalled, timed-out, or unstartable run propagates to its Go
// install action, so it preserves any prior retained binary. Successful
// products install independently after their private execution.
if (!compileonly) {
i = 0;
for (i < groups.len) {
let g: *pkggroup = &groups[i];
if (g.publish.len != 0 && !g.notests
&& pkgproductbuilt(g, buildonly) && !g.runstartfailed
&& pkgrunok(g)) {
pkginstalloutput(g, builder);
};
i += 1;
};
};
i = 0; i = 0;
for (i < plans.len) { for (i < plans.len) {
failed += pkgemitplan(&plans[i], groups, compileonly, buildonly); failed += pkgemitplan(&plans[i], groups, compileonly, buildonly);

View File

@@ -1515,6 +1515,7 @@ type sepproduct = struct {
directoryproduct: bool, directoryproduct: bool,
notests: bool, notests: bool,
buildaction: bool, // loaded product retained in the action list buildaction: bool, // loaded product retained in the action list
publicout: bool,
context: i32, context: i32,
root: i32, root: i32,
variantroot: i32, variantroot: i32,
@@ -6134,6 +6135,7 @@ type septxnentry = struct {
backup: *u8, backup: *u8,
hadold: bool, hadold: bool,
installed: bool, installed: bool,
publicoutput: bool,
}; };
fn sepalloctxnentries(cap: i32) ([]septxnentry | nomem) = { fn sepalloctxnentries(cap: i32) ([]septxnentry | nomem) = {
@@ -6207,8 +6209,8 @@ fn septxnbackup(dst: *u8) *u8 = {
return buf.ptr; return buf.ptr;
}; };
fn septxnadd(entries: *[]septxnentry, n: *i32, fn septxnaddmode(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8) bool = { stage: *u8, dst: *u8, publicoutput: bool) bool = {
if (cstreq(stage, dst)) { if (cstreq(stage, dst)) {
cerrpath("ww: transaction path collision: ", dst, "\n"); cerrpath("ww: transaction path collision: ", dst, "\n");
return false; return false;
@@ -6253,10 +6255,21 @@ fn septxnadd(entries: *[]septxnentry, n: *i32,
(*entries)[*n].backup = backup; (*entries)[*n].backup = backup;
(*entries)[*n].hadold = false; (*entries)[*n].hadold = false;
(*entries)[*n].installed = false; (*entries)[*n].installed = false;
(*entries)[*n].publicoutput = publicoutput;
*n += 1; *n += 1;
return true; return true;
}; };
fn septxnadd(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8) bool = {
return septxnaddmode(entries, n, stage, dst, false);
};
fn septxnaddpublic(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8) bool = {
return septxnaddmode(entries, n, stage, dst, true);
};
fn septxnaddpkgsuffix(entries: *[]septxnentry, n: *i32, fn septxnaddpkgsuffix(entries: *[]septxnentry, n: *i32,
g: *sepgraph, pi: i32, scratch: *u8, g: *sepgraph, pi: i32, scratch: *u8,
stagesuffix: str, dstsuffix: str) bool = { stagesuffix: str, dstsuffix: str) bool = {
@@ -6271,6 +6284,91 @@ fn septxndiscard(entries: []septxnentry, n: i32) void = {
for (i < n) { os.remove(pathstr(entries[i].stage)); i += 1; }; for (i < n) { os.remove(pathstr(entries[i].stage)); i += 1; };
}; };
fn sepoutputprefix(buf: *u8, n: i64, magic: str) bool = {
if (n < 0i64 || n < magic.len: i64) { return false; };
let i: i32 = 0;
for (i < magic.len) {
if (buf[i] != magic.ptr[i]) { return false; };
i += 1;
};
return true;
};
fn sepoutput2(buf: *u8, n: i64, a: u8, b: u8) bool = {
return n >= 2i64 && buf[0u64] == a && buf[1u64] == b;
};
fn sepoutput4(buf: *u8, n: i64, a: u8, b: u8, c: u8, d: u8) bool = {
return n >= 4i64 && buf[0u64] == a && buf[1u64] == b
&& buf[2u64] == c && buf[3u64] == d;
};
fn sepoutput6(buf: *u8, n: i64, a: u8, b: u8, c: u8, d: u8,
e: u8, f: u8) bool = {
return n >= 6i64 && buf[0u64] == a && buf[1u64] == b
&& buf[2u64] == c && buf[3u64] == d
&& buf[4u64] == e && buf[5u64] == f;
};
// Go 1.26.5 work.objectMagic, plus WW's compiler-owned interface prefix.
fn sepisobjectoutput(path: *u8) bool = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return false; };
let buf: [64]u8;
let got: u64 = 0u64;
let bad: bool = false;
for (got < 64u64) {
let n: i64 = os.read(fd, &buf[0] + got, 64u64 - got);
if (n > 0) { got += n: u64; }
else if (n == 0) { break; }
else if (n != -4i64) { bad = true; break; };
};
os.close(fd);
if (bad) { return false; };
let n: i64 = got: i64;
if (sepoutputprefix(&buf[0], n, "!<arch>\n")
|| sepoutputprefix(&buf[0], n, "<bigaf>\n")
|| sepoutput4(&buf[0], n, 127u8, 69u8, 76u8, 70u8)
|| sepoutput4(&buf[0], n, 254u8, 237u8, 250u8, 206u8)
|| sepoutput4(&buf[0], n, 254u8, 237u8, 250u8, 207u8)
|| sepoutput4(&buf[0], n, 206u8, 250u8, 237u8, 254u8)
|| sepoutput4(&buf[0], n, 207u8, 250u8, 237u8, 254u8)
|| sepoutput6(&buf[0], n, 77u8, 90u8, 144u8, 0u8, 3u8, 0u8)
|| sepoutput6(&buf[0], n, 77u8, 90u8, 120u8, 0u8, 1u8, 0u8)
|| sepoutput4(&buf[0], n, 0u8, 0u8, 1u8, 235u8)
|| sepoutput4(&buf[0], n, 0u8, 0u8, 138u8, 151u8)
|| sepoutput4(&buf[0], n, 0u8, 0u8, 6u8, 71u8)
|| sepoutput4(&buf[0], n, 0u8, 97u8, 115u8, 109u8)
|| sepoutput2(&buf[0], n, 1u8, 223u8)
|| sepoutput2(&buf[0], n, 1u8, 247u8)
|| sepoutputprefix(&buf[0], n, "//ww:module ")) {
return true;
};
return false;
};
fn sepcheckdstoverwrite(dst: *u8) bool = {
let fi: os.filestat;
match (os.stat(&fi, pathstr(dst))) {
case void => {
let typ: u32 = (fi.mode: u32) & 61440u32;
if (typ == os.mode.DIR: u32) {
cerr("ww: build output "); sepputquoted(dst);
cerr(" already exists and is a directory\n");
return false;
};
if (typ == os.mode.REG: u32 && fi.sz != 0u64
&& !sepisobjectoutput(dst)) {
cerr("ww: build output "); sepputquoted(dst);
cerr(" already exists and is not an object file\n");
return false;
};
};
case let e: os.oserror => void;
};
return true;
};
fn septxncommit(entries: []septxnentry, n: i32) bool = { fn septxncommit(entries: []septxnentry, n: i32) bool = {
let i: i32 = 0; let i: i32 = 0;
let valid: bool = true; let valid: bool = true;
@@ -6283,6 +6381,17 @@ fn septxncommit(entries: []septxnentry, n: i32) bool = {
}; };
i += 1; i += 1;
}; };
if (valid) {
i = 0;
for (i < n) {
if (entries[i].publicoutput
&& !sepcheckdstoverwrite(entries[i].dst)) {
valid = false;
break;
};
i += 1;
};
};
if (valid) { if (valid) {
i = 0; i = 0;
for (i < n) { for (i < n) {
@@ -6614,6 +6723,50 @@ fn septxnrelease(entries: *[]septxnentry, n: i32) void = {
entries.cap = 0; entries.cap = 0;
}; };
// Running retained tests enter this Go-like install action only after their
// request-private executable has returned successfully.
fn sepinstalltestoutput(stage: *u8, dst: *u8) i32 = {
let installstage: *u8 = sepappendlit(stage, ".install");
if (installstage == nil) { return 1; };
if (cstrlen(installstage) + 1u64 > os.PATH_MAX: u64
|| pathexistsnofollow(installstage) != 0
|| copyexecutablestage(stage, installstage) != 0) {
cerr("ww: cannot stage retained test output\n");
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
let parent: *u8 = seplexicalparent(dst);
if (parent == nil) {
os.remove(pathstr(installstage));
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
let created: sepcreateddirs;
created.n = 0;
if (sepmkdirsrecord(parent, 511, &created) != 0) {
cerrpath("ww: cannot create test output directory ", parent, "\n");
os.free(parent: *void, cstrlen(parent) + 1u64);
os.remove(pathstr(installstage));
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
os.free(parent: *void, cstrlen(parent) + 1u64);
let entries: []septxnentry;
let n: i32 = 0;
if (!septxnaddpublic(&entries, &n, installstage, dst)
|| !septxncommit(entries, n)) {
septxndiscard(entries, n);
septxnrelease(&entries, n);
seprollbackdirs(&created);
os.remove(pathstr(installstage));
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
septxnrelease(&entries, n);
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 0;
};
fn sepfinishfail(entries: *[]septxnentry, n: i32) i32 = { fn sepfinishfail(entries: *[]septxnentry, n: i32) i32 = {
septxndiscard(*entries, n); septxndiscard(*entries, n);
septxnrelease(entries, n); septxnrelease(entries, n);
@@ -6924,19 +7077,24 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8,
producti = 0; producti = 0;
for (producti < nproducts) { for (producti < nproducts) {
if (products[producti].stageout != nil if (products[producti].stageout != nil
&& !septxnadd(&entries, &ntxn, products[producti].stageout, && ((products[producti].publicout
products[producti].out)) { && !septxnaddpublic(&entries, &ntxn,
return sepfinishfail(&entries, ntxn); products[producti].stageout, products[producti].out))
|| (!products[producti].publicout
&& !septxnadd(&entries, &ntxn,
products[producti].stageout, products[producti].out)))) {
return sepfinishfail(&entries, ntxn);
}; };
if (products[producti].stagepublish != nil if (products[producti].stagepublish != nil
&& !septxnadd(&entries, &ntxn, products[producti].stagepublish, && !septxnaddpublic(&entries, &ntxn,
products[producti].stagepublish,
products[producti].publish)) { products[producti].publish)) {
return sepfinishfail(&entries, ntxn); return sepfinishfail(&entries, ntxn);
}; };
if (products[producti].stageiface != nil) { if (products[producti].stageiface != nil) {
let outiface: *u8 = sepappendlit(products[producti].out, ".wwi"); let outiface: *u8 = sepappendlit(products[producti].out, ".wwi");
if (outiface == nil if (outiface == nil
|| !septxnadd(&entries, &ntxn, || !septxnaddpublic(&entries, &ntxn,
products[producti].stageiface, outiface)) { products[producti].stageiface, outiface)) {
return sepfinishfail(&entries, ntxn); return sepfinishfail(&entries, ntxn);
}; };
@@ -8213,7 +8371,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32, objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32,
requirecommand: i32, istest: i32, requirecommand: i32, istest: i32,
rootvariant: i32, testpackage: *u8, emitasm: i32, rootvariant: i32, testpackage: *u8, emitasm: i32,
keepscratch: i32, workdir: *u8, createoutputdir: *u8, publicoutput: bool, keepscratch: i32, workdir: *u8,
createoutputdir: *u8,
defaultoutputdir: *u8, outputpatherror: bool) i32 = { defaultoutputdir: *u8, outputpatherror: bool) i32 = {
let scratch: *u8 = nil; let scratch: *u8 = nil;
let g: *sepgraph = nil; let g: *sepgraph = nil;
@@ -8235,6 +8394,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
product.directoryproduct = false; product.directoryproduct = false;
product.notests = false; product.notests = false;
product.buildaction = true; product.buildaction = true;
product.publicout = publicoutput;
product.root = -1; product.root = -1;
product.variantroot = -1; product.variantroot = -1;
product.support = -1; product.support = -1;
@@ -8691,7 +8851,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let rc: i32 = buildonesep(selfdir, resolved, isdir, rootidentity, let rc: i32 = buildonesep(selfdir, resolved, isdir, rootidentity,
outp, outp, incs.ptr, &lf, outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil, 0i32, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, 0i32, workdir, nil, nil, false); emitasm, false, 0i32, workdir, nil, nil, false);
let cleanbad: bool = false; let cleanbad: bool = false;
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
@@ -8710,7 +8870,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
return buildonesep(selfdir, resolved, isdir, rootidentity, return buildonesep(selfdir, resolved, isdir, rootidentity,
out, objstem, incs.ptr, &lf, out, objstem, incs.ptr, &lf,
publishpackage, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil, publishpackage, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, 1i32, workdir, createoutputdir, defaultoutputdir, emitasm, true, 1i32, workdir, createoutputdir, defaultoutputdir,
outputpatherror); outputpatherror);
}; };
@@ -8893,7 +9053,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (buildonesep(selfdir, resolved, isdir, rootidentity, if (buildonesep(selfdir, resolved, isdir, rootidentity,
outp, outp, incs.ptr, &lf, outp, outp, incs.ptr, &lf,
0i32, 1i32, 0i32, SEP_VARIANT_PRODUCTION, nil, 0i32, 1i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
0i32, 0i32, nil, nil, nil, false) != 0) { 0i32, false, 0i32, nil, nil, nil, false) != 0) {
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n"); cerr("ww: cannot remove temporary output\n");
@@ -8958,10 +9118,12 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
let owntmp: bool = false; let owntmp: bool = false;
let retainout: bool = outstem != nil let retainout: bool = outstem != nil
&& !cstreqlit(outstem, "/dev/null"); && !cstreqlit(outstem, "/dev/null");
if (retainout) { let deferredinstall: bool = retainout && compileonly == 0
&& emitasm == 0;
if (retainout && !deferredinstall) {
outp = outstem; outp = outstem;
objstem = outstem; objstem = outstem;
} else { if (workdir != nil) { } else { if (workdir != nil && !deferredinstall) {
// The workdir owns the persistent test binary the same way it // The workdir owns the persistent test binary the same way it
// owns the package artifacts. // owns the package artifacts.
outp = joinpathlit(workdir, "main"); outp = joinpathlit(workdir, "main");
@@ -8975,6 +9137,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
}; };
outp = joinpathlit(tmp.ptr, "main"); outp = joinpathlit(tmp.ptr, "main");
objstem = outp; objstem = outp;
if (retainout) { objstem = outstem; };
}; }; }; };
// E3-C1: separate compilation is the sole build path (task #87). // E3-C1: separate compilation is the sole build path (task #87).
let lf: lflags; let lf: lflags;
@@ -8986,7 +9149,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
if (retainout) { keep = 1; }; if (retainout) { keep = 1; };
let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf, let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf,
0i32, 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil, 0i32, 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, keep, workdir, nil, nil, false); emitasm, retainout && !deferredinstall, keep, workdir,
nil, nil, false);
if (bres != 0) { if (bres != 0) {
if (owntmp) { if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
@@ -9037,6 +9201,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
rc = -1; rc = -1;
}; };
}; }; }; };
if (rc == 0 && deferredinstall
&& sepinstalltestoutput(outp, outstem) != 0) { rc = 1; };
if (owntmp) { if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp)); let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) { if (cleanrc != 0 && cleanrc != -2i32) {
@@ -9052,6 +9218,10 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
}; };
fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (argc - start == 3
&& cstreqlit(argv[start], "--ww-install-test-output")) {
return sepinstalltestoutput(argv[start + 1], argv[start + 2]);
};
// -I parsing mirrors dobuild/dorun so a single-file test can resolve // -I parsing mirrors dobuild/dorun so a single-file test can resolve
// transitive imports (e.g. 905_nkname asttest → tok); coupled to the // transitive imports (e.g. 905_nkname asttest → tok); coupled to the
// -T flip (task #5/#10). // -T flip (task #5/#10).
@@ -9203,7 +9373,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let publish: *u8 = argv[i + 8]; let publish: *u8 = argv[i + 8];
let status: *u8 = argv[i + 9]; let status: *u8 = argv[i + 9];
let pn: u64 = cstrlen(name); let pn: u64 = cstrlen(name);
let buildproduct: bool = cstreqlit(kind, "build"); let publicbuildproduct: bool =
cstreqlit(kind, "build-public");
let buildproduct: bool = cstreqlit(kind, "build")
|| publicbuildproduct;
let testproduct: bool = cstreqlit(kind, "test"); let testproduct: bool = cstreqlit(kind, "test");
let hasproduction: bool = !cstreqlit(production, "-"); let hasproduction: bool = !cstreqlit(production, "-");
let hasinternal: bool = !cstreqlit(internal, "-"); let hasinternal: bool = !cstreqlit(internal, "-");
@@ -9248,6 +9421,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
product.directoryproduct = true; product.directoryproduct = true;
product.notests = testproduct && !hasinternal && !hasexternal; product.notests = testproduct && !hasinternal && !hasexternal;
product.buildaction = true; product.buildaction = true;
product.publicout = publicbuildproduct;
product.root = -1; product.root = -1;
product.variantroot = -1; product.variantroot = -1;
product.productionroot = -1; product.productionroot = -1;

View File

@@ -12443,7 +12443,6 @@ fn runtimepath(relative: str) str = {
let rollbackdiag: str = ""; let rollbackdiag: str = "";
let missingdiag: str = ""; let missingdiag: str = "";
let occupieddiag: str = ""; let occupieddiag: str = "";
let failurebytes: str = "";
let persistbasebytes: str = ""; let persistbasebytes: str = "";
let persistchangedbytes: str = ""; let persistchangedbytes: str = "";
let baseenv: []str = os.getenvs(); let baseenv: []str = os.getenvs();
@@ -12595,29 +12594,23 @@ fn runtimepath(relative: str) str = {
" [no tests]\n"))); " [no tests]\n")));
assert(out.stderr.len == 0 && !os.exists(noneroot)); assert(out.stderr.len == 0 && !os.exists(noneroot));
// Publication commits before execution. A failing test therefore leaves // Pinned builderTest makes the install action depend on the run action.
// a runnable retained binary while its product failure stays on stdout. // A failing run therefore preserves a prior destination and never enters
// the retained-output overwrite guard.
let failurebin: str = strings.concat(root, "/failure-", tags[si], let failurebin: str = strings.concat(root, "/failure-", tags[si],
".test"); ".test");
writefile(failurebin, "failure-sentinel\n");
let failureav: []str = [driver(stages[si]), "test", "-I", suite, let failureav: []str = [driver(stages[si]), "test", "-I", suite,
"-o", failurebin, failed]; "-o", failurebin, failed];
runcommandenvdir(root, strings.concat("publish-runtime-", tags[si]), runcommandenvdir(root, strings.concat("publish-runtime-", tags[si]),
failureav, env, root, failureav, env, root,
(60i64 * (time.second: i64)): time.duration, &out); (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1); expectexit(&out, 1);
assert(os.exists(failurebin) && !os.exists(strings.concat(failurebin, assert(same(readfile(failurebin), "failure-sentinel\n"));
".new"))); assert(!os.exists(strings.concat(failurebin, ".new")));
assert(has(out.stdout, assert(has(out.stdout,
"failed_decl.runtime_failure ... FAIL (exit 1)\n")); "failed_decl.runtime_failure ... FAIL (exit 1)\n"));
assert(out.stderr.len == 0); assert(out.stderr.len == 0);
let fi: os.filestat;
match (os.stat(&fi, failurebin)) {
case void => void;
case let e: os.oserror => abort("retained failure binary missing");
};
assert(((fi.mode: u32) & 73u32) != 0u32);
if (si == 0) { failurebytes = readfile(failurebin); }
else { assert(same(failurebytes, readfile(failurebin))); };
// -w keeps semantic actions persistent while -c and -o remain execution // -w keeps semantic actions persistent while -c and -o remain execution
// and presentation policy. Compile-only can cold-build and invalidate; // and presentation policy. Compile-only can cold-build and invalidate;
@@ -12681,6 +12674,377 @@ fn runtimepath(relative: str) str = {
clean(root); clean(root);
}; };
// Go 1.26.5's BuildInstallFunc protects caller data at the install boundary:
// after producers, and after a successful run for retained running tests.
// Exercise that one predicate through direct, raw, package, and test routes.
@test fn public_output_overwrite_safety() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let dep: str = strings.concat(source, "/lib/dep");
let app: str = strings.concat(source, "/cmd/app");
let bad: str = strings.concat(source, "/cmd/bad");
let checked: str = strings.concat(source, "/pkg/check");
let failed: str = strings.concat(source, "/pkg/failed");
let invocation: str = strings.concat(root, "/invoke");
mkdirall(dep); mkdirall(app); mkdirall(bad); mkdirall(checked);
mkdirall(failed); mkdirall(invocation);
writefile(strings.concat(dep, "/dep.ww"), strings.concat(
"package renamed;\n",
"export fn value() int = { return 9; };\n"));
let appsource: str = strings.concat(app, "/main.ww");
let appbase: str = strings.concat(
"package main;\n",
"import stable lib.dep;\n",
"export fn main() int = { return stable.value(); };\n");
let appchanged: str = strings.concat(
"package main;\n",
"import stable lib.dep;\n",
"export fn main() int = { return stable.value() + 1; };\n");
writefile(appsource, appbase);
writefile(strings.concat(bad, "/main.ww"), strings.concat(
"package main;\n",
"import missing nowhere.present;\n",
"export fn main() int = { return missing.value(); };\n"));
writefile(strings.concat(checked, "/check.ww"),
"package checked;\nfn value() int = { return 4; };\n");
writefile(strings.concat(checked, "/check_test.ww"), strings.concat(
"package checked;\n",
"@test fn guarded_run() void = { assert(value() == 4); };\n"));
writefile(strings.concat(failed, "/failed_test.ww"), strings.concat(
"package failed;\n",
"@test fn failed_run() void = { assert(false); };\n"));
let raw: str = strings.concat(root, "/raw.ww");
writefile(raw,
"package main;\nexport fn main() int = { return 3; };\n");
let linkerwrapper: str = strings.concat(root, "/overwrite-w6l.sh");
writeexecutable(linkerwrapper, strings.concat(
"#!/bin/sh\n",
"printf 'link\\n' >> \"$WW_OVERWRITE_TRACE\"\n",
"exec \"$WW_OVERWRITE_REAL_L\" \"$@\"\n"));
let drivers: []str = ["ww", "ww_ww"];
let linkers: []str = ["w6l", "w6l_ww"];
let tags: []str = ["c", "ww"];
let commandout: str = strings.concat(root, "/command.bin");
let rawout: str = strings.concat(root, "/raw.bin");
let defaultout: str = strings.concat(invocation, "/app");
let libout: str = strings.concat(root, "/library.a");
let testbin: str = strings.concat(root, "/check.test");
let symlinktarget: str = strings.concat(root, "/symlink-target");
let symlinkout: str = strings.concat(root, "/symlink.bin");
let fifoout: str = strings.concat(root, "/fifo.bin");
writefile(symlinktarget, "symlink-sentinel\n");
assert(os.symlink(symlinktarget, symlinkout) == 0);
let builddir: str = strings.concat(root, "/build-output");
let buildchild: str = strings.concat(builddir, "/app");
let testdir: str = strings.concat(root, "/test-output");
let testchild: str = strings.concat(testdir, "/check.test");
mkdirall(buildchild); mkdirall(testchild);
writefile(strings.concat(buildchild, "/marker"), "build-directory\n");
writefile(strings.concat(testchild, "/marker"), "test-directory\n");
let commanddiag: str = "";
let rawdiag: str = "";
let defaultdiag: str = "";
let symlinkdiag: str = "";
let builddirdiag: str = "";
let librarydiag: str = "";
let libraryarchivediag: str = "";
let testcompilediag: str = "";
let testrundiag: str = "";
let testdirdiag: str = "";
let loaddiag: str = "";
let commandbytes: str = "";
let librarybytes: str = "";
let interfacebytes: str = "";
let testbytes: str = "";
let unitbytes: str = "";
let baseenv: []str = os.getenvs();
let out: commandout;
let si: i32 = 0;
for (si < drivers.len) {
let trace: str = strings.concat(root, "/overwrite-", tags[si],
".trace");
writefile(trace, "");
let env: []str = alloc([], (baseenv.len + 3): u64)!;
let ei: i32 = 0;
for (ei < baseenv.len) {
if (!strings.hasprefix(baseenv[ei], "WW_W6L=")
&& !strings.hasprefix(baseenv[ei], "WW_OVERWRITE_TRACE=")
&& !strings.hasprefix(baseenv[ei], "WW_OVERWRITE_REAL_L=")) {
append(env, baseenv[ei]);
};
ei += 1;
};
append(env, strings.concat("WW_W6L=", linkerwrapper));
append(env, strings.concat("WW_OVERWRITE_TRACE=", trace));
append(env, strings.concat("WW_OVERWRITE_REAL_L=",
driver(linkers[si])));
let work: str = strings.concat(root, "/command-work-", tags[si]);
let badwork: str = strings.concat(root, "/bad-work-", tags[si]);
let rawwork: str = strings.concat(root, "/raw-work-", tags[si]);
let defaultwork: str = strings.concat(root, "/default-work-", tags[si]);
let symlinkwork: str = strings.concat(root, "/symlink-work-", tags[si]);
let fifowork: str = strings.concat(root, "/fifo-work-", tags[si]);
let dirwork: str = strings.concat(root, "/dir-work-", tags[si]);
let libwork: str = strings.concat(root, "/lib-work-", tags[si]);
let testwork: str = strings.concat(root, "/test-work-", tags[si]);
let testdirwork: str = strings.concat(root, "/test-dir-work-", tags[si]);
mkdirall(work); mkdirall(badwork); mkdirall(rawwork);
mkdirall(defaultwork); mkdirall(symlinkwork); mkdirall(fifowork);
mkdirall(dirwork);
mkdirall(libwork); mkdirall(testwork); mkdirall(testdirwork);
// A nonempty caller file rejects only after the linker ran, and a cold
// persistent generation remains unpublished.
if (os.exists(commandout)) { assert(os.remove(commandout) == 0); };
writefile(commandout, "caller-command\n");
rewritefile(trace, "");
let commandav: []str = [driver(drivers[si]), "build", "-w", work,
"-I", source, "-o", commandout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-command-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, strings.concat("ww: build output \"", commandout,
"\" already exists and is not an object file\n")));
assert(same(readfile(commandout), "caller-command\n"));
assert(permissionmode(commandout) == 384u32);
assert(readfile(trace).len != 0 && directoryisempty(work));
assert(!os.exists(strings.concat(commandout, ".new"))
&& !directoryhasfragment(root, "command.bin.wwtxn."));
if (si == 0) { commanddiag = strings.dup(out.stderr); }
else { assert(same(commanddiag, out.stderr)); };
// Package/import failure keeps load precedence and starts no linker.
rewritefile(trace, "");
let badav: []str = [driver(drivers[si]), "build", "-w", badwork,
"-I", source, "-o", commandout, "cmd.bad"];
runcommandenv(root, strings.concat("overwrite-load-", tags[si]), badav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "cannot find package nowhere.present"));
assert(!has(out.stderr, "already exists and is not an object file"));
assert(readfile(trace).len == 0 && directoryisempty(badwork));
assert(same(readfile(commandout), "caller-command\n"));
if (si == 0) { loaddiag = strings.dup(out.stderr); }
else { assert(same(loaddiag, out.stderr)); };
// Empty reservations and recognized ELF outputs are replaceable. A
// rejected invalidation preserves the prior persistent generation.
rewritefile(commandout, ""); rewritefile(trace, "");
runcommandenv(root, strings.concat("overwrite-empty-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let built: str = readfile(commandout);
assert(strings.hasprefix(built, "\x7fELF"));
let unit: str = readfile(strings.concat(work, "/cmd.app.unit.ww"));
if (si == 0) {
commandbytes = strings.dup(built);
unitbytes = strings.dup(unit);
} else {
assert(same(commandbytes, built) && same(unitbytes, unit));
};
rewritefile(appsource, appchanged);
rewritefile(commandout, "caller-after-change\n");
rewritefile(trace, "");
runcommandenv(root, strings.concat("overwrite-invalidated-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(readfile(trace).len != 0);
assert(same(readfile(commandout), "caller-after-change\n"));
assert(same(readfile(strings.concat(work, "/cmd.app.unit.ww")), unit));
rewritefile(appsource, appbase);
rewritefile(commandout, built);
runcommandenv(root, strings.concat("overwrite-elf-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let runav: []str = [commandout];
runcommand(root, strings.concat("overwrite-command-run-", tags[si]),
runav, (10i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 9);
// Default, raw, symlink-following, and output-directory child routes
// use the same late guard and never strand rollback backups.
if (os.exists(defaultout)) { rewritefile(defaultout, "default-sentinel\n"); }
else { writefile(defaultout, "default-sentinel\n"); };
let defaultav: []str = [driver(drivers[si]), "build", "-w",
defaultwork, "-I", source, "cmd.app"];
runcommandenvdir(root, strings.concat("overwrite-default-", tags[si]),
defaultav, env, invocation,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(defaultout), "default-sentinel\n"));
if (si == 0) { defaultdiag = strings.dup(out.stderr); }
else { assert(same(defaultdiag, out.stderr)); };
if (os.exists(rawout)) { rewritefile(rawout, "raw-sentinel\n"); }
else { writefile(rawout, "raw-sentinel\n"); };
let rawav: []str = [driver(drivers[si]), "build", "-w", rawwork,
"-o", rawout, raw];
runcommandenv(root, strings.concat("overwrite-raw-", tags[si]), rawav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(rawout), "raw-sentinel\n"));
if (si == 0) { rawdiag = strings.dup(out.stderr); }
else { assert(same(rawdiag, out.stderr)); };
let symlinkav: []str = [driver(drivers[si]), "build", "-w",
symlinkwork, "-I", source, "-o", symlinkout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-symlink-", tags[si]),
symlinkav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(symlinkout), "symlink-sentinel\n"));
if (si == 0) { symlinkdiag = strings.dup(out.stderr); }
else { assert(same(symlinkdiag, out.stderr)); };
if (os.exists(fifoout)) { assert(os.remove(fifoout) == 0); };
let fifoav: []str = ["/usr/bin/mkfifo", fifoout];
runcommand(root, strings.concat("overwrite-mkfifo-", tags[si]), fifoav,
(10i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let fifobuildav: []str = [driver(drivers[si]), "build", "-w",
fifowork, "-I", source, "-o", fifoout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-fifo-", tags[si]),
fifobuildav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(strings.hasprefix(readfile(fifoout), "\x7fELF"));
let dirav: []str = [driver(drivers[si]), "build", "-w", dirwork,
"-I", source, "-o", strings.concat(builddir, "/"), "cmd.app"];
runcommandenv(root, strings.concat("overwrite-build-dir-", tags[si]),
dirav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "already exists and is a directory"));
assert(same(readfile(strings.concat(buildchild, "/marker")),
"build-directory\n"));
assert(!directoryhasfragment(builddir, ".wwtxn."));
if (si == 0) { builddirdiag = strings.dup(out.stderr); }
else { assert(same(builddirdiag, out.stderr)); };
// Archive and WW-interface outputs each participate in the guard. An
// ordinary repeat accepts both recognized prior outputs; arbitrary
// interface or archive text rejects without changing either member.
if (os.exists(libout)) { assert(os.remove(libout) == 0); };
if (os.exists(strings.concat(libout, ".wwi"))) {
assert(os.remove(strings.concat(libout, ".wwi")) == 0);
};
let libav: []str = [driver(drivers[si]), "build", "-w", libwork,
"-I", source, "-o", libout, "lib.dep"];
runcommandenv(root, strings.concat("overwrite-library-new-", tags[si]),
libav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let archive: str = readfile(libout);
let iface: str = readfile(strings.concat(libout, ".wwi"));
assert(strings.hasprefix(archive, "!<arch>\n"));
assert(strings.hasprefix(iface, "//ww:module "));
runcommandenv(root, strings.concat("overwrite-library-repeat-", tags[si]),
libav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(archive, readfile(libout))
&& same(iface, readfile(strings.concat(libout, ".wwi"))));
if (si == 0) {
librarybytes = strings.dup(archive);
interfacebytes = strings.dup(iface);
} else {
assert(same(librarybytes, archive)
&& same(interfacebytes, iface));
};
rewritefile(strings.concat(libout, ".wwi"), "caller-interface\n");
runcommandenv(root, strings.concat("overwrite-library-reject-", tags[si]),
libav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(libout), archive));
assert(same(readfile(strings.concat(libout, ".wwi")),
"caller-interface\n"));
if (si == 0) { librarydiag = strings.dup(out.stderr); }
else { assert(same(librarydiag, out.stderr)); };
rewritefile(libout, "caller-archive\n");
rewritefile(strings.concat(libout, ".wwi"), iface);
runcommandenv(root, strings.concat("overwrite-library-archive-",
tags[si]), libav, env,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(libout), "caller-archive\n"));
assert(same(readfile(strings.concat(libout, ".wwi")), iface));
if (si == 0) { libraryarchivediag = strings.dup(out.stderr); }
else { assert(same(libraryarchivediag, out.stderr)); };
// Compile-only installs check after linking. Running retention executes
// the private test first; a successful run then checks, while a failed
// run never installs or diagnoses the caller destination.
if (os.exists(testbin)) { rewritefile(testbin, "caller-test\n"); }
else { writefile(testbin, "caller-test\n"); };
let testcompileav: []str = [driver(drivers[si]), "test", "-c", "-w",
testwork, "-I", source, "-o", testbin, "pkg.check"];
runcommandenv(root, strings.concat("overwrite-test-c-", tags[si]),
testcompileav, env,
(90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(testbin), "caller-test\n"));
if (si == 0) { testcompilediag = strings.dup(out.stderr); }
else { assert(same(testcompilediag, out.stderr)); };
rewritefile(testbin, "");
runcommandenv(root, strings.concat("overwrite-test-empty-", tags[si]),
testcompileav, env,
(90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let compiledtest: str = readfile(testbin);
assert(strings.hasprefix(compiledtest, "\x7fELF"));
if (si == 0) { testbytes = strings.dup(compiledtest); }
else { assert(same(testbytes, compiledtest)); };
rewritefile(testbin, "caller-running-test\n");
let testrunav: []str = [driver(drivers[si]), "test", "-w", testwork,
"-I", source, "-o", testbin, "pkg.check"];
runcommandenv(root, strings.concat("overwrite-test-run-", tags[si]),
testrunav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stdout, "checked.guarded_run ... ok\n"));
assert(same(readfile(testbin), "caller-running-test\n"));
if (si == 0) { testrundiag = strings.dup(out.stderr); }
else { assert(same(testrundiag, out.stderr)); };
rewritefile(testbin, compiledtest);
runcommandenv(root, strings.concat("overwrite-test-elf-", tags[si]),
testrunav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "checked.guarded_run ... ok\n"));
let beforefailure: str = readfile(testbin);
let failav: []str = [driver(drivers[si]), "test", "-w", testwork,
"-I", source, "-o", testbin, "pkg.failed"];
runcommandenv(root, strings.concat("overwrite-test-fail-", tags[si]),
failav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stdout, "failed.failed_run ... FAIL (exit 1)\n"));
assert(out.stderr.len == 0 && same(readfile(testbin), beforefailure));
assert(!os.exists(strings.concat(testbin, ".new"))
&& !os.exists(strings.concat(testbin, ".install")));
let testdirav: []str = [driver(drivers[si]), "test", "-c", "-w",
testdirwork, "-I", source, "-o", strings.concat(testdir, "/"),
"pkg.check"];
runcommandenv(root, strings.concat("overwrite-test-dir-", tags[si]),
testdirav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "already exists and is a directory"));
assert(same(readfile(strings.concat(testchild, "/marker")),
"test-directory\n"));
assert(!directoryhasfragment(testdir, ".wwtxn."));
if (si == 0) { testdirdiag = strings.dup(out.stderr); }
else { assert(same(testdirdiag, out.stderr)); };
// Assembly and exact-null routes have no public install action.
let asmout: str = strings.concat(root, "/assembly-", tags[si]);
writefile(asmout, "assembly-sentinel\n");
let asmav: []str = [driver(drivers[si]), "build", "-S", "-w",
work, "-I", source, "-o", asmout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-asm-", tags[si]), asmav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(readfile(asmout), "assembly-sentinel\n"));
let nullav: []str = [driver(drivers[si]), "build", "-I", source,
"-o", "/dev/null", "cmd.app"];
runcommandenv(root, strings.concat("overwrite-null-", tags[si]), nullav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
si += 1;
};
clean(root);
};
// A single directory root used to bypass the package coordinator and treat // A single directory root used to bypass the package coordinator and treat
// every non-null -o spelling as one file. Exercise the Go output-directory // every non-null -o spelling as one file. Exercise the Go output-directory
// branch at that dispatch boundary while the established graph transaction // branch at that dispatch boundary while the established graph transaction