ww: protect public install outputs
This commit is contained in:
209
cmd/ww/main.c
209
cmd/ww/main.c
@@ -1309,6 +1309,7 @@ struct sepproduct {
|
||||
int directory_product;
|
||||
int no_tests;
|
||||
int build_action; /* loaded product retained in the action list */
|
||||
int public_out;
|
||||
int context;
|
||||
int root;
|
||||
int variant_root; /* retained single-unit root outside directory products */
|
||||
@@ -4983,6 +4984,7 @@ struct septxnentry {
|
||||
char *backup;
|
||||
int had_old;
|
||||
int installed;
|
||||
int public_output;
|
||||
};
|
||||
|
||||
struct septxn {
|
||||
@@ -4991,7 +4993,8 @@ struct septxn {
|
||||
};
|
||||
|
||||
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) {
|
||||
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->dst = strdup(dst);
|
||||
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) {
|
||||
sep_fail_nomem();
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
sep_txn_discard(struct septxn *tx)
|
||||
{
|
||||
@@ -5049,8 +5065,87 @@ sep_txn_free(struct septxn *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
|
||||
* 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
|
||||
sep_txn_commit(struct septxn *tx)
|
||||
{
|
||||
@@ -5060,6 +5155,10 @@ sep_txn_commit(struct septxn *tx)
|
||||
tx->v[i].stage);
|
||||
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++)
|
||||
if (path_exists_nofollow(tx->v[i].backup) != 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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
|
||||
* package universe, compile the dependency-first union once, then link each
|
||||
* root from its own complete reachable archive closure. The dependency-first
|
||||
@@ -6713,11 +6858,14 @@ prepare_transaction:
|
||||
}
|
||||
for (int i = 0; i < nproducts; i++) {
|
||||
if (products[i].stage_out != NULL
|
||||
&& sep_txn_add(&tx, products[i].stage_out,
|
||||
products[i].out) < 0)
|
||||
&& (products[i].public_out
|
||||
? 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;
|
||||
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)
|
||||
goto request_fail;
|
||||
if (products[i].stage_iface != NULL) {
|
||||
@@ -6725,7 +6873,7 @@ prepare_transaction:
|
||||
int on = snprintf(outiface, sizeof outiface, "%s.wwi",
|
||||
products[i].out);
|
||||
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)
|
||||
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,
|
||||
int require_command, int is_test,
|
||||
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)
|
||||
{
|
||||
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,
|
||||
.variant = root_variant,
|
||||
.build_action = 1,
|
||||
.public_out = public_output,
|
||||
.root = -1,
|
||||
.variant_root = -1,
|
||||
.support = -1,
|
||||
@@ -7225,7 +7375,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, NULL, NULL, 0);
|
||||
emit_asm, 0, 0, workdir, NULL, NULL, 0);
|
||||
int cleanfail = 0;
|
||||
if (unlink(tmp) != 0 && errno != ENOENT) {
|
||||
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,
|
||||
&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);
|
||||
free(incs);
|
||||
return rc;
|
||||
@@ -7287,7 +7437,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, NULL, NULL, 0);
|
||||
0, 1, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, 0, NULL, NULL, NULL, 0);
|
||||
free(incs);
|
||||
if (buildrc != 0) {
|
||||
if (unlink(tmp) != 0 && errno != ENOENT)
|
||||
@@ -7334,6 +7484,9 @@ do_run(int argc, char **argv)
|
||||
static int
|
||||
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;
|
||||
struct sepproduct *products = NULL;
|
||||
int nproducts = 0, productcap = 0;
|
||||
@@ -7475,7 +7628,10 @@ do_test(int argc, char **argv)
|
||||
const char *publish = argv[++i];
|
||||
const char *status = argv[++i];
|
||||
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 has_production = strcmp(production, "-") != 0;
|
||||
int has_internal = strcmp(internal, "-") != 0;
|
||||
@@ -7527,6 +7683,7 @@ do_test(int argc, char **argv)
|
||||
products[nproducts].no_tests = test_product
|
||||
&& !has_internal && !has_external;
|
||||
products[nproducts].build_action = 1;
|
||||
products[nproducts].public_out = public_build_product;
|
||||
products[nproducts].root = -1;
|
||||
products[nproducts].variant_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];
|
||||
const char *outp;
|
||||
int owntmp = (!outstem[0] || discard_output) && !workdir[0];
|
||||
if (outstem[0] && !discard_output) outp = outstem;
|
||||
else if (workdir[0]) {
|
||||
int retain_output = outstem[0] && !discard_output;
|
||||
int deferred_install = retain_output && !compileonly && !emit_asm;
|
||||
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
|
||||
* way it owns the package artifacts. */
|
||||
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,
|
||||
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 (owntmp && unlink(outp) != 0 && errno != ENOENT)
|
||||
fputs("ww: cannot remove temporary output\n", stderr);
|
||||
@@ -7837,6 +7998,9 @@ do_test(int argc, char **argv)
|
||||
return cleanfail ? 1 : 0;
|
||||
}
|
||||
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) {
|
||||
fputs("ww: cannot remove temporary output\n", stderr);
|
||||
if (rc == 0) rc = 1;
|
||||
@@ -7860,9 +8024,12 @@ do_test(int argc, char **argv)
|
||||
}
|
||||
char tmpdir[PATH_MAX] = {0}, tmp[PATH_MAX];
|
||||
const char *outp;
|
||||
int owntmp = (!outstem[0] || discard_output) && !workdir[0];
|
||||
if (outstem[0] && !discard_output) outp = outstem;
|
||||
else if (workdir[0]) {
|
||||
int retain_output = outstem[0] && !discard_output;
|
||||
int deferred_install = retain_output && !compileonly && !emit_asm;
|
||||
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);
|
||||
if (tn < 0 || (size_t)tn >= sizeof tmp) {
|
||||
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,
|
||||
outstem[0] && !discard_output ? outstem : tmp,
|
||||
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 (owntmp && unlink(outp) != 0 && errno != ENOENT)
|
||||
fputs("ww: cannot remove temporary output\n", stderr);
|
||||
@@ -7905,6 +8073,9 @@ do_test(int argc, char **argv)
|
||||
return cleanfail ? 1 : 0;
|
||||
}
|
||||
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) {
|
||||
fputs("ww: cannot remove temporary output\n", stderr);
|
||||
if (rc == 0) rc = 1;
|
||||
|
||||
@@ -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 pinned action dependencies: the run consumes the temporary link action,
|
||||
not the install action. Duplicate names are likewise a materialization
|
||||
collision, not package identity. Go's sources do not assert that a set of
|
||||
retained binaries is installed as one rollback transaction; WW keeps its
|
||||
existing stronger request-wide transaction while matching the observable
|
||||
accepted, rejected, and preserved outputs. No installed host Go behavior was
|
||||
used as authority.
|
||||
collision, not package identity. Section 11.31 completes the later install
|
||||
dependency: compile-only products retain the request transaction, while a
|
||||
running retained product installs independently only after its successful run.
|
||||
No installed host Go behavior was used as authority.
|
||||
|
||||
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`
|
||||
@@ -6268,23 +6267,24 @@ performs ordinary production validation, reports `[no tests]`, and creates no
|
||||
binary or otherwise-unused output directory. Successful compile-only products
|
||||
are silent, matching Go's no-op print action.
|
||||
|
||||
After linking, the driver copies the private runnable bytes to a distinct
|
||||
`.new` inode opened with executable mode `0777` subject to umask. Temporary
|
||||
runnable, retained copy, statuses, changed persistent actions, tool records,
|
||||
and stamp then enter the existing one-request transaction. All producers and
|
||||
linkers complete before installation. Any load, compile, assemble, archive,
|
||||
link, stage, or install failure preserves old retained binaries and persistent
|
||||
bytes, discards all stages, removes cold scratch, and rolls back only output
|
||||
prefixes created by that request. Occupied or dangling `.new` paths reject
|
||||
before tools and are never overwritten.
|
||||
For `-c`, the driver copies the private runnable bytes to a distinct `.new`
|
||||
inode opened with executable mode `0777` subject to umask. Temporary runnable,
|
||||
retained copy, statuses, changed persistent actions, tool records, and stamp
|
||||
then enter the existing one-request transaction. All producers and linkers
|
||||
complete before installation. Any load, compile, assemble, archive, link,
|
||||
stage, or install failure preserves old retained binaries and persistent bytes,
|
||||
discards all stages, removes cold scratch, and rolls back only output prefixes
|
||||
created by that request. Occupied or dangling `.new` paths reject before tools
|
||||
and are never overwritten.
|
||||
|
||||
Execution begins only after publication commits. Assertion failure, signal,
|
||||
timeout, or child-setup failure therefore leaves an explicitly retained binary
|
||||
while retaining the established stdout/stderr result routing and sibling
|
||||
isolation. Parallel products stage independent runnable/copy pairs; the shared
|
||||
transaction prevents partial sibling publication and canonical result emission
|
||||
order remains unchanged. Direct invocation of a retained binary continues to
|
||||
inherit caller cwd, environment, and separate standard descriptors.
|
||||
For running `-o`, the build transaction commits only the private runnable,
|
||||
status, and semantic actions. The coordinator executes that runnable and, on a
|
||||
successful result, invokes the selected driver stage's public install action.
|
||||
Assertion failure, signal, timeout, interruption, or child-setup failure skips
|
||||
that action and preserves any prior binary. Successful parallel products
|
||||
install independently after their runs; canonical result emission order stays
|
||||
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
|
||||
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
|
||||
non-directory rejection; occupied stages; serial and parallel sibling
|
||||
publication; injected late-link rollback over old files and newly created
|
||||
parents; runtime-failure retention; persistent cold/warm/invalidation behavior;
|
||||
diagnostic equality; retained binary byte identity; and absence of `.new`
|
||||
residue. Existing package tests continue to own all action/test variants,
|
||||
parents; runtime-failure preservation; persistent cold/warm/invalidation
|
||||
behavior; diagnostic equality; retained binary byte identity; and absence of
|
||||
`.new` residue. Existing package tests continue to own all action/test variants,
|
||||
graph identity, output ordering, cwd/environment/stdin, timeout, and broader
|
||||
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
|
||||
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 119–235](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 248–261](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 1904–2000](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 2118–2150](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 459–558](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 1257–1364](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 72–207](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 1–20](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 1–11](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 47–57 and 64–76](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
|
||||
|
||||
Five candidates were developed as coherent systems, not as feature bins.
|
||||
|
||||
33
docs/spec.md
33
docs/spec.md
@@ -335,6 +335,22 @@ ImportPath = ident { "." ident } .
|
||||
output. Output paths and directory metadata never become package, import,
|
||||
graph, action,
|
||||
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
|
||||
no-main rejection, but it reaches no install action: destination length,
|
||||
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.
|
||||
|
||||
The retained file is an executable, byte-identical copy of the private
|
||||
runnable. It joins package artifacts and statuses in the request-wide atomic
|
||||
publication transaction. Build, link, stage, or install failure preserves old
|
||||
destinations and removes temporary stages and invocation-created output
|
||||
prefixes. Test execution starts only after that transaction commits, so a
|
||||
runtime failure leaves an explicitly retained binary. A no-test product
|
||||
publishes no binary and does not create a directory solely for one.
|
||||
runnable. A compile-only retained binary joins package artifacts and statuses
|
||||
in the request-wide atomic publication transaction. Build, link, stage, or
|
||||
install failure preserves old destinations and removes temporary stages and
|
||||
invocation-created output prefixes. For a running `-o` request, the private
|
||||
binary executes first. Only a successful run enters the guarded install
|
||||
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
|
||||
retention without changing publication identity or introducing a test-result
|
||||
cache.
|
||||
|
||||
@@ -258,11 +258,25 @@ install-only destination validation or output-directory creation.
|
||||
|
||||
The retained executable is byte-identical to the temporary runnable and has
|
||||
executable mode `0777` filtered by the caller's umask, but it is never the path
|
||||
executed by the coordinator. Publication participates in the driver's one
|
||||
request-wide transaction: producer, linker, staging, or installation failure
|
||||
preserves every prior destination and removes stages and newly created output
|
||||
prefixes. A runtime failure occurs after commit and therefore leaves the saved
|
||||
binary. The language runtime owns individual `@test` functions.
|
||||
executed by the coordinator. Compile-only publication participates in the
|
||||
driver's one request-wide transaction: producer, linker, staging, or
|
||||
installation failure preserves every prior destination and removes stages and
|
||||
newly created output prefixes. A running retained request instead withholds
|
||||
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
|
||||
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
|
||||
that exact tree on every later success or failure. Directory-package test
|
||||
plans instead keep their cold semantic-action scratch and runnable binary
|
||||
inside the coordinator's temporary root; only the optional retained executable
|
||||
escapes through the transaction above. `ww run` and no-output single-file
|
||||
inside the coordinator's temporary root; a compile-only retained executable
|
||||
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
|
||||
scratch and their temporary executable beneath one freshly acquired directory,
|
||||
remove both after every build result, and make cleanup failure fail the
|
||||
|
||||
@@ -39,9 +39,13 @@ type pkggroup = struct {
|
||||
bin: str,
|
||||
buildok: str,
|
||||
runoutput: str,
|
||||
installoutput: str,
|
||||
state: i32,
|
||||
runstartfailed: bool,
|
||||
runres: exec.result,
|
||||
publicbin: bool,
|
||||
installattempted: bool,
|
||||
installres: exec.result,
|
||||
};
|
||||
|
||||
type pkgplan = struct {
|
||||
@@ -64,6 +68,7 @@ type pkgplan = struct {
|
||||
outputcollisionbase: str,
|
||||
outputcollisiondir: str,
|
||||
suppressbuildreports: bool,
|
||||
deferpublish: bool,
|
||||
};
|
||||
|
||||
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");
|
||||
return false;
|
||||
};
|
||||
g.publicbin = false;
|
||||
g.installattempted = false;
|
||||
if (buildonly) {
|
||||
if (outputdir && strings.compare(g.pkg, "main") == 0
|
||||
&& !p.outputpatherror && !p.emitasm) {
|
||||
if (!pkgjoinpath(outname, g.basename, &g.bin)) { return false; };
|
||||
} else if (outname.len != 0 && !outputdir) { g.bin = outname; }
|
||||
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.test")) { return false; };
|
||||
};
|
||||
if (!pkgstring(&g.runoutput, g.root, "/test.output")
|
||||
|| !pkgstring(&g.installoutput, g.root, "/install.output")
|
||||
|| !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; };
|
||||
i += 1;
|
||||
};
|
||||
@@ -1484,7 +1496,8 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
|
||||
for (i < p.end) {
|
||||
let g: *pkggroup = &groups[i];
|
||||
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"); };
|
||||
append(ba, g.pkg);
|
||||
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, "-"); };
|
||||
append(ba, g.dir);
|
||||
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, "-"); };
|
||||
append(ba, g.buildok);
|
||||
i += 1;
|
||||
@@ -1591,6 +1604,31 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
|
||||
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 = {
|
||||
if (buildonly || g.notests) { return pkgisreg(g.buildok); };
|
||||
return pkgisreg(g.buildok) && pkgisreg(g.bin);
|
||||
@@ -1602,6 +1640,35 @@ fn pkgrunok(g: *pkggroup) bool = {
|
||||
&& 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 = {
|
||||
if (g.notests) {
|
||||
pkgput(os.STDOUT_FILENO, "? ");
|
||||
@@ -1653,8 +1720,9 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup,
|
||||
failed += 1;
|
||||
} else if (buildonly) {
|
||||
void;
|
||||
} else if (!pkgemitgroup(&groups[i], compileonly)) {
|
||||
failed += 1;
|
||||
} else {
|
||||
if (!pkgemitgroup(&groups[i], compileonly)) { failed += 1; };
|
||||
if (!compileonly && !pkgemitinstall(&groups[i])) { failed += 1; };
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
@@ -2385,11 +2453,12 @@ export fn packagecommand(args: []str) int = {
|
||||
plan.outputcollisiondir = outputcollisiondir;
|
||||
plan.suppressbuildreports = buildonly
|
||||
&& (outputdir || defaultoutputdir.len != 0);
|
||||
plan.deferpublish = !buildonly && !compileonly && testretain && !testnull;
|
||||
append(plans, plan);
|
||||
let createdir: str = "";
|
||||
if (buildonly && outputdir) {
|
||||
createdir = outname;
|
||||
} else if (!buildonly && testretain && !testnull) {
|
||||
} else if (!buildonly && compileonly && testretain && !testnull) {
|
||||
i = 0;
|
||||
for (i < groups.len) {
|
||||
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); };
|
||||
};
|
||||
|
||||
// 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;
|
||||
for (i < plans.len) {
|
||||
failed += pkgemitplan(&plans[i], groups, compileonly, buildonly);
|
||||
|
||||
@@ -1515,6 +1515,7 @@ type sepproduct = struct {
|
||||
directoryproduct: bool,
|
||||
notests: bool,
|
||||
buildaction: bool, // loaded product retained in the action list
|
||||
publicout: bool,
|
||||
context: i32,
|
||||
root: i32,
|
||||
variantroot: i32,
|
||||
@@ -6134,6 +6135,7 @@ type septxnentry = struct {
|
||||
backup: *u8,
|
||||
hadold: bool,
|
||||
installed: bool,
|
||||
publicoutput: bool,
|
||||
};
|
||||
|
||||
fn sepalloctxnentries(cap: i32) ([]septxnentry | nomem) = {
|
||||
@@ -6207,8 +6209,8 @@ fn septxnbackup(dst: *u8) *u8 = {
|
||||
return buf.ptr;
|
||||
};
|
||||
|
||||
fn septxnadd(entries: *[]septxnentry, n: *i32,
|
||||
stage: *u8, dst: *u8) bool = {
|
||||
fn septxnaddmode(entries: *[]septxnentry, n: *i32,
|
||||
stage: *u8, dst: *u8, publicoutput: bool) bool = {
|
||||
if (cstreq(stage, dst)) {
|
||||
cerrpath("ww: transaction path collision: ", dst, "\n");
|
||||
return false;
|
||||
@@ -6253,10 +6255,21 @@ fn septxnadd(entries: *[]septxnentry, n: *i32,
|
||||
(*entries)[*n].backup = backup;
|
||||
(*entries)[*n].hadold = false;
|
||||
(*entries)[*n].installed = false;
|
||||
(*entries)[*n].publicoutput = publicoutput;
|
||||
*n += 1;
|
||||
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,
|
||||
g: *sepgraph, pi: i32, scratch: *u8,
|
||||
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; };
|
||||
};
|
||||
|
||||
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 = {
|
||||
let i: i32 = 0;
|
||||
let valid: bool = true;
|
||||
@@ -6283,6 +6381,17 @@ fn septxncommit(entries: []septxnentry, n: i32) bool = {
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
if (valid) {
|
||||
i = 0;
|
||||
for (i < n) {
|
||||
if (entries[i].publicoutput
|
||||
&& !sepcheckdstoverwrite(entries[i].dst)) {
|
||||
valid = false;
|
||||
break;
|
||||
};
|
||||
i += 1;
|
||||
};
|
||||
};
|
||||
if (valid) {
|
||||
i = 0;
|
||||
for (i < n) {
|
||||
@@ -6614,6 +6723,50 @@ fn septxnrelease(entries: *[]septxnentry, n: i32) void = {
|
||||
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 = {
|
||||
septxndiscard(*entries, n);
|
||||
septxnrelease(entries, n);
|
||||
@@ -6924,19 +7077,24 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8,
|
||||
producti = 0;
|
||||
for (producti < nproducts) {
|
||||
if (products[producti].stageout != nil
|
||||
&& !septxnadd(&entries, &ntxn, products[producti].stageout,
|
||||
products[producti].out)) {
|
||||
return sepfinishfail(&entries, ntxn);
|
||||
&& ((products[producti].publicout
|
||||
&& !septxnaddpublic(&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
|
||||
&& !septxnadd(&entries, &ntxn, products[producti].stagepublish,
|
||||
&& !septxnaddpublic(&entries, &ntxn,
|
||||
products[producti].stagepublish,
|
||||
products[producti].publish)) {
|
||||
return sepfinishfail(&entries, ntxn);
|
||||
};
|
||||
if (products[producti].stageiface != nil) {
|
||||
let outiface: *u8 = sepappendlit(products[producti].out, ".wwi");
|
||||
if (outiface == nil
|
||||
|| !septxnadd(&entries, &ntxn,
|
||||
|| !septxnaddpublic(&entries, &ntxn,
|
||||
products[producti].stageiface, outiface)) {
|
||||
return sepfinishfail(&entries, ntxn);
|
||||
};
|
||||
@@ -8213,7 +8371,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
|
||||
objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32,
|
||||
requirecommand: i32, istest: i32,
|
||||
rootvariant: i32, testpackage: *u8, emitasm: i32,
|
||||
keepscratch: i32, workdir: *u8, createoutputdir: *u8,
|
||||
publicoutput: bool, keepscratch: i32, workdir: *u8,
|
||||
createoutputdir: *u8,
|
||||
defaultoutputdir: *u8, outputpatherror: bool) i32 = {
|
||||
let scratch: *u8 = nil;
|
||||
let g: *sepgraph = nil;
|
||||
@@ -8235,6 +8394,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
|
||||
product.directoryproduct = false;
|
||||
product.notests = false;
|
||||
product.buildaction = true;
|
||||
product.publicout = publicoutput;
|
||||
product.root = -1;
|
||||
product.variantroot = -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,
|
||||
outp, outp, incs.ptr, &lf,
|
||||
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 cleanrc: i32 = os.remove(pathstr(outp));
|
||||
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,
|
||||
out, objstem, incs.ptr, &lf,
|
||||
publishpackage, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
|
||||
emitasm, 1i32, workdir, createoutputdir, defaultoutputdir,
|
||||
emitasm, true, 1i32, workdir, createoutputdir, defaultoutputdir,
|
||||
outputpatherror);
|
||||
};
|
||||
|
||||
@@ -8893,7 +9053,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
if (buildonesep(selfdir, resolved, isdir, rootidentity,
|
||||
outp, outp, incs.ptr, &lf,
|
||||
0i32, 1i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
|
||||
0i32, 0i32, nil, nil, nil, false) != 0) {
|
||||
0i32, false, 0i32, nil, nil, nil, false) != 0) {
|
||||
let cleanrc: i32 = os.remove(pathstr(outp));
|
||||
if (cleanrc != 0 && cleanrc != -2i32) {
|
||||
cerr("ww: cannot remove temporary output\n");
|
||||
@@ -8958,10 +9118,12 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
|
||||
let owntmp: bool = false;
|
||||
let retainout: bool = outstem != nil
|
||||
&& !cstreqlit(outstem, "/dev/null");
|
||||
if (retainout) {
|
||||
let deferredinstall: bool = retainout && compileonly == 0
|
||||
&& emitasm == 0;
|
||||
if (retainout && !deferredinstall) {
|
||||
outp = outstem;
|
||||
objstem = outstem;
|
||||
} else { if (workdir != nil) {
|
||||
} else { if (workdir != nil && !deferredinstall) {
|
||||
// The workdir owns the persistent test binary the same way it
|
||||
// owns the package artifacts.
|
||||
outp = joinpathlit(workdir, "main");
|
||||
@@ -8975,6 +9137,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
|
||||
};
|
||||
outp = joinpathlit(tmp.ptr, "main");
|
||||
objstem = outp;
|
||||
if (retainout) { objstem = outstem; };
|
||||
}; };
|
||||
// E3-C1: separate compilation is the sole build path (task #87).
|
||||
let lf: lflags;
|
||||
@@ -8986,7 +9149,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
|
||||
if (retainout) { keep = 1; };
|
||||
let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf,
|
||||
0i32, 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil,
|
||||
emitasm, keep, workdir, nil, nil, false);
|
||||
emitasm, retainout && !deferredinstall, keep, workdir,
|
||||
nil, nil, false);
|
||||
if (bres != 0) {
|
||||
if (owntmp) {
|
||||
let cleanrc: i32 = os.remove(pathstr(outp));
|
||||
@@ -9037,6 +9201,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
|
||||
rc = -1;
|
||||
};
|
||||
}; };
|
||||
if (rc == 0 && deferredinstall
|
||||
&& sepinstalltestoutput(outp, outstem) != 0) { rc = 1; };
|
||||
if (owntmp) {
|
||||
let cleanrc: i32 = os.remove(pathstr(outp));
|
||||
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 = {
|
||||
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
|
||||
// transitive imports (e.g. 905_nkname asttest → tok); coupled to the
|
||||
// -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 status: *u8 = argv[i + 9];
|
||||
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 hasproduction: bool = !cstreqlit(production, "-");
|
||||
let hasinternal: bool = !cstreqlit(internal, "-");
|
||||
@@ -9248,6 +9421,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
|
||||
product.directoryproduct = true;
|
||||
product.notests = testproduct && !hasinternal && !hasexternal;
|
||||
product.buildaction = true;
|
||||
product.publicout = publicbuildproduct;
|
||||
product.root = -1;
|
||||
product.variantroot = -1;
|
||||
product.productionroot = -1;
|
||||
|
||||
@@ -12443,7 +12443,6 @@ fn runtimepath(relative: str) str = {
|
||||
let rollbackdiag: str = "";
|
||||
let missingdiag: str = "";
|
||||
let occupieddiag: str = "";
|
||||
let failurebytes: str = "";
|
||||
let persistbasebytes: str = "";
|
||||
let persistchangedbytes: str = "";
|
||||
let baseenv: []str = os.getenvs();
|
||||
@@ -12595,29 +12594,23 @@ fn runtimepath(relative: str) str = {
|
||||
" [no tests]\n")));
|
||||
assert(out.stderr.len == 0 && !os.exists(noneroot));
|
||||
|
||||
// Publication commits before execution. A failing test therefore leaves
|
||||
// a runnable retained binary while its product failure stays on stdout.
|
||||
// Pinned builderTest makes the install action depend on the run action.
|
||||
// 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],
|
||||
".test");
|
||||
writefile(failurebin, "failure-sentinel\n");
|
||||
let failureav: []str = [driver(stages[si]), "test", "-I", suite,
|
||||
"-o", failurebin, failed];
|
||||
runcommandenvdir(root, strings.concat("publish-runtime-", tags[si]),
|
||||
failureav, env, root,
|
||||
(60i64 * (time.second: i64)): time.duration, &out);
|
||||
expectexit(&out, 1);
|
||||
assert(os.exists(failurebin) && !os.exists(strings.concat(failurebin,
|
||||
".new")));
|
||||
assert(same(readfile(failurebin), "failure-sentinel\n"));
|
||||
assert(!os.exists(strings.concat(failurebin, ".new")));
|
||||
assert(has(out.stdout,
|
||||
"failed_decl.runtime_failure ... FAIL (exit 1)\n"));
|
||||
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
|
||||
// and presentation policy. Compile-only can cold-build and invalidate;
|
||||
@@ -12681,6 +12674,377 @@ fn runtimepath(relative: str) str = {
|
||||
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
|
||||
// every non-null -o spelling as one file. Exercise the Go output-directory
|
||||
// branch at that dispatch boundary while the established graph transaction
|
||||
|
||||
Reference in New Issue
Block a user