From d5fdc27c0a03eee272ca381b9b0753531f64e693 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 13 Aug 2026 23:42:37 +0900 Subject: [PATCH] ww: implement recursive local package patterns --- Makefile | 3 +- cmd/ww/main.c | 429 +++++++++++-- internal/wwpackage/isprint.ww | 227 +++++++ internal/wwpackage/package.ww | 1125 +++++++++++++++++++++++++++------ selfhost/cmd/ww/main.ww | 552 +++++++++++++--- 5 files changed, 1976 insertions(+), 360 deletions(-) create mode 100644 internal/wwpackage/isprint.ww diff --git a/Makefile b/Makefile index 39cc3142..d7057fa3 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,8 @@ RT_OBJ = $(RT_S:rt/%.s=$(OBJ)/rt/%.o) $(RT_WW:rt/%.ww=$(OBJ)/rt/%.o) WWTEST_BIN = $(BIN)/wwtest WWTEST_SRC = cmd/wwtest/wwtest.ww \ - internal/wwpackage/package.ww lib/os/exec/exec.ww \ + internal/wwpackage/package.ww internal/wwpackage/isprint.ww \ + lib/os/exec/exec.ww \ lib/ascii/ascii.ww lib/bytes/bytes.ww \ lib/crypto/math/math.ww lib/crypto/sha256/sha256.ww \ lib/endian/big.ww lib/endian/little.ww lib/endian/network.ww \ diff --git a/cmd/ww/main.c b/cmd/ww/main.c index d50eb562..99858899 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -24,9 +24,9 @@ static const char *usage = "usage: ww [-V] [args...]\n" " -V print version and exit\n" -" build [-S] [-w DIR] [-I DIR] [-o FILE] [path] build a local package graph\n" +" build [-S] [-w DIR] [-I DIR] [-o FILE] [path ...] build local package graphs\n" " run [path] ... build then exec, passing extra args to the program\n" -" test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n" +" test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n" " version print version and exit\n" "\n" " path forms:\n" @@ -34,13 +34,14 @@ static const char *usage = " foo search cwd, -I dirs, then the source library for foo.ww or foo/\n" " lib/foo directory: build its package sources\n" " -o publishes a non-main archive FILE + FILE.wwi\n" -" lib/... every package under lib, recursively (test only)\n" +" lib/... every eligible package under lib, recursively\n" " . build the cwd's .ww\n"; static char *self_dir; static const char *self_path; static char *sep_sprintf(const char *, ...); static int sep_reserve(void **, int *, int, size_t); +static int sep_fail_size(void); static const char * envpath(const char *name) @@ -115,28 +116,38 @@ run_test_bin(const char *bin, const char *pattern) * package root through `ww test -c ... package.ww`, so that file boundary also * prevents delegation recursion. */ static int -exec_package_tests(int argc, char **argv, const char *target, - const char *resolved, const char *root_identity, int add_dot) +exec_package_command(int argc, char **argv, const char *target, + const char *resolved, const char *root_identity, int add_dot, + int build_only) { const char *override = getenv("WW_WWTEST"); - char fallback[PATH_MAX]; - const char *prog = override && override[0] ? override : fallback; - if (prog == fallback) { - int pn = snprintf(fallback, sizeof fallback, "%s/wwtest", self_dir); - if (pn < 0 || (size_t)pn >= sizeof fallback) { - fputs("ww test: package coordinator path is too long\n", stderr); - return 1; - } + char *fallback = NULL; + const char *prog = override && override[0] ? override : NULL; + if (prog == NULL) { + fallback = sep_sprintf("%s/wwtest", self_dir); + if (fallback == NULL) return 1; + prog = fallback; } - char **xargv = calloc((size_t)argc + 8, sizeof *xargv); + if (argc < 0 || argc > INT_MAX - 10) { + free(fallback); + sep_fail_size(); + return 1; + } + char **xargv = calloc((size_t)argc + 10, sizeof *xargv); if (xargv == NULL) { - fputs("ww test: cannot allocate package coordinator arguments\n", - stderr); + fprintf(stderr, + "ww %s: cannot allocate package coordinator arguments\n", + build_only ? "build" : "test"); + free(fallback); return 1; } int n = 0, dotted = 0; xargv[n++] = (char *)prog; xargv[n++] = "package"; + if (build_only) { + xargv[n++] = "--ww-operation"; + xargv[n++] = "build"; + } xargv[n++] = "--ww-driver"; xargv[n++] = (char *)self_path; if (root_identity != NULL) { @@ -154,8 +165,10 @@ exec_package_tests(int argc, char **argv, const char *target, if (add_dot && !dotted) xargv[n++] = "."; xargv[n] = NULL; execv(prog, xargv); /* inherit the caller's environment */ - fputs("ww test: cannot exec package test coordinator\n", stderr); + fprintf(stderr, "ww %s: cannot exec package coordinator\n", + build_only ? "build" : "test"); free(xargv); + free(fallback); return 1; } @@ -558,6 +571,7 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, const char *nm = ent->d_name; size_t nl = strlen(nm); if (nl <= 3) continue; + if (nm[0] == '.' || nm[0] == '_') continue; if (strcmp(nm + nl - 3, ".ww") != 0) continue; int is_test = nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0; if (variant == SEP_VARIANT_PRODUCTION && is_test) continue; @@ -573,7 +587,30 @@ enumerate_dir_ww(const char *dirpath, int variant, const char *test_package, return -2; } struct stat st; - if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) { + if (lstat(path, &st) != 0) { + fprintf(stderr, + "ww: %s: package source is not a regular file\n", path); + source_list_free(prod, nprod); + source_list_free(tests, ntests); + closedir(d); + *out_files = NULL; + return -2; + } + if (S_ISLNK(st.st_mode)) { + if (stat(path, &st) != 0) { + fprintf(stderr, + "ww: %s: package source is not a regular file\n", + path); + source_list_free(prod, nprod); + source_list_free(tests, ntests); + closedir(d); + *out_files = NULL; + return -2; + } + /* go/build ignores a source-shaped symlink to a directory. */ + if (S_ISDIR(st.st_mode)) continue; + } + if (!S_ISREG(st.st_mode)) { fprintf(stderr, "ww: %s: package source is not a regular file\n", path); source_list_free(prod, nprod); @@ -3553,6 +3590,16 @@ validate_package_output_path(const char *out) return 0; } +static int +validate_command_output_path(const char *out) +{ + if (out == NULL || strlen(out) + 1 > (size_t)PATH_MAX) { + fprintf(stderr, "ww: command output path is too long\n"); + return -1; + } + return 0; +} + /* A coordinator-private completion marker distinguishes a newly linked * product from a caller-owned binary left behind by an earlier invocation. */ static int @@ -3611,6 +3658,66 @@ invalidate_workdir_units(const char *scratch) return rc; } +struct sep_created_dirs { + char path[PATH_MAX]; + unsigned short offset[(PATH_MAX + 1) / 2]; + int n; +}; + +static void +sep_rollback_dirs(struct sep_created_dirs *created) +{ + while (created->n > 0) { + size_t at = created->offset[--created->n]; + char saved = created->path[at]; + created->path[at] = '\0'; + (void)rmdir(created->path); + created->path[at] = saved; + } +} + +/* Create one caller-requested directory tree after semantic preflight. Record + * exactly the prefixes minted by this call so a later pre-producer setup + * failure can roll them back without touching pre-existing caller state. */ +static int +sep_mkdirs(const char *path, mode_t mode, struct sep_created_dirs *created) +{ + memset(created, 0, sizeof *created); + size_t n = strlen(path); + if (n == 0 || n >= PATH_MAX) return -1; + char buf[PATH_MAX]; + memcpy(buf, path, n + 1); + while (n > 1 && buf[n - 1] == '/') buf[--n] = '\0'; + memcpy(created->path, buf, n + 1); + for (char *p = buf + (buf[0] == '/' ? 1 : 0); ; p++) { + if (*p != '/' && *p != '\0') continue; + char saved = *p; + *p = '\0'; + if (buf[0] != '\0') { + struct stat st; + if (stat(buf, &st) != 0) { + if (errno != ENOENT + || created->n >= (int)(sizeof created->offset + / sizeof created->offset[0]) + || mkdir(buf, mode) != 0) { + *p = saved; + sep_rollback_dirs(created); + return -1; + } + created->offset[created->n++] = + (unsigned short)(p - buf); + } else if (!S_ISDIR(st.st_mode)) { + *p = saved; + sep_rollback_dirs(created); + return -1; + } + } + *p = saved; + if (saved == '\0') break; + } + 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 @@ -3625,7 +3732,8 @@ build_one_sep_impl(const char *src, int entry_is_dir, const struct seplinkflags *linkflags, int publish_package, int require_command, int is_test, struct sepproduct *products, int nproducts, int emit_asm, - const char *workdir, char *scratchout, size_t scratchoutsz, + const char *workdir, int create_workdir, const char *create_output_dir, + char *scratchout, size_t scratchoutsz, struct sepgraph **graphout) { sep_fatal_allocation = 0; @@ -3679,10 +3787,14 @@ build_one_sep_impl(const char *src, int entry_is_dir, if (stem == NULL) return 1; const char *ostem = (objstem && objstem[0]) ? objstem : stem; int warm = workdir != NULL && workdir[0] != 0; + int workdir_exists = 0; char scratch[PATH_MAX]; if (warm) { struct stat wst; - if (stat(workdir, &wst) != 0 || !S_ISDIR(wst.st_mode)) { + int wr = stat(workdir, &wst); + if (wr == 0 && S_ISDIR(wst.st_mode)) { + workdir_exists = 1; + } else if (wr == 0 || !create_workdir || errno != ENOENT) { fprintf(stderr, "ww: workdir %s is not a directory\n", workdir); return 1; @@ -3690,7 +3802,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, /* The workdir is caller-owned and persistent: no acquisition, * no refusal, and scratchout stays empty so the wrapper never * cleans it. */ - if (strlen(workdir) + 1 > sizeof scratch) return 1; + if (strlen(workdir) + 1 > sizeof scratch) { + fprintf(stderr, "ww: workdir path is too long\n"); + return 1; + } memcpy(scratch, workdir, strlen(workdir) + 1); } else { int n = snprintf(scratch, sizeof scratch, "%s.sepwork", ostem); @@ -3930,12 +4045,18 @@ build_one_sep_impl(const char *src, int entry_is_dir, products[i].root = mainpkg; } } - int root_package = !is_test + for (int i = 0; i < nproducts; i++) { + int root = products[i].root; + if (!g->pkg[root].failed && sep_root_is_command(&g->pkg[root]) + && validate_command_output_path(products[i].out) < 0) + return 1; + } + int root_package = !is_test && nproducts == 1 && !g->pkg[products[0].root].failed && !sep_root_is_command(&g->pkg[products[0].root]); if (sep_validate_artifact_paths(g, scratch) < 0) return 1; - if (warm && sep_validate_workdir_owners(g, scratch) < 0) + if (warm && workdir_exists && sep_validate_workdir_owners(g, scratch) < 0) return 1; int *order = calloc((size_t)g->n, sizeof *order); int *stack = calloc((size_t)g->n, sizeof *stack); @@ -4004,9 +4125,30 @@ build_one_sep_impl(const char *src, int entry_is_dir, /* Product completion and persistent package state remain untouched until * all source-derived imports, contextual legality, cycles, command kind, * output paths, and action closures have passed their pre-tool checks. */ + struct sep_created_dirs created_output = {0}; + struct sep_created_dirs created_work = {0}; + if (create_output_dir != NULL && create_output_dir[0] != '\0' + && sep_mkdirs(create_output_dir, 0700, &created_output) != 0) { + fprintf(stderr, "ww: cannot create build output directory %s\n", + create_output_dir); + free(order); + return 1; + } + if (warm && !workdir_exists) { + if (!create_workdir + || sep_mkdirs(scratch, 0700, &created_work) != 0) { + fprintf(stderr, "ww: workdir %s is not a directory\n", scratch); + sep_rollback_dirs(&created_output); + free(order); + return 1; + } + workdir_exists = 1; + } if (!warm) { if (mkdir(scratch, 0755) != 0) { fprintf(stderr, "ww: cannot create scratch %s\n", scratch); + sep_rollback_dirs(&created_work); + sep_rollback_dirs(&created_output); free(order); return 1; } @@ -4251,7 +4393,19 @@ build_one_sep_impl(const char *src, int entry_is_dir, } } } - if (emit_asm) { free(order); return any_failed ? 1 : 0; } + if (emit_asm) { + for (int i = 0; i < nproducts; i++) { + int root = products[i].root; + if (g->pkg[root].failed) { any_failed = 1; continue; } + if (record_product_status(products[i].status) != 0) { + fprintf(stderr, "ww: cannot record package-build product\n"); + g->pkg[root].failed = 1; + any_failed = 1; + } + } + free(order); + return any_failed ? 1 : 0; + } if (root_package) { int root = products[0].root; if (g->pkg[root].failed) { free(order); return 1; } @@ -4274,6 +4428,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, return 1; } } + if (record_product_status(products[0].status) != 0) { + fprintf(stderr, "ww: cannot record package-build product\n"); + free(order); + return 1; + } free(order); return 0; } @@ -4303,6 +4462,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, int root = products[i].root; int variant_root = products[i].variant_root; if (g->pkg[root].failed) { any_failed = 1; continue; } + if (!is_test && !sep_root_is_command(&g->pkg[root])) { + if (record_product_status(products[i].status) != 0) { + fprintf(stderr, + "ww: cannot record package-build product\n"); + g->pkg[root].failed = 1; + any_failed = 1; + } + continue; + } for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0; int *linkorder = calloc((size_t)g->n, sizeof *linkorder); int *linkstack = calloc((size_t)g->n, sizeof *linkstack); @@ -4422,7 +4590,7 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, product.artifact = "__root"; int r = build_one_sep_impl(src, entry_is_dir, out, objstem, extra_includes, linkflags, publish_package, require_command, is_test, - &product, 1, emit_asm, workdir, scratch, + &product, 1, emit_asm, workdir, 0, NULL, scratch, sizeof scratch, &g); sep_graph_free(g); if (!keepscratch && scratch[0]) { @@ -4456,15 +4624,18 @@ build_one_sep(const char *src, int entry_is_dir, const char *root_identity, static int build_package_tests(const char *src, const char *root_identity, const char *extra_includes, const char *workdir, - struct sepproduct *products, int nproducts) + struct sepproduct *products, int nproducts, int is_test, + int publish_package, const struct seplinkflags *linkflags, int emit_asm, + int create_workdir, const char *create_output_dir) { char scratch[PATH_MAX] = {0}; struct sepgraph *g = NULL; for (int i = 0; i < nproducts; i++) products[i].identity = root_identity; int r = build_one_sep_impl(src, 1, products[0].out, - products[0].out, extra_includes, NULL, 0, 0, 1, - products, nproducts, 0, workdir, scratch, sizeof scratch, &g); + products[0].out, extra_includes, linkflags, publish_package, 0, is_test, + products, nproducts, emit_asm, workdir, create_workdir, + create_output_dir, scratch, sizeof scratch, &g); sep_graph_free(g); return r; } @@ -4607,13 +4778,18 @@ parse_build_flags(const char *cmd, int argc, char **argv, struct seplinkflags *linkflags, char *outpath, size_t outsz, char *workdir, size_t workdirsz, - const char **src_out, int *emit_asm_out) + const char **src_out, int *emit_asm_out, int *terminator_out) { *src_out = NULL; if (emit_asm_out) *emit_asm_out = 0; + if (terminator_out) *terminator_out = 0; int i = 0; for (; i < argc; i++) { - if (strcmp(argv[i], "-S") == 0) { + if (strcmp(cmd, "build") == 0 && strcmp(argv[i], "--") == 0) { + if (terminator_out) *terminator_out = 1; + i++; + break; + } else if (strcmp(argv[i], "-S") == 0) { if (emit_asm_out == NULL) { fprintf(stderr, "ww %s: unknown flag\n", cmd); return -1; @@ -4699,6 +4875,12 @@ parse_build_flags(const char *cmd, int argc, char **argv, return -1; } else if (*src_out == NULL) { *src_out = argv[i]; + /* Go's FlagSet stops at the first positional. For build, + * everything after it is package-request input, even "--". */ + if (strcmp(cmd, "build") == 0) { + i++; + break; + } } else { break; /* leave remaining argv to caller (run-args) */ } @@ -4717,14 +4899,20 @@ do_build(int argc, char **argv) char outflag[PATH_MAX] = {0}; char workdir[PATH_MAX] = {0}; int emit_asm = 0; - if (parse_build_flags("build", argc, argv, incs, incsz, + int saw_terminator = 0; + int next = parse_build_flags("build", argc, argv, incs, incsz, &linkflags, outflag, sizeof outflag, workdir, sizeof workdir, - &src, &emit_asm) < 0) { + &src, &emit_asm, &saw_terminator); + if (next < 0) { free(incs); return 2; } if (src == NULL) src = "."; + if (strstr(src, "...") != NULL || next < argc || saw_terminator) { + free(incs); + return exec_package_command(argc, argv, src, NULL, NULL, 0, 1); + } struct stat requested; int literal = stat(src, &requested) == 0; char resolved[PATH_MAX]; @@ -4770,7 +4958,7 @@ do_run(int argc, char **argv) char outflag[PATH_MAX] = {0}; /* -o accepted+ignored: run always uses the temp */ int next = parse_build_flags("run", argc, argv, incs, incsz, &linkflags, - outflag, sizeof outflag, NULL, 0, &src, NULL); + outflag, sizeof outflag, NULL, 0, &src, NULL, NULL); if (next < 0) { free(incs); return 2; } if (src == NULL) src = "."; struct stat requested; @@ -4865,6 +5053,11 @@ do_test(int argc, char **argv) char outstem[PATH_MAX] = {0}; char workdir[PATH_MAX] = {0}; int packageopts = 0; + int package_build = 0; + int package_publish = 0; + int package_create_workdir = 0; + const char *package_create_output_dir = NULL; + struct seplinkflags package_linkflags = {0}; int afterdash = 0; const char *request_identity = NULL; /* #17: an optional second positional after the target is a fnmatch @@ -4904,6 +5097,36 @@ do_test(int argc, char **argv) return 2; } request_identity = argv[++i]; + } else if (strcmp(argv[i], "--ww-package-build") == 0) { + if (package_build) { + fprintf(stderr, + "ww test: invalid --ww-package-build\n"); + return 2; + } + package_build = 1; + compileonly = 1; + } else if (strcmp(argv[i], "--ww-package-publish") == 0) { + if (package_publish) { + fprintf(stderr, + "ww test: invalid --ww-package-publish\n"); + return 2; + } + package_publish = 1; + } else if (strcmp(argv[i], "--ww-create-workdir") == 0) { + if (package_create_workdir) { + fprintf(stderr, + "ww test: invalid --ww-create-workdir\n"); + return 2; + } + package_create_workdir = 1; + } else if (strcmp(argv[i], "--ww-create-output-dir") == 0) { + if (i + 1 >= argc || package_create_output_dir != NULL + || argv[i + 1][0] == '\0') { + fprintf(stderr, + "ww test: invalid --ww-create-output-dir\n"); + return 2; + } + package_create_output_dir = argv[++i]; } else if (strcmp(argv[i], "--ww-package-test") == 0) { if (i + 5 >= argc) { fprintf(stderr, @@ -4954,6 +5177,40 @@ do_test(int argc, char **argv) nproducts++; } else if (strcmp(argv[i], "-S") == 0) { emit_asm = 1; + } else if (package_build + && strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { + if (package_linkflags.nlibs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww test: too many -l\n"); + return 2; + } + package_linkflags.libs[package_linkflags.nlibs++] = argv[i] + 2; + } else if (package_build && strcmp(argv[i], "-l") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "ww test: -l needs an argument\n"); + return 2; + } + if (package_linkflags.nlibs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww test: too many -l\n"); + return 2; + } + package_linkflags.libs[package_linkflags.nlibs++] = argv[++i]; + } else if (package_build && strcmp(argv[i], "-L") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "ww test: -L needs an argument\n"); + return 2; + } + if (package_linkflags.nlibdirs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww test: too many -L\n"); + return 2; + } + package_linkflags.libdirs[package_linkflags.nlibdirs++] = argv[++i]; + } else if (package_build + && strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) { + if (package_linkflags.nlibdirs >= SEP_MAXLFLAGS) { + fprintf(stderr, "ww test: too many -L\n"); + return 2; + } + package_linkflags.libdirs[package_linkflags.nlibdirs++] = argv[i] + 2; } else if (strcmp(argv[i], "-list") == 0) { packageopts = 1; } else if (strcmp(argv[i], "-j") == 0 || @@ -5002,7 +5259,7 @@ do_test(int argc, char **argv) } } const char *target = src ? src : "."; - if (emit_asm && !outstem[0]) { + if (emit_asm && !outstem[0] && nproducts == 0) { fprintf(stderr, "ww test: -S needs -o\n"); return 2; } @@ -5033,17 +5290,66 @@ do_test(int argc, char **argv) "ww test: package-test variant rejects package options\n"); return 2; } + if (package_build && nproducts == 0) { + fprintf(stderr, "ww test: --ww-package-build needs products\n"); + return 2; + } + if (package_publish && (!package_build || nproducts != 1)) { + fprintf(stderr, "ww test: invalid --ww-package-publish\n"); + return 2; + } + if (package_create_output_dir != NULL && !package_build) { + fprintf(stderr, "ww test: invalid private directory creation\n"); + return 2; + } + if (package_create_workdir && nproducts == 0) { + fprintf(stderr, "ww test: invalid private directory creation\n"); + return 2; + } + if (package_create_workdir && !workdir[0]) { + fprintf(stderr, "ww test: --ww-create-workdir needs -w\n"); + return 2; + } + if (!package_build && (package_linkflags.nlibdirs != 0 + || package_linkflags.nlibs != 0)) { + fprintf(stderr, "ww test: unknown flag\n"); + return 2; + } + if (package_build) { + for (int i = 0; i < nproducts; i++) { + if (products[i].variant != SEP_VARIANT_PRODUCTION) { + fprintf(stderr, + "ww test: package-build products must be production\n"); + return 2; + } + } + } + if (pattern != NULL) { + struct stat first; + if (stat(target, &first) != 0 || !S_ISREG(first.st_mode)) { + char first_resolved[PATH_MAX]; + int first_is_dir = 0; + if (stat(target, &first) == 0 + || !resolve_module(target, incs, first_resolved, + sizeof first_resolved, &first_is_dir) + || first_is_dir) { + return exec_package_command(argc, argv, src, + first_is_dir ? first_resolved : NULL, + first_is_dir ? target : request_identity, + 0, 0); + } + /* A logical import that resolves to one regular source keeps + * the historical second-positional test-name filter. */ + } + } if (nproducts != 0 && outstem[0]) { fprintf(stderr, "ww test: package-test products reject -o\n"); return 2; } - /* Go's ./... form: a trailing "..." element is a package-tree - * request for the coordinator, never a literal path — recognized - * before stat, with the directory-mode rejects. */ - size_t tlen = strlen(target); - if (strcmp(target, "...") == 0 || - (tlen >= 4 && strcmp(target + tlen - 4, "/...") == 0)) { + /* Every local spelling containing "..." is a package pattern. It enters + * request selection before literal-path or logical-import resolution. */ + if (strstr(target, "...") != NULL) { if (nproducts != 0) { fprintf(stderr, "ww test: package-test variant needs one directory\n"); @@ -5061,14 +5367,9 @@ do_test(int argc, char **argv) "ww test: -o needs -c for a package target\n"); return 2; } - if (pattern) { - fprintf(stderr, - "ww test: pattern needs a single test file\n"); - return 2; - } - /* -w forwards: the coordinator keys one persistent driver - * workdir for the complete selected test request. */ - return exec_package_tests(argc, argv, src, NULL, NULL, 0); + /* -w forwards one caller-owned semantic-action store shared by + * the complete selected package universe. */ + return exec_package_command(argc, argv, src, NULL, NULL, 0, 0); } struct stat st; if (stat(target, &st) != 0) { @@ -5082,7 +5383,7 @@ do_test(int argc, char **argv) return 1; } if (is_dir) { - if (emit_asm) { + if (emit_asm && nproducts == 0) { fprintf(stderr, "ww test: -S needs a single test file\n"); return 2; @@ -5092,11 +5393,6 @@ do_test(int argc, char **argv) "ww test: -o needs -c for a package target\n"); return 2; } - if (pattern) { - fprintf(stderr, - "ww test: pattern needs a single test file\n"); - return 2; - } if (nproducts != 0) { if (!compileonly) { fprintf(stderr, @@ -5104,13 +5400,16 @@ do_test(int argc, char **argv) return 2; } int r = build_package_tests(resolved, request_identity, - incs, workdir, products, nproducts); + incs, workdir, products, nproducts, + package_build ? 0 : 1, package_publish, + package_build ? &package_linkflags : NULL, emit_asm, + package_create_workdir, package_create_output_dir); free(products); free(incs); return r; } - return exec_package_tests(argc, argv, src, resolved, - request_identity != NULL ? request_identity : target, 0); + return exec_package_command(argc, argv, src, resolved, + request_identity != NULL ? request_identity : target, 0, 0); } if (packageopts) { fprintf(stderr, @@ -5253,7 +5552,7 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: %s is neither file nor directory\n", target); return 1; } - if (emit_asm) { + if (emit_asm && nproducts == 0) { fprintf(stderr, "ww test: -S needs a single test file\n"); return 2; } @@ -5261,12 +5560,6 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: -o needs -c for a package target\n"); return 2; } - /* The second bare positional remains the legacy single-file filter form; - * package filtering uses explicit -run/-filter options. */ - if (pattern) { - fprintf(stderr, "ww test: pattern needs a single test file\n"); - return 2; - } if (nproducts != 0) { if (!compileonly) { fprintf(stderr, @@ -5274,13 +5567,15 @@ do_test(int argc, char **argv) return 2; } int r = build_package_tests(target, request_identity, incs, workdir, - products, nproducts); + products, nproducts, package_build ? 0 : 1, + package_publish, package_build ? &package_linkflags : NULL, + emit_asm, package_create_workdir, package_create_output_dir); free(products); free(incs); return r; } - return exec_package_tests(argc, argv, src, NULL, request_identity, - src == NULL); + return exec_package_command(argc, argv, src, NULL, request_identity, + src == NULL, 0); } int diff --git a/internal/wwpackage/isprint.ww b/internal/wwpackage/isprint.ww new file mode 100644 index 00000000..4b2804f0 --- /dev/null +++ b/internal/wwpackage/isprint.ww @@ -0,0 +1,227 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license. +// +// Go 1.26.5 strconv.IsPrint tables, generated in the pinned Go tree at +// c19862e5f8415b4f24b189d065ed739517c548ba. They keep unmatched-pattern +// diagnostics byte-for-byte aligned with strconv.Quote without importing a +// Unicode database or adding runtime lookup state. + +package wwpackage; + +let pkgprint16: [424]u16 = [ + 0x0020u16, 0x007eu16, 0x00a1u16, 0x0377u16, 0x037au16, 0x037fu16, 0x0384u16, 0x0556u16, + 0x0559u16, 0x058au16, 0x058du16, 0x05c7u16, 0x05d0u16, 0x05eau16, 0x05efu16, 0x05f4u16, + 0x0606u16, 0x070du16, 0x0710u16, 0x074au16, 0x074du16, 0x07b1u16, 0x07c0u16, 0x07fau16, + 0x07fdu16, 0x082du16, 0x0830u16, 0x085bu16, 0x085eu16, 0x086au16, 0x0870u16, 0x088eu16, + 0x0898u16, 0x098cu16, 0x098fu16, 0x0990u16, 0x0993u16, 0x09b2u16, 0x09b6u16, 0x09b9u16, + 0x09bcu16, 0x09c4u16, 0x09c7u16, 0x09c8u16, 0x09cbu16, 0x09ceu16, 0x09d7u16, 0x09d7u16, + 0x09dcu16, 0x09e3u16, 0x09e6u16, 0x09feu16, 0x0a01u16, 0x0a0au16, 0x0a0fu16, 0x0a10u16, + 0x0a13u16, 0x0a39u16, 0x0a3cu16, 0x0a42u16, 0x0a47u16, 0x0a48u16, 0x0a4bu16, 0x0a4du16, + 0x0a51u16, 0x0a51u16, 0x0a59u16, 0x0a5eu16, 0x0a66u16, 0x0a76u16, 0x0a81u16, 0x0ab9u16, + 0x0abcu16, 0x0acdu16, 0x0ad0u16, 0x0ad0u16, 0x0ae0u16, 0x0ae3u16, 0x0ae6u16, 0x0af1u16, + 0x0af9u16, 0x0b0cu16, 0x0b0fu16, 0x0b10u16, 0x0b13u16, 0x0b39u16, 0x0b3cu16, 0x0b44u16, + 0x0b47u16, 0x0b48u16, 0x0b4bu16, 0x0b4du16, 0x0b55u16, 0x0b57u16, 0x0b5cu16, 0x0b63u16, + 0x0b66u16, 0x0b77u16, 0x0b82u16, 0x0b8au16, 0x0b8eu16, 0x0b95u16, 0x0b99u16, 0x0b9fu16, + 0x0ba3u16, 0x0ba4u16, 0x0ba8u16, 0x0baau16, 0x0baeu16, 0x0bb9u16, 0x0bbeu16, 0x0bc2u16, + 0x0bc6u16, 0x0bcdu16, 0x0bd0u16, 0x0bd0u16, 0x0bd7u16, 0x0bd7u16, 0x0be6u16, 0x0bfau16, + 0x0c00u16, 0x0c39u16, 0x0c3cu16, 0x0c4du16, 0x0c55u16, 0x0c5au16, 0x0c5du16, 0x0c5du16, + 0x0c60u16, 0x0c63u16, 0x0c66u16, 0x0c6fu16, 0x0c77u16, 0x0cb9u16, 0x0cbcu16, 0x0ccdu16, + 0x0cd5u16, 0x0cd6u16, 0x0cddu16, 0x0ce3u16, 0x0ce6u16, 0x0cf3u16, 0x0d00u16, 0x0d4fu16, + 0x0d54u16, 0x0d63u16, 0x0d66u16, 0x0d96u16, 0x0d9au16, 0x0dbdu16, 0x0dc0u16, 0x0dc6u16, + 0x0dcau16, 0x0dcau16, 0x0dcfu16, 0x0ddfu16, 0x0de6u16, 0x0defu16, 0x0df2u16, 0x0df4u16, + 0x0e01u16, 0x0e3au16, 0x0e3fu16, 0x0e5bu16, 0x0e81u16, 0x0ebdu16, 0x0ec0u16, 0x0ed9u16, + 0x0edcu16, 0x0edfu16, 0x0f00u16, 0x0f6cu16, 0x0f71u16, 0x0fdau16, 0x1000u16, 0x10c7u16, + 0x10cdu16, 0x10cdu16, 0x10d0u16, 0x124du16, 0x1250u16, 0x125du16, 0x1260u16, 0x128du16, + 0x1290u16, 0x12b5u16, 0x12b8u16, 0x12c5u16, 0x12c8u16, 0x1315u16, 0x1318u16, 0x135au16, + 0x135du16, 0x137cu16, 0x1380u16, 0x1399u16, 0x13a0u16, 0x13f5u16, 0x13f8u16, 0x13fdu16, + 0x1400u16, 0x169cu16, 0x16a0u16, 0x16f8u16, 0x1700u16, 0x1715u16, 0x171fu16, 0x1736u16, + 0x1740u16, 0x1753u16, 0x1760u16, 0x1773u16, 0x1780u16, 0x17ddu16, 0x17e0u16, 0x17e9u16, + 0x17f0u16, 0x17f9u16, 0x1800u16, 0x1819u16, 0x1820u16, 0x1878u16, 0x1880u16, 0x18aau16, + 0x18b0u16, 0x18f5u16, 0x1900u16, 0x192bu16, 0x1930u16, 0x193bu16, 0x1940u16, 0x1940u16, + 0x1944u16, 0x196du16, 0x1970u16, 0x1974u16, 0x1980u16, 0x19abu16, 0x19b0u16, 0x19c9u16, + 0x19d0u16, 0x19dau16, 0x19deu16, 0x1a1bu16, 0x1a1eu16, 0x1a7cu16, 0x1a7fu16, 0x1a89u16, + 0x1a90u16, 0x1a99u16, 0x1aa0u16, 0x1aadu16, 0x1ab0u16, 0x1aceu16, 0x1b00u16, 0x1b4cu16, + 0x1b50u16, 0x1bf3u16, 0x1bfcu16, 0x1c37u16, 0x1c3bu16, 0x1c49u16, 0x1c4du16, 0x1c88u16, + 0x1c90u16, 0x1cbau16, 0x1cbdu16, 0x1cc7u16, 0x1cd0u16, 0x1cfau16, 0x1d00u16, 0x1f15u16, + 0x1f18u16, 0x1f1du16, 0x1f20u16, 0x1f45u16, 0x1f48u16, 0x1f4du16, 0x1f50u16, 0x1f7du16, + 0x1f80u16, 0x1fd3u16, 0x1fd6u16, 0x1fefu16, 0x1ff2u16, 0x1ffeu16, 0x2010u16, 0x2027u16, + 0x2030u16, 0x205eu16, 0x2070u16, 0x2071u16, 0x2074u16, 0x209cu16, 0x20a0u16, 0x20c0u16, + 0x20d0u16, 0x20f0u16, 0x2100u16, 0x218bu16, 0x2190u16, 0x2426u16, 0x2440u16, 0x244au16, + 0x2460u16, 0x2b73u16, 0x2b76u16, 0x2cf3u16, 0x2cf9u16, 0x2d27u16, 0x2d2du16, 0x2d2du16, + 0x2d30u16, 0x2d67u16, 0x2d6fu16, 0x2d70u16, 0x2d7fu16, 0x2d96u16, 0x2da0u16, 0x2e5du16, + 0x2e80u16, 0x2ef3u16, 0x2f00u16, 0x2fd5u16, 0x2ff0u16, 0x2ffbu16, 0x3001u16, 0x3096u16, + 0x3099u16, 0x30ffu16, 0x3105u16, 0x31e3u16, 0x31f0u16, 0xa48cu16, 0xa490u16, 0xa4c6u16, + 0xa4d0u16, 0xa62bu16, 0xa640u16, 0xa6f7u16, 0xa700u16, 0xa7cau16, 0xa7d0u16, 0xa7d9u16, + 0xa7f2u16, 0xa82cu16, 0xa830u16, 0xa839u16, 0xa840u16, 0xa877u16, 0xa880u16, 0xa8c5u16, + 0xa8ceu16, 0xa8d9u16, 0xa8e0u16, 0xa953u16, 0xa95fu16, 0xa97cu16, 0xa980u16, 0xa9d9u16, + 0xa9deu16, 0xaa36u16, 0xaa40u16, 0xaa4du16, 0xaa50u16, 0xaa59u16, 0xaa5cu16, 0xaac2u16, + 0xaadbu16, 0xaaf6u16, 0xab01u16, 0xab06u16, 0xab09u16, 0xab0eu16, 0xab11u16, 0xab16u16, + 0xab20u16, 0xab6bu16, 0xab70u16, 0xabedu16, 0xabf0u16, 0xabf9u16, 0xac00u16, 0xd7a3u16, + 0xd7b0u16, 0xd7c6u16, 0xd7cbu16, 0xd7fbu16, 0xf900u16, 0xfa6du16, 0xfa70u16, 0xfad9u16, + 0xfb00u16, 0xfb06u16, 0xfb13u16, 0xfb17u16, 0xfb1du16, 0xfbc2u16, 0xfbd3u16, 0xfd8fu16, + 0xfd92u16, 0xfdc7u16, 0xfdcfu16, 0xfdcfu16, 0xfdf0u16, 0xfe19u16, 0xfe20u16, 0xfe6bu16, + 0xfe70u16, 0xfefcu16, 0xff01u16, 0xffbeu16, 0xffc2u16, 0xffc7u16, 0xffcau16, 0xffcfu16, + 0xffd2u16, 0xffd7u16, 0xffdau16, 0xffdcu16, 0xffe0u16, 0xffeeu16, 0xfffcu16, 0xfffdu16, +]; + +let pkgnotprint16: [133]u16 = [ + 0x00adu16, 0x038bu16, 0x038du16, 0x03a2u16, 0x0530u16, 0x0590u16, 0x061cu16, 0x06ddu16, + 0x083fu16, 0x085fu16, 0x08e2u16, 0x0984u16, 0x09a9u16, 0x09b1u16, 0x09deu16, 0x0a04u16, + 0x0a29u16, 0x0a31u16, 0x0a34u16, 0x0a37u16, 0x0a3du16, 0x0a5du16, 0x0a84u16, 0x0a8eu16, + 0x0a92u16, 0x0aa9u16, 0x0ab1u16, 0x0ab4u16, 0x0ac6u16, 0x0acau16, 0x0b00u16, 0x0b04u16, + 0x0b29u16, 0x0b31u16, 0x0b34u16, 0x0b5eu16, 0x0b84u16, 0x0b91u16, 0x0b9bu16, 0x0b9du16, + 0x0bc9u16, 0x0c0du16, 0x0c11u16, 0x0c29u16, 0x0c45u16, 0x0c49u16, 0x0c57u16, 0x0c8du16, + 0x0c91u16, 0x0ca9u16, 0x0cb4u16, 0x0cc5u16, 0x0cc9u16, 0x0cdfu16, 0x0cf0u16, 0x0d0du16, + 0x0d11u16, 0x0d45u16, 0x0d49u16, 0x0d80u16, 0x0d84u16, 0x0db2u16, 0x0dbcu16, 0x0dd5u16, + 0x0dd7u16, 0x0e83u16, 0x0e85u16, 0x0e8bu16, 0x0ea4u16, 0x0ea6u16, 0x0ec5u16, 0x0ec7u16, + 0x0ecfu16, 0x0f48u16, 0x0f98u16, 0x0fbdu16, 0x0fcdu16, 0x10c6u16, 0x1249u16, 0x1257u16, + 0x1259u16, 0x1289u16, 0x12b1u16, 0x12bfu16, 0x12c1u16, 0x12d7u16, 0x1311u16, 0x1680u16, + 0x176du16, 0x1771u16, 0x180eu16, 0x191fu16, 0x1a5fu16, 0x1b7fu16, 0x1f58u16, 0x1f5au16, + 0x1f5cu16, 0x1f5eu16, 0x1fb5u16, 0x1fc5u16, 0x1fdcu16, 0x1ff5u16, 0x208fu16, 0x2b96u16, + 0x2d26u16, 0x2da7u16, 0x2dafu16, 0x2db7u16, 0x2dbfu16, 0x2dc7u16, 0x2dcfu16, 0x2dd7u16, + 0x2ddfu16, 0x2e9au16, 0x3040u16, 0x3130u16, 0x318fu16, 0x321fu16, 0xa7d2u16, 0xa7d4u16, + 0xa9ceu16, 0xa9ffu16, 0xab27u16, 0xab2fu16, 0xfb37u16, 0xfb3du16, 0xfb3fu16, 0xfb42u16, + 0xfb45u16, 0xfe53u16, 0xfe67u16, 0xfe75u16, 0xffe7u16, +]; + +let pkgprint32: [508]u32 = [ + 0x010000u32, 0x01004du32, 0x010050u32, 0x01005du32, 0x010080u32, 0x0100fau32, 0x010100u32, 0x010102u32, + 0x010107u32, 0x010133u32, 0x010137u32, 0x01019cu32, 0x0101a0u32, 0x0101a0u32, 0x0101d0u32, 0x0101fdu32, + 0x010280u32, 0x01029cu32, 0x0102a0u32, 0x0102d0u32, 0x0102e0u32, 0x0102fbu32, 0x010300u32, 0x010323u32, + 0x01032du32, 0x01034au32, 0x010350u32, 0x01037au32, 0x010380u32, 0x0103c3u32, 0x0103c8u32, 0x0103d5u32, + 0x010400u32, 0x01049du32, 0x0104a0u32, 0x0104a9u32, 0x0104b0u32, 0x0104d3u32, 0x0104d8u32, 0x0104fbu32, + 0x010500u32, 0x010527u32, 0x010530u32, 0x010563u32, 0x01056fu32, 0x0105bcu32, 0x010600u32, 0x010736u32, + 0x010740u32, 0x010755u32, 0x010760u32, 0x010767u32, 0x010780u32, 0x0107bau32, 0x010800u32, 0x010805u32, + 0x010808u32, 0x010838u32, 0x01083cu32, 0x01083cu32, 0x01083fu32, 0x01089eu32, 0x0108a7u32, 0x0108afu32, + 0x0108e0u32, 0x0108f5u32, 0x0108fbu32, 0x01091bu32, 0x01091fu32, 0x010939u32, 0x01093fu32, 0x01093fu32, + 0x010980u32, 0x0109b7u32, 0x0109bcu32, 0x0109cfu32, 0x0109d2u32, 0x010a06u32, 0x010a0cu32, 0x010a35u32, + 0x010a38u32, 0x010a3au32, 0x010a3fu32, 0x010a48u32, 0x010a50u32, 0x010a58u32, 0x010a60u32, 0x010a9fu32, + 0x010ac0u32, 0x010ae6u32, 0x010aebu32, 0x010af6u32, 0x010b00u32, 0x010b35u32, 0x010b39u32, 0x010b55u32, + 0x010b58u32, 0x010b72u32, 0x010b78u32, 0x010b91u32, 0x010b99u32, 0x010b9cu32, 0x010ba9u32, 0x010bafu32, + 0x010c00u32, 0x010c48u32, 0x010c80u32, 0x010cb2u32, 0x010cc0u32, 0x010cf2u32, 0x010cfau32, 0x010d27u32, + 0x010d30u32, 0x010d39u32, 0x010e60u32, 0x010eadu32, 0x010eb0u32, 0x010eb1u32, 0x010efdu32, 0x010f27u32, + 0x010f30u32, 0x010f59u32, 0x010f70u32, 0x010f89u32, 0x010fb0u32, 0x010fcbu32, 0x010fe0u32, 0x010ff6u32, + 0x011000u32, 0x01104du32, 0x011052u32, 0x011075u32, 0x01107fu32, 0x0110c2u32, 0x0110d0u32, 0x0110e8u32, + 0x0110f0u32, 0x0110f9u32, 0x011100u32, 0x011147u32, 0x011150u32, 0x011176u32, 0x011180u32, 0x0111f4u32, + 0x011200u32, 0x011241u32, 0x011280u32, 0x0112a9u32, 0x0112b0u32, 0x0112eau32, 0x0112f0u32, 0x0112f9u32, + 0x011300u32, 0x01130cu32, 0x01130fu32, 0x011310u32, 0x011313u32, 0x011344u32, 0x011347u32, 0x011348u32, + 0x01134bu32, 0x01134du32, 0x011350u32, 0x011350u32, 0x011357u32, 0x011357u32, 0x01135du32, 0x011363u32, + 0x011366u32, 0x01136cu32, 0x011370u32, 0x011374u32, 0x011400u32, 0x011461u32, 0x011480u32, 0x0114c7u32, + 0x0114d0u32, 0x0114d9u32, 0x011580u32, 0x0115b5u32, 0x0115b8u32, 0x0115ddu32, 0x011600u32, 0x011644u32, + 0x011650u32, 0x011659u32, 0x011660u32, 0x01166cu32, 0x011680u32, 0x0116b9u32, 0x0116c0u32, 0x0116c9u32, + 0x011700u32, 0x01171au32, 0x01171du32, 0x01172bu32, 0x011730u32, 0x011746u32, 0x011800u32, 0x01183bu32, + 0x0118a0u32, 0x0118f2u32, 0x0118ffu32, 0x011906u32, 0x011909u32, 0x011909u32, 0x01190cu32, 0x011938u32, + 0x01193bu32, 0x011946u32, 0x011950u32, 0x011959u32, 0x0119a0u32, 0x0119a7u32, 0x0119aau32, 0x0119d7u32, + 0x0119dau32, 0x0119e4u32, 0x011a00u32, 0x011a47u32, 0x011a50u32, 0x011aa2u32, 0x011ab0u32, 0x011af8u32, + 0x011b00u32, 0x011b09u32, 0x011c00u32, 0x011c45u32, 0x011c50u32, 0x011c6cu32, 0x011c70u32, 0x011c8fu32, + 0x011c92u32, 0x011cb6u32, 0x011d00u32, 0x011d36u32, 0x011d3au32, 0x011d47u32, 0x011d50u32, 0x011d59u32, + 0x011d60u32, 0x011d98u32, 0x011da0u32, 0x011da9u32, 0x011ee0u32, 0x011ef8u32, 0x011f00u32, 0x011f3au32, + 0x011f3eu32, 0x011f59u32, 0x011fb0u32, 0x011fb0u32, 0x011fc0u32, 0x011ff1u32, 0x011fffu32, 0x012399u32, + 0x012400u32, 0x012474u32, 0x012480u32, 0x012543u32, 0x012f90u32, 0x012ff2u32, 0x013000u32, 0x01342fu32, + 0x013440u32, 0x013455u32, 0x014400u32, 0x014646u32, 0x016800u32, 0x016a38u32, 0x016a40u32, 0x016a69u32, + 0x016a6eu32, 0x016ac9u32, 0x016ad0u32, 0x016aedu32, 0x016af0u32, 0x016af5u32, 0x016b00u32, 0x016b45u32, + 0x016b50u32, 0x016b77u32, 0x016b7du32, 0x016b8fu32, 0x016e40u32, 0x016e9au32, 0x016f00u32, 0x016f4au32, + 0x016f4fu32, 0x016f87u32, 0x016f8fu32, 0x016f9fu32, 0x016fe0u32, 0x016fe4u32, 0x016ff0u32, 0x016ff1u32, + 0x017000u32, 0x0187f7u32, 0x018800u32, 0x018cd5u32, 0x018d00u32, 0x018d08u32, 0x01aff0u32, 0x01b122u32, + 0x01b132u32, 0x01b132u32, 0x01b150u32, 0x01b152u32, 0x01b155u32, 0x01b155u32, 0x01b164u32, 0x01b167u32, + 0x01b170u32, 0x01b2fbu32, 0x01bc00u32, 0x01bc6au32, 0x01bc70u32, 0x01bc7cu32, 0x01bc80u32, 0x01bc88u32, + 0x01bc90u32, 0x01bc99u32, 0x01bc9cu32, 0x01bc9fu32, 0x01cf00u32, 0x01cf2du32, 0x01cf30u32, 0x01cf46u32, + 0x01cf50u32, 0x01cfc3u32, 0x01d000u32, 0x01d0f5u32, 0x01d100u32, 0x01d126u32, 0x01d129u32, 0x01d172u32, + 0x01d17bu32, 0x01d1eau32, 0x01d200u32, 0x01d245u32, 0x01d2c0u32, 0x01d2d3u32, 0x01d2e0u32, 0x01d2f3u32, + 0x01d300u32, 0x01d356u32, 0x01d360u32, 0x01d378u32, 0x01d400u32, 0x01d49fu32, 0x01d4a2u32, 0x01d4a2u32, + 0x01d4a5u32, 0x01d4a6u32, 0x01d4a9u32, 0x01d50au32, 0x01d50du32, 0x01d546u32, 0x01d54au32, 0x01d6a5u32, + 0x01d6a8u32, 0x01d7cbu32, 0x01d7ceu32, 0x01da8bu32, 0x01da9bu32, 0x01daafu32, 0x01df00u32, 0x01df1eu32, + 0x01df25u32, 0x01df2au32, 0x01e000u32, 0x01e018u32, 0x01e01bu32, 0x01e02au32, 0x01e030u32, 0x01e06du32, + 0x01e08fu32, 0x01e08fu32, 0x01e100u32, 0x01e12cu32, 0x01e130u32, 0x01e13du32, 0x01e140u32, 0x01e149u32, + 0x01e14eu32, 0x01e14fu32, 0x01e290u32, 0x01e2aeu32, 0x01e2c0u32, 0x01e2f9u32, 0x01e2ffu32, 0x01e2ffu32, + 0x01e4d0u32, 0x01e4f9u32, 0x01e7e0u32, 0x01e8c4u32, 0x01e8c7u32, 0x01e8d6u32, 0x01e900u32, 0x01e94bu32, + 0x01e950u32, 0x01e959u32, 0x01e95eu32, 0x01e95fu32, 0x01ec71u32, 0x01ecb4u32, 0x01ed01u32, 0x01ed3du32, + 0x01ee00u32, 0x01ee24u32, 0x01ee27u32, 0x01ee3bu32, 0x01ee42u32, 0x01ee42u32, 0x01ee47u32, 0x01ee54u32, + 0x01ee57u32, 0x01ee64u32, 0x01ee67u32, 0x01ee9bu32, 0x01eea1u32, 0x01eebbu32, 0x01eef0u32, 0x01eef1u32, + 0x01f000u32, 0x01f02bu32, 0x01f030u32, 0x01f093u32, 0x01f0a0u32, 0x01f0aeu32, 0x01f0b1u32, 0x01f0f5u32, + 0x01f100u32, 0x01f1adu32, 0x01f1e6u32, 0x01f202u32, 0x01f210u32, 0x01f23bu32, 0x01f240u32, 0x01f248u32, + 0x01f250u32, 0x01f251u32, 0x01f260u32, 0x01f265u32, 0x01f300u32, 0x01f6d7u32, 0x01f6dcu32, 0x01f6ecu32, + 0x01f6f0u32, 0x01f6fcu32, 0x01f700u32, 0x01f776u32, 0x01f77bu32, 0x01f7d9u32, 0x01f7e0u32, 0x01f7ebu32, + 0x01f7f0u32, 0x01f7f0u32, 0x01f800u32, 0x01f80bu32, 0x01f810u32, 0x01f847u32, 0x01f850u32, 0x01f859u32, + 0x01f860u32, 0x01f887u32, 0x01f890u32, 0x01f8adu32, 0x01f8b0u32, 0x01f8b1u32, 0x01f900u32, 0x01fa53u32, + 0x01fa60u32, 0x01fa6du32, 0x01fa70u32, 0x01fa7cu32, 0x01fa80u32, 0x01fa88u32, 0x01fa90u32, 0x01fac5u32, + 0x01faceu32, 0x01fadbu32, 0x01fae0u32, 0x01fae8u32, 0x01faf0u32, 0x01faf8u32, 0x01fb00u32, 0x01fbcau32, + 0x01fbf0u32, 0x01fbf9u32, 0x020000u32, 0x02a6dfu32, 0x02a700u32, 0x02b739u32, 0x02b740u32, 0x02b81du32, + 0x02b820u32, 0x02cea1u32, 0x02ceb0u32, 0x02ebe0u32, 0x02f800u32, 0x02fa1du32, 0x030000u32, 0x03134au32, + 0x031350u32, 0x0323afu32, 0x0e0100u32, 0x0e01efu32, +]; + +let pkgnotprint32: [112]u16 = [ + 0x000cu16, 0x0027u16, 0x003bu16, 0x003eu16, 0x018fu16, 0x039eu16, 0x057bu16, + 0x058bu16, 0x0593u16, 0x0596u16, 0x05a2u16, 0x05b2u16, 0x05bau16, 0x0786u16, 0x07b1u16, + 0x0809u16, 0x0836u16, 0x0856u16, 0x08f3u16, 0x0a04u16, 0x0a14u16, 0x0a18u16, 0x0e7fu16, + 0x0eaau16, 0x10bdu16, 0x1135u16, 0x11e0u16, 0x1212u16, 0x1287u16, 0x1289u16, 0x128eu16, + 0x129eu16, 0x1304u16, 0x1329u16, 0x1331u16, 0x1334u16, 0x133au16, 0x145cu16, 0x1914u16, + 0x1917u16, 0x1936u16, 0x1c09u16, 0x1c37u16, 0x1ca8u16, 0x1d07u16, 0x1d0au16, 0x1d3bu16, + 0x1d3eu16, 0x1d66u16, 0x1d69u16, 0x1d8fu16, 0x1d92u16, 0x1f11u16, 0x246fu16, 0x6a5fu16, + 0x6abfu16, 0x6b5au16, 0x6b62u16, 0xaff4u16, 0xaffcu16, 0xafffu16, 0xd455u16, 0xd49du16, + 0xd4adu16, 0xd4bau16, 0xd4bcu16, 0xd4c4u16, 0xd506u16, 0xd515u16, 0xd51du16, 0xd53au16, + 0xd53fu16, 0xd545u16, 0xd551u16, 0xdaa0u16, 0xe007u16, 0xe022u16, 0xe025u16, 0xe7e7u16, + 0xe7ecu16, 0xe7efu16, 0xe7ffu16, 0xee04u16, 0xee20u16, 0xee23u16, 0xee28u16, 0xee33u16, + 0xee38u16, 0xee3au16, 0xee48u16, 0xee4au16, 0xee4cu16, 0xee50u16, 0xee53u16, 0xee58u16, + 0xee5au16, 0xee5cu16, 0xee5eu16, 0xee60u16, 0xee63u16, 0xee6bu16, 0xee73u16, 0xee78u16, + 0xee7du16, 0xee7fu16, 0xee8au16, 0xeea4u16, 0xeeaau16, 0xf0c0u16, 0xf0d0u16, 0xfabeu16, + 0xfb93u16, +]; + +fn pkgbsearch16(values: []u16, target: u16) i32 = { + let low: i32 = 0; + let high: i32 = values.len; + for (low < high) { + let middle: i32 = low + (high - low) / 2; + if (values[middle] < target) { low = middle + 1; } + else { high = middle; }; + }; + return low; +}; + +fn pkgbsearch32(values: []u32, target: u32) i32 = { + let low: i32 = 0; + let high: i32 = values.len; + for (low < high) { + let middle: i32 = low + (high - low) / 2; + if (values[middle] < target) { low = middle + 1; } + else { high = middle; }; + }; + return low; +}; + +fn pkgisprint(r: u32) bool = { + if (r <= 0xffu32) { + if (r >= 0x20u32 && r <= 0x7eu32) { return true; }; + if (r >= 0xa1u32 && r <= 0xffu32) { return r != 0xadu32; }; + return false; + }; + if (r < 0x10000u32) { + let rr: u16 = r: u16; + let i: i32 = pkgbsearch16(pkgprint16, rr); + if (i >= pkgprint16.len) { return false; }; + let start: i32 = i; + if (start % 2 != 0) { start -= 1; }; + if (start + 1 >= pkgprint16.len + || rr < pkgprint16[start] || pkgprint16[start + 1] < rr) { + return false; + }; + let excluded: i32 = pkgbsearch16(pkgnotprint16, rr); + return excluded >= pkgnotprint16.len + || pkgnotprint16[excluded] != rr; + }; + let i: i32 = pkgbsearch32(pkgprint32, r); + if (i >= pkgprint32.len) { return false; }; + let start: i32 = i; + if (start % 2 != 0) { start -= 1; }; + if (start + 1 >= pkgprint32.len + || r < pkgprint32[start] || pkgprint32[start + 1] < r) { + return false; + }; + if (r >= 0x20000u32) { return true; }; + let rr: u16 = (r - 0x10000u32): u16; + let excluded: i32 = pkgbsearch16(pkgnotprint32, rr); + return excluded >= pkgnotprint32.len + || pkgnotprint32[excluded] != rr; +}; diff --git a/internal/wwpackage/package.ww b/internal/wwpackage/package.ww index b76fc154..e5e50173 100644 --- a/internal/wwpackage/package.ww +++ b/internal/wwpackage/package.ww @@ -1,7 +1,5 @@ package wwpackage; -import crypto.sha256; -import hash; import os; import os.exec; import strconv; @@ -41,22 +39,61 @@ type pkggroup = struct { type pkgplan = struct { dir: str, - sourceroot: str, identity: str, root: str, workdir: str, - workdircreated: bool, + outputdir: str, buildout: str, builderr: str, start: i32, end: i32, state: i32, buildres: exec.result, + buildonly: bool, + publish: bool, + emitasm: bool, }; def PKG_COUNT_MAX: i32 = 2147483647; def PKG_INITIAL_CAP: i32 = 8; +// strings.concat/dup abort on allocation failure. Request selection and plan +// construction instead keep every new allocation fallible and diagnostic. +fn pkgstring(out: *str, values: str...) bool = { + let total: i64 = 0i64; + let i: i32 = 0; + for (i < values.len) { + total += values[i].len: i64; + if (total > PKG_COUNT_MAX: i64) { + pkgputln(os.STDERR_FILENO, + "wwtest package: package graph is too large"); + return false; + }; + i += 1; + }; + if (total == 0i64) { *out = ""; return true; }; + let allocation: ([]u8 | nomem) = pkgallocbytes(total: i32); + let bytes: []u8; + match (allocation) { + case let value: []u8 => bytes = value; + case nomem => { + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return false; + }; + }; + i = 0; + for (i < values.len) { + let j: i32 = 0; + for (j < values[i].len) { + append(bytes, values[i][j]); + j += 1; + }; + i += 1; + }; + *out = strings.frombytes(bytes); + return true; +}; + fn pkgallocstrs(cap: i32) ([]str | nomem) = { let value: []str = alloc([], cap: u64)?; return value; @@ -194,7 +231,9 @@ fn toolenv(tmpdir: str, out: *[]str) bool = { i += 1; }; append(env, "LC_ALL=C"); - append(env, strings.concat("TMPDIR=", tmpdir)); + let tmpenv: str; + if (!pkgstring(&tmpenv, "TMPDIR=", tmpdir)) { return false; }; + append(env, tmpenv); *out = env; return true; }; @@ -216,6 +255,134 @@ fn pkgputln(fd: i32, s: str) void = { pkgput(fd, "\n"); }; +fn pkgputhexbyte(fd: i32, value: u8) void = { + let digits: str = "0123456789abcdef"; + let encoded: [2]u8; + let high: i32 = (value / 16u8): i32; + let low: i32 = (value % 16u8): i32; + encoded[0] = digits[high]; + encoded[1] = digits[low]; + os.write(fd, &encoded[0], 2u64); +}; + +fn pkgputhexrune(fd: i32, value: u32, digits: i32) void = { + let alphabet: str = "0123456789abcdef"; + let encoded: [8]u8; + let i: i32 = digits - 1; + for (i >= 0) { + let shift: u32 = (i: u32) * 4u32; + let d: i32 = ((value >> shift) & 15u32): i32; + encoded[digits - 1 - i] = alphabet[d]; + i -= 1; + }; + os.write(fd, &encoded[0], digits: u64); +}; + +fn pkgutf8width(value: str, index: i32) i32 = { + let c: u8 = value[index]; + if (c < 128u8) { return 1; }; + if (c >= 194u8 && c <= 223u8 && index + 1 < value.len + && value[index + 1] >= 128u8 && value[index + 1] <= 191u8) { + return 2; + }; + if (index + 2 < value.len + && value[index + 2] >= 128u8 && value[index + 2] <= 191u8) { + let c1: u8 = value[index + 1]; + if ((c == 224u8 && c1 >= 160u8 && c1 <= 191u8) + || (c >= 225u8 && c <= 236u8 && c1 >= 128u8 && c1 <= 191u8) + || (c == 237u8 && c1 >= 128u8 && c1 <= 159u8) + || (c >= 238u8 && c <= 239u8 && c1 >= 128u8 && c1 <= 191u8)) { + return 3; + }; + }; + if (index + 3 < value.len + && value[index + 2] >= 128u8 && value[index + 2] <= 191u8 + && value[index + 3] >= 128u8 && value[index + 3] <= 191u8) { + let c1: u8 = value[index + 1]; + if ((c == 240u8 && c1 >= 144u8 && c1 <= 191u8) + || (c >= 241u8 && c <= 243u8 && c1 >= 128u8 && c1 <= 191u8) + || (c == 244u8 && c1 >= 128u8 && c1 <= 143u8)) { + return 4; + }; + }; + return 1; +}; + +// Decode one filesystem-name rune with the same malformed-byte behavior as +// utf8.DecodeRuneInString: each malformed byte is one U+FFFD rune. +fn pkgutf8rune(value: str, index: i32) u32 = { + let width: i32 = pkgutf8width(value, index); + let c0: u32 = value[index]: u32; + if (width == 1) { + if (c0 >= 128u32) { return 0xfffdu32; }; + return c0; + }; + let c1: u32 = value[index + 1]: u32; + if (width == 2) { + return ((c0 & 31u32) << 6u32) | (c1 & 63u32); + }; + let c2: u32 = value[index + 2]: u32; + if (width == 3) { + return ((c0 & 15u32) << 12u32) + | ((c1 & 63u32) << 6u32) | (c2 & 63u32); + }; + let c3: u32 = value[index + 3]: u32; + return ((c0 & 7u32) << 18u32) | ((c1 & 63u32) << 12u32) + | ((c2 & 63u32) << 6u32) | (c3 & 63u32); +}; + +fn pkgvalidutf8(value: str) bool = { + let i: i32 = 0; + for (i < value.len) { + let width: i32 = pkgutf8width(value, i); + if (value[i] >= 128u8 && width == 1) { return false; }; + i += width; + }; + return true; +}; + +// Go's unmatched-pattern diagnostic uses strconv.Quote. Filesystem arguments +// are emitted byte-exactly for Go-printable UTF-8, with Go escapes for +// controls, non-printing runes, and malformed bytes. +fn pkgputquoted(fd: i32, value: str) void = { + pkgput(fd, "\""); + let i: i32 = 0; + for (i < value.len) { + let c: u8 = value[i]; + let width: i32 = pkgutf8width(value, i); + if (c >= 128u8 && width == 1) { + pkgput(fd, "\\x"); pkgputhexbyte(fd, c); i += 1; continue; + }; + if (width > 1) { + let r: u32 = pkgutf8rune(value, i); + if (pkgisprint(r)) { + os.write(fd, value.ptr + (i: u64), width: u64); + } else if (r < 0x10000u32) { + pkgput(fd, "\\u"); pkgputhexrune(fd, r, 4); + } else { + pkgput(fd, "\\U"); pkgputhexrune(fd, r, 8); + }; + i += width; + continue; + }; + if (c == '"') { pkgput(fd, "\\\""); } + else { if (c == '\\') { pkgput(fd, "\\\\"); } + else { if (c == 7u8) { pkgput(fd, "\\a"); } + else { if (c == 8u8) { pkgput(fd, "\\b"); } + else { if (c == 12u8) { pkgput(fd, "\\f"); } + else { if (c == '\n') { pkgput(fd, "\\n"); } + else { if (c == '\r') { pkgput(fd, "\\r"); } + else { if (c == '\t') { pkgput(fd, "\\t"); } + else { if (c == 11u8) { pkgput(fd, "\\v"); } + else { if (c < 32u8 || c == 127u8) { + pkgput(fd, "\\x"); pkgputhexbyte(fd, c); + } else { os.write(fd, &value[i], 1u64); }; + };};};};};};};};}; + i += 1; + }; + pkgput(fd, "\""); +}; + fn pkgfailpath(path: str, reason: str) void = { pkgput(os.STDERR_FILENO, "wwtest package: "); pkgput(os.STDERR_FILENO, path); @@ -224,12 +391,14 @@ fn pkgfailpath(path: str, reason: str) void = { }; fn pkgusage() void = { - let s: str = strings.concat( - "usage: wwtest package [-c] [-list] [-j N] [-I DIR] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/...] [-- GLOB ...]\n", - " *_test.ww is the sole test-source form; @test elsewhere is rejected\n", - " -c retains the compiled package binaries; -j N runs up to N build or test processes at once\n", - " -w DIR keys one persistent shared build workdir for the test request\n"); - pkgput(os.STDERR_FILENO, s); + pkgput(os.STDERR_FILENO, + "usage: wwtest package [-c] [-S] [-list] [-j N] [-I DIR] [-L DIR] [-l LIB] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/... ...] [-- GLOB ...]\n"); + pkgput(os.STDERR_FILENO, + " *_test.ww is the sole test-source form; @test elsewhere is rejected\n"); + pkgput(os.STDERR_FILENO, + " -c retains the compiled package binaries; -j N runs up to N build or test processes at once\n"); + pkgput(os.STDERR_FILENO, + " -w DIR is one persistent semantic-action store shared by the selected packages\n"); }; fn pkgread(path: str, out: *str) bool = { @@ -388,6 +557,23 @@ fn pkgisdir(path: str) bool = { }; }; +fn pkgstatdir(path: str) bool = { + let fi: os.filestat; + match (os.stat(&fi, path)) { + case void => return pkgmodeis(fi.mode, os.mode.DIR); + case let e: os.oserror => return false; + }; +}; + +fn pkgoutputdir(path: str) bool = { + return pkgstatdir(path) || strings.hassuffix(path, "/"); +}; + +fn pkgjoinpath(dir: str, name: str, out: *str) bool = { + if (strings.hassuffix(dir, "/")) { return pkgstring(out, dir, name); }; + return pkgstring(out, dir, "/", name); +}; + fn pkgisreg(path: str) bool = { let fi: os.filestat; match (os.lstat(&fi, path)) { @@ -397,44 +583,265 @@ fn pkgisreg(path: str) bool = { }; fn pkgkeepfile(name: str) bool = { - if (!strings.hassuffix(name, ".ww")) { return false; }; + if (name.len <= 3 || name[0] == '.' || name[0] == '_') { return false; }; + return strings.hassuffix(name, ".ww"); +}; + +type pkgmatcher = struct { + full: str, + base: str, + fullcap: u64, + basecap: u64, + trailing: bool, + valid: bool, +}; + +fn pkgsortstrings(ss: []str) void = { + let i: i32 = 1; + for (i < ss.len) { + let j: i32 = i; + for (j > 0 && strings.compare(ss[j - 1], ss[j]) > 0) { + let value: str = ss[j]; + ss[j] = ss[j - 1]; + ss[j - 1] = value; + j -= 1; + }; + i += 1; + }; +}; + +fn pkgcleanpath(spelling: str, out: *str) bool = { + if (spelling.len > PKG_COUNT_MAX - 2) { + pkgputln(os.STDERR_FILENO, + "wwtest package: package graph is too large"); + return false; + }; + let componentallocation: ([]str | nomem) = + pkgallocstrs(spelling.len + 1); + let components: []str; + match (componentallocation) { + case let value: []str => components = value; + case nomem => { + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return false; + }; + }; + let absolute: bool = spelling.len != 0 && spelling[0] == '/'; + let start: i32 = 0; + for (start <= spelling.len) { + let end: i32 = start; + for (end < spelling.len && spelling[end] != '/') { end += 1; }; + let component: str = spelling[start:end]; + if (component.len == 0 || strings.compare(component, ".") == 0) { + void; + } else if (strings.compare(component, "..") == 0) { + if (components.len != 0 + && strings.compare(components[components.len - 1], "..") != 0) { + components.len -= 1; + } else if (!absolute) { + append(components, component); + }; + } else { + append(components, component); + }; + if (end == spelling.len) { break; }; + start = end + 1; + }; + let byteallocation: ([]u8 | nomem) = pkgallocbytes(spelling.len + 2); + let cleaned: []u8; + match (byteallocation) { + case let value: []u8 => cleaned = value; + case nomem => { + os.free(components.ptr: *void, + ((spelling.len + 1): u64) * (size(str): u64)); + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return false; + }; + }; + if (absolute) { append(cleaned, '/': u8); }; + let i: i32 = 0; + for (i < components.len) { + if (i != 0) { append(cleaned, '/': u8); }; + let j: i32 = 0; + for (j < components[i].len) { + append(cleaned, components[i][j]); + j += 1; + }; + i += 1; + }; + if (cleaned.len == 0) { append(cleaned, '.': u8); }; + *out = strings.frombytes(cleaned); + os.free(components.ptr: *void, + ((spelling.len + 1): u64) * (size(str): u64)); return true; }; -// One explicit package directory; with recurse (the DIR/... form) every -// subdirectory whose name does not begin with '.' or '_' is descended as -// well, Go's ./... convention. An explicitly selected root may be a symlink; -// canonical planning coalesces it with the target directory. Recursive child -// symlinks remain skipped and symlink source files remain rejected. -fn pkgdiscoverdir(path: str, st: *pkgdiscover, recurse: bool) void = { +fn pkgfirstellipsis(value: str) i32 = { + let i: i32 = 0; + for (i + 2 < value.len) { + if (value[i] == '.' && value[i + 1] == '.' + && value[i + 2] == '.') { return i; }; + i += 1; + }; + return -1; +}; + +// Non-terminal vendor elements become a byte that a wildcard cannot consume. +// Explicit vendor elements receive the same byte and therefore still match. +fn pkgvendorform(value: str, out: *str, capacity: *u64) bool = { + *capacity = value.len: u64; + if (value.len == 0) { *out = ""; return true; }; + let allocation: ([]u8 | nomem) = pkgallocbytes(value.len); + let encoded: []u8; + match (allocation) { + case let bytes: []u8 => encoded = bytes; + case nomem => return false; + }; + let start: i32 = 0; + for (start < value.len) { + let end: i32 = start; + for (end < value.len && value[end] != '/') { end += 1; }; + if (end < value.len && end - start == 6 + && strings.compare(value[start:end], "vendor") == 0) { + append(encoded, 0u8); + } else { + let i: i32 = start; + for (i < end) { append(encoded, value[i]); i += 1; }; + }; + if (end == value.len) { break; }; + append(encoded, '/': u8); + start = end + 1; + }; + *out = strings.frombytes(encoded); + return true; +}; + +fn pkgfreeform(value: str, capacity: u64) void = { + if (value.ptr != nil && capacity != 0u64) { + os.free(value.ptr: *void, capacity); + }; +}; + +fn pkgglob(pattern: str, name: str) bool = { + let pi: i32 = 0; + let ni: i32 = 0; + let star: i32 = -1; + let resume: i32 = 0; + for (ni < name.len) { + if (pi + 2 < pattern.len && pattern[pi] == '.' + && pattern[pi + 1] == '.' && pattern[pi + 2] == '.') { + star = pi; + pi += 3; + resume = ni; + continue; + }; + if (pi < pattern.len + && pkgutf8rune(pattern, pi) == pkgutf8rune(name, ni)) { + pi += pkgutf8width(pattern, pi); + ni += pkgutf8width(name, ni); + continue; + }; + if (star >= 0 && resume < name.len && name[resume] != 0u8 + && name[resume] != '\n') { + resume += pkgutf8width(name, resume); + ni = resume; + pi = star + 3; + continue; + }; + return false; + }; + for (pi + 2 < pattern.len && pattern[pi] == '.' + && pattern[pi + 1] == '.' && pattern[pi + 2] == '.') { + pi += 3; + }; + return pi == pattern.len; +}; + +fn pkgmakematcher(pattern: str, out: *pkgmatcher) bool = { + out.full = ""; + out.base = ""; + out.fullcap = 0u64; + out.basecap = 0u64; + out.trailing = strings.hassuffix(pattern, "/..."); + out.valid = pkgvalidutf8(pattern); + if (!out.valid) { return true; }; + if (!pkgvendorform(pattern, &out.full, &out.fullcap)) { return false; }; + if (out.trailing && !pkgvendorform(pattern[0:pattern.len - 4], + &out.base, &out.basecap)) { + pkgfreeform(out.full, out.fullcap); + return false; + }; + return true; +}; + +fn pkgfreematcher(m: *pkgmatcher) void = { + pkgfreeform(m.full, m.fullcap); + pkgfreeform(m.base, m.basecap); +}; + +fn pkgmatchdir(m: *pkgmatcher, name: str, out: *bool) bool = { + if (!m.valid) { *out = false; return true; }; + // pkgcleanpath removes a leading ./ from a relative pattern. Descent from + // its implicit "." traversal root produces ./child spellings, so compare + // both values in the same cleaned coordinate system. + if (strings.hasprefix(name, "./") && name.len > 2) { name = name[2:name.len]; }; + let candidate: str; + let capacity: u64; + if (!pkgvendorform(name, &candidate, &capacity)) { return false; }; + *out = pkgglob(m.full, candidate) + || (m.trailing && pkgglob(m.base, candidate)); + pkgfreeform(candidate, capacity); + return true; +}; + +fn pkgexcluded(name: str) bool = { + return name.len != 0 && strings.compare(name, ".") != 0 + && strings.compare(name, "..") != 0 + && (name[0] == '.' || name[0] == '_' + || strings.compare(name, "testdata") == 0); +}; + +// A wildcard walk follows an explicitly selected root symlink but never a +// child symlink. Selection is separate from descent: vendor subtrees are +// walked, while the matcher prevents an implicit wildcard from consuming a +// non-terminal vendor element. +fn pkgdiscoverdir(pathname: str, st: *pkgdiscover, + matcher: *pkgmatcher) void = { if (st.fatal) { return; }; let rootstat: os.filestat; - match (os.lstat(&rootstat, path)) { + match (os.lstat(&rootstat, pathname)) { case void => void; case let e: os.oserror => { - pkgfailpath(path, "cannot stat package directory"); + pkgfailpath(pathname, "cannot stat package directory"); st.errors += 1; return; }; }; if (pkgmodeis(rootstat.mode, os.mode.LINK)) { - match (os.stat(&rootstat, path)) { + match (os.stat(&rootstat, pathname)) { case void => void; case let e: os.oserror => { - pkgfailpath(path, "cannot stat package directory"); + pkgfailpath(pathname, "cannot stat package directory"); st.errors += 1; return; }; }; }; if (!pkgmodeis(rootstat.mode, os.mode.DIR)) { - pkgfailpath(path, "expected one package directory"); + pkgfailpath(pathname, "expected one package directory"); st.errors += 1; return; }; - let fd: i32 = os.open(path, os.flag.RDONLY, 0i32); + let selected: bool = matcher == nil; + if (matcher != nil && !pkgmatchdir(matcher, pathname, &selected)) { + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + st.errors += 1; + st.fatal = true; + return; + }; + let fd: i32 = os.open(pathname, os.flag.RDONLY, 0i32); if (fd < 0) { - pkgfailpath(path, "cannot open directory"); + pkgfailpath(pathname, "cannot open directory"); st.errors += 1; return; }; @@ -451,6 +858,11 @@ fn pkgdiscoverdir(path: str, st: *pkgdiscover, recurse: bool) void = { }; }; buf.len = 8192; + let names: pkgdiscover; + let emptynames: []str; + names.paths = emptynames; + names.errors = 0; + names.fatal = false; let n: i64 = os.getdents64(fd, buf.ptr, 8192u64); for (n > 0i64) { let off: u64 = 0u64; @@ -458,7 +870,7 @@ fn pkgdiscoverdir(path: str, st: *pkgdiscover, recurse: bool) void = { let reclen: u64 = (buf[off + 16u64]: u64) + (buf[off + 17u64]: u64) * 256u64; if (reclen < 20u64 || off + reclen > n: u64) { - pkgfailpath(path, "malformed directory entry"); + pkgfailpath(pathname, "malformed directory entry"); st.errors += 1; off = n: u64; continue; @@ -469,54 +881,79 @@ fn pkgdiscoverdir(path: str, st: *pkgdiscover, recurse: bool) void = { let name: str; name.ptr = np; name.len = nl; - if (pkgkeepfile(name)) { - let child: str = strings.concat(path, "/", name); - let fi: os.filestat; - match (os.lstat(&fi, child)) { - case void => { - if (pkgmodeis(fi.mode, os.mode.LINK)) { - pkgfailpath(child, - "symlink source is not allowed"); - st.errors += 1; - } else if (pkgmodeis(fi.mode, os.mode.REG)) { - if (!pkgappenddiscovered(st, child)) { + if (!(strings.compare(name, ".") == 0 + || strings.compare(name, "..") == 0)) { + let ownedname: str; + if (!pkgstring(&ownedname, name)) { + st.errors += 1; + st.fatal = true; + os.close(fd); + return; + }; + if (!pkgappenddiscovered(&names, ownedname)) { + st.errors += names.errors; + st.fatal = names.fatal; os.close(fd); return; }; }; - }; - case let e: os.oserror => { - pkgfailpath(child, "cannot stat source"); - st.errors += 1; - }; - }; - } else if (recurse && name.len != 0 - && name[0] != '.' && name[0] != '_') { - let child: str = strings.concat(path, "/", name); - let fi: os.filestat; - match (os.lstat(&fi, child)) { - case void => { - if (pkgmodeis(fi.mode, os.mode.DIR)) { - pkgdiscoverdir(child, st, true); - if (st.fatal) { os.close(fd); return; }; - }; - }; - case let e: os.oserror => { - pkgfailpath(child, - "cannot stat directory entry"); - st.errors += 1; - }; - }; - }; off += reclen; }; n = os.getdents64(fd, buf.ptr, 8192u64); }; if (n < 0i64) { - pkgfailpath(path, "directory read failed"); + pkgfailpath(pathname, "directory read failed"); st.errors += 1; }; os.close(fd); + pkgsortstrings(names.paths); + let ni: i32 = 0; + for (ni < names.paths.len) { + let name: str = names.paths[ni]; + let child: str; + if (!pkgstring(&child, pathname, "/", name)) { + st.errors += 1; st.fatal = true; return; + }; + let fi: os.filestat; + match (os.lstat(&fi, child)) { + case void => { + if (pkgmodeis(fi.mode, os.mode.DIR)) { + if (matcher != nil && !pkgexcluded(name)) { + pkgdiscoverdir(child, st, matcher); + if (st.fatal) { return; }; + }; + } else if (selected && pkgkeepfile(name)) { + let sourceisregular: bool = pkgmodeis(fi.mode, os.mode.REG); + if (pkgmodeis(fi.mode, os.mode.LINK)) { + let target: os.filestat; + match (os.stat(&target, child)) { + case void => { + // Match go/build: a file symlink is a source under + // its directory-entry name; a directory target is not. + sourceisregular = pkgmodeis(target.mode, os.mode.REG); + }; + case let e: os.oserror => { + pkgfailpath(child, "cannot stat source"); + st.errors += 1; + }; + }; + }; + if (sourceisregular && !pkgappenddiscovered(st, child)) { + return; + }; + }; + }; + case let e: os.oserror => { + if (selected && pkgkeepfile(name)) { + pkgfailpath(child, "cannot stat source"); + } else { + pkgfailpath(child, "cannot stat directory entry"); + }; + st.errors += 1; + }; + }; + ni += 1; + }; }; // Group paths by containing directory before filename order. A plain full-path @@ -560,15 +997,17 @@ fn pkgsortgroups(gs: []pkggroup) void = { fn pkgdedup(ss: []str) []str = { if (ss.len == 0) { return ss; }; - let out: []str = alloc([], ss.len: u64)!; - let i: i32 = 0; + let n: i32 = 1; + let i: i32 = 1; for (i < ss.len) { - if (i == 0 || strings.compare(ss[i - 1], ss[i]) != 0) { - append(out, ss[i]); + if (strings.compare(ss[n - 1], ss[i]) != 0) { + ss[n] = ss[i]; + n += 1; }; i += 1; }; - return out; + ss.len = n; + return ss; }; fn pkgparsedec(s: str, max: i64) i64 = { @@ -585,10 +1024,10 @@ fn pkgparsedec(s: str, max: i64) i64 = { return n; }; -fn pkgdefaultbuilder() str = { +fn pkgdefaultbuilder(out: *str) bool = { let av: []str = os.args(); - if (av.len == 0 || av[0].len == 0) { return "ww"; }; - return strings.concat(pkgdirname(av[0]), "/ww"); + if (av.len == 0 || av[0].len == 0) { *out = "ww"; return true; }; + return pkgstring(out, pkgdirname(av[0]), "/ww"); }; fn pkgmakedir(path: str) bool = { @@ -598,18 +1037,40 @@ fn pkgmakedir(path: str) bool = { // Resolve a directory to the kernel's symlink-free absolute spelling. The // coordinator is single-threaded while planning, so the temporary cwd change // cannot race a child process launch. -fn pkgcanonicaldir(path: str, out: *str) bool = { - let before: []u8 = alloc([], os.PATH_MAX: u64)!; +fn pkgcanonicaldir(path: str, out: *str, oom: *bool) bool = { + *oom = false; + let beforeallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX); + let before: []u8; + match (beforeallocation) { + case let value: []u8 => before = value; + case nomem => { + *oom = true; + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return false; + }; + }; before.len = os.PATH_MAX; let bn: i64 = os.getcwd(before.ptr, before.len: u64); if (bn <= 1i64 || bn > before.len: i64) { return false; }; if (os.chdir(path) != 0) { return false; }; - let after: []u8 = alloc([], os.PATH_MAX: u64)!; + let afterallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX); + let after: []u8; + match (afterallocation) { + case let value: []u8 => after = value; + case nomem => { + *oom = true; + let restored: i32 = os.chdir(strings.frombytes( + before[0:(bn - 1i64): i32])); + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return false; + }; + }; after.len = os.PATH_MAX; let an: i64 = os.getcwd(after.ptr, after.len: u64); let restored: i32 = os.chdir(strings.frombytes(before[0:(bn - 1i64): i32])); if (restored != 0 || an <= 1i64 || an > after.len: i64) { return false; }; - *out = strings.dup(strings.frombytes(after[0:(an - 1i64): i32])); + after.len = (an - 1i64): i32; + *out = strings.frombytes(after); return true; }; @@ -657,7 +1118,10 @@ fn pkgremoveall(path: str) bool = { name.ptr = np; name.len = nl; if (!(strings.compare(name, ".") == 0 || strings.compare(name, "..") == 0)) { - let child: str = strings.concat(path, "/", name); + let child: str; + if (!pkgstring(&child, path, "/", name)) { + ok = false; off = n: u64; continue; + }; let dtype: u8 = buf[off + 18u64]; if (dtype == 4u8 || (dtype == 0u8 && pkgisdir(child))) { if (!pkgremoveall(child)) { ok = false; }; @@ -683,87 +1147,50 @@ fn pkgemitfile(path: str, fd: i32) bool = { return true; }; -fn pkgworkescape(s: str) str = { - let out: []u8 = alloc([], (s.len * 2 + 1): u64)!; - let i: i32 = 0; - for (i < s.len) { - if (s[i] == '_') { - append(out, '_'); append(out, 'u'); - } else { if (s[i] == '/') { - append(out, '_'); append(out, 's'); - } else { - append(out, s[i]); - }; }; - i += 1; - }; - return strings.frombytes(out); -}; - -fn pkgworkkey(dir: str) str = { - let escaped: str = strings.concat("d_", pkgworkescape(dir)); - if (escaped.len <= 255) { return escaped; }; - let state: sha256.state = sha256.sha256(); - let h: *hash.hash = (&state): *hash.hash; - hash.write(h, strings.toutf8("ww-request-workdir-v1:")); - hash.write(h, strings.toutf8(dir)); - let digest: [32]u8; - hash.sum(h, digest[0:32]); - let out: []u8 = alloc([], 65u64)!; - let digits: str = "0123456789abcdef"; - let i: i32 = 0; - for (i < 32) { - let high: i32 = (digest[i] / 16u8): i32; - let low: i32 = (digest[i] % 16u8): i32; - append(out, digits[high]); - append(out, digits[low]); - i += 1; - }; - return strings.concat("d_", strings.frombytes(out)); -}; - fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32, - compileonly: bool, outname: str, workroot: str) bool = { + compileonly: bool, buildonly: bool, outname: str, outputdir: bool, + workroot: str) bool = { let num: str = strconv.i32tos(index, strconv.base.DEC); - p.root = strings.concat(root, "/plan-", num); - if (!pkgmakedir(p.root)) { return false; }; - p.workdir = ""; - p.workdircreated = false; - if (workroot.len != 0) { - // Canonical request-directory identity makes equivalent spellings and - // product reorderings select one command-scoped driver workdir. - p.workdir = strings.concat(workroot, "/", - pkgworkkey(p.sourceroot)); - match (os.mkdirs(workroot, 448)) { - case void => void; - case let e: os.oserror => return false; - }; - let mr: i32 = os.mkdir(p.workdir, 448i32); - if (mr == 0) { p.workdircreated = true; } - else if (mr != -17 || !pkgisdir(p.workdir)) { - return false; - }; + if (!pkgstring(&p.root, root, "/plan-", num)) { return false; }; + if (!pkgmakedir(p.root)) { + pkgfailpath(p.root, "cannot create directory-plan temporary path"); + return false; }; - p.buildout = strings.concat(p.root, "/build.stdout"); - p.builderr = strings.concat(p.root, "/build.stderr"); + // The caller's -w directory is the semantic-action store itself. A pattern + // spelling or traversal prefix must never select another cache container. + // The private driver creates it only after graph preflight succeeds. + p.workdir = workroot; + p.outputdir = ""; + if (outputdir) { p.outputdir = outname; }; + if (!pkgstring(&p.buildout, p.root, "/build.stdout") + || !pkgstring(&p.builderr, p.root, "/build.stderr")) { return false; }; let i: i32 = p.start; for (i < p.end) { let g: *pkggroup = &groups[i]; g.plan = index; - g.root = strings.concat(p.root, "/product-", - strconv.i32tos(i - p.start, strconv.base.DEC)); - if (!pkgmakedir(g.root)) { return false; }; - if (compileonly) { + if (!pkgstring(&g.root, p.root, "/product-", + strconv.i32tos(i - p.start, strconv.base.DEC))) { return false; }; + if (!pkgmakedir(g.root)) { + pkgfailpath(g.root, "cannot create product temporary path"); + return false; + }; + if (buildonly) { + if (outputdir && strings.compare(g.pkg, "main") == 0) { + if (!pkgjoinpath(outname, pkgbase(g.dir), &g.bin)) { return false; }; + } else if (outname.len != 0 && !outputdir) { g.bin = outname; } + else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; }; + } else if (compileonly) { if (outname.len != 0) { g.bin = outname; } else { - g.bin = strings.concat(g.dir, "/", g.pkg, ".test"); + if (!pkgstring(&g.bin, g.dir, "/", g.pkg, ".test")) { return false; }; }; } else { - g.bin = strings.concat(g.root, "/package.test"); + if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; }; }; - g.runout = strings.concat(g.root, "/test.stdout"); - g.runerr = strings.concat(g.root, "/test.stderr"); - g.buildok = strings.concat(g.root, "/build.ok"); + if (!pkgstring(&g.runout, g.root, "/test.stdout") + || !pkgstring(&g.runerr, g.root, "/test.stderr") + || !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; }; i += 1; }; return true; @@ -802,9 +1229,9 @@ fn pkgreportcommand(kind: str, g: *pkggroup, r: *exec.result) void = { }; fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, - h: *exec.process) bool = { + libdirs: []str, libs: []str, h: *exec.process) bool = { let nproducts: i32 = p.end - p.start; - let capacity: i32 = 12; + let capacity: i32 = 16; if (nproducts < 0 || nproducts > (PKG_COUNT_MAX - capacity) / 6) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); return false; @@ -815,6 +1242,16 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, return false; }; capacity += includes.len * 2; + if (libdirs.len > (PKG_COUNT_MAX - capacity) / 2) { + pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); + return false; + }; + capacity += libdirs.len * 2; + if (libs.len > (PKG_COUNT_MAX - capacity) / 2) { + pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); + return false; + }; + capacity += libs.len * 2; let allocation: ([]str | nomem) = pkgallocstrs(capacity); let ba: []str; match (allocation) { @@ -827,6 +1264,14 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, append(ba, builder); append(ba, "test"); append(ba, "-c"); + if (p.buildonly) { append(ba, "--ww-package-build"); }; + if (p.publish) { append(ba, "--ww-package-publish"); }; + if (p.emitasm) { append(ba, "-S"); }; + if (p.workdir.len != 0) { append(ba, "--ww-create-workdir"); }; + if (p.outputdir.len != 0) { + append(ba, "--ww-create-output-dir"); + append(ba, p.outputdir); + }; if (p.identity.len != 0) { append(ba, "--ww-root-identity"); append(ba, p.identity); @@ -845,16 +1290,24 @@ fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str, append(ba, g.buildok); i += 1; }; - // Recursive descendants derive full identities relative to the canonical - // discovery root using the driver's ordinary ordered import lookup. - append(ba, "-I"); - append(ba, p.sourceroot); let ii: i32 = 0; for (ii < includes.len) { append(ba, "-I"); append(ba, includes[ii]); ii += 1; }; + ii = 0; + for (ii < libdirs.len) { + append(ba, "-L"); + append(ba, libdirs[ii]); + ii += 1; + }; + ii = 0; + for (ii < libs.len) { + append(ba, "-l"); + append(ba, libs[ii]); + ii += 1; + }; if (p.workdir.len != 0) { append(ba, "-w"); append(ba, p.workdir); @@ -893,7 +1346,9 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, }; }; append(ra, g.bin); - append(ra, strings.concat("-package=", g.pkg)); + let packagearg: str; + if (!pkgstring(&packagearg, "-package=", g.pkg)) { return false; }; + append(ra, packagearg); if (list) { append(ra, "-list"); }; if (timeoutarg.len != 0) { append(ra, timeoutarg); }; let i: i32 = 0; @@ -914,7 +1369,8 @@ fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str, return true; }; -fn pkgproductbuilt(g: *pkggroup) bool = { +fn pkgproductbuilt(g: *pkggroup, buildonly: bool) bool = { + if (buildonly) { return pkgisreg(g.buildok); }; return pkgisreg(g.buildok) && pkgisreg(g.bin); }; @@ -963,7 +1419,7 @@ fn pkgemitgroup(g: *pkggroup, compileonly: bool) bool = { // A directory build capture is emitted once, followed by its independently // executed products in their existing byte-sorted group order. fn pkgemitplan(p: *pkgplan, groups: []pkggroup, - compileonly: bool) i32 = { + compileonly: bool, buildonly: bool) i32 = { let buildcaptures: bool = pkgemitfile(p.buildout, os.STDOUT_FILENO); buildcaptures = pkgemitfile(p.builderr, os.STDERR_FILENO) && buildcaptures; if (!buildcaptures) { @@ -973,9 +1429,11 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup, let failed: i32 = 0; let i: i32 = p.start; for (i < p.end) { - if (!pkgproductbuilt(&groups[i])) { + if (!pkgproductbuilt(&groups[i], buildonly)) { pkgreportcommand("build", &groups[i], &p.buildres); failed += 1; + } else if (buildonly) { + void; } else if (!pkgemitgroup(&groups[i], compileonly)) { failed += 1; }; @@ -986,9 +1444,11 @@ fn pkgemitplan(p: *pkgplan, groups: []pkggroup, export fn packagecommand(args: []str) int = { let compileonly: bool = false; + let buildonly: bool = false; let list: bool = false; let jobs: i32 = 1; let afterdash: bool = false; + let buildrootseen: bool = false; if (args.len == PKG_COUNT_MAX) { pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large"); @@ -1022,18 +1482,52 @@ export fn packagecommand(args: []str) int = { return 1; }; }; + let libdirallocation: ([]str | nomem) = pkgallocstrs(argcapacity); + let libdirs: []str; + match (libdirallocation) { + case let value: []str => libdirs = value; + case nomem => { + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return 1; + }; + }; + let liballocation: ([]str | nomem) = pkgallocstrs(argcapacity); + let libs: []str; + match (liballocation) { + case let value: []str => libs = value; + case nomem => { + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return 1; + }; + }; + let emitasm: bool = false; let timeoutarg: str = ""; let outname: str = ""; + let explicitout: bool = false; let workroot: str = ""; let requestidentity: str = ""; - let builder: str = pkgdefaultbuilder(); + let builder: str; + if (!pkgdefaultbuilder(&builder)) { return 1; }; let i: i32 = 0; for (i < args.len) { let a: str = args[i]; - if (afterdash) { append(filters, a); i += 1; continue; }; + if (buildonly && buildrootseen) { + append(roots, a); + i += 1; + continue; + }; + if (afterdash) { + if (buildonly) { append(roots, a); } + else { append(filters, a); }; + i += 1; + continue; + }; if (strings.compare(a, "--") == 0) { afterdash = true; i += 1; continue; }; if (strings.compare(a, "-c") == 0) { compileonly = true; i += 1; continue; }; - if (strings.compare(a, "-list") == 0) { list = true; i += 1; continue; }; + if (strings.compare(a, "-S") == 0) { emitasm = true; i += 1; continue; }; + if (!buildonly && strings.compare(a, "-list") == 0) { + list = true; i += 1; continue; + }; if (strings.compare(a, "-I") == 0) { if (i + 1 >= args.len) { pkgusage(); return 2; }; append(includes, args[i + 1]); @@ -1045,14 +1539,38 @@ export fn packagecommand(args: []str) int = { i += 1; continue; }; + if (strings.compare(a, "-L") == 0) { + if (i + 1 >= args.len) { pkgusage(); return 2; }; + append(libdirs, args[i + 1]); + i += 2; + continue; + }; + if (strings.hasprefix(a, "-L") && a.len > 2) { + append(libdirs, a[2:a.len]); + i += 1; + continue; + }; + if (strings.compare(a, "-l") == 0) { + if (i + 1 >= args.len) { pkgusage(); return 2; }; + append(libs, args[i + 1]); + i += 2; + continue; + }; + if (strings.hasprefix(a, "-l") && a.len > 2) { + append(libs, a[2:a.len]); + i += 1; + continue; + }; if (strings.compare(a, "-o") == 0) { if (i + 1 >= args.len) { pkgusage(); return 2; }; outname = args[i + 1]; + explicitout = true; i += 2; continue; }; if (strings.hasprefix(a, "-o") && a.len > 2) { outname = a[2:a.len]; + explicitout = true; i += 1; continue; }; @@ -1100,6 +1618,17 @@ export fn packagecommand(args: []str) int = { i += 2; continue; }; + if (strings.compare(a, "--ww-operation") == 0) { + if (i + 1 >= args.len || strings.compare(args[i + 1], "build") != 0 + || buildonly) { + pkgusage(); + return 2; + }; + buildonly = true; + compileonly = true; + i += 2; + continue; + }; if (strings.compare(a, "--ww-root-identity") == 0) { if (i + 1 >= args.len || args[i + 1].len == 0 || requestidentity.len != 0) { @@ -1112,15 +1641,17 @@ export fn packagecommand(args: []str) int = { }; if (a.len != 0 && a[0] == '-') { pkgusage(); return 2; }; append(roots, a); + if (buildonly) { buildrootseen = true; }; i += 1; }; if (roots.len == 0) { append(roots, "."); }; - if (roots.len != 1) { - pkgputln(os.STDERR_FILENO, - "wwtest package: exactly one package directory is supported"); + pkgsortstrings(roots); + if (compileonly && !buildonly + && (list || filters.len != 0 || timeoutarg.len != 0)) { + pkgusage(); return 2; }; - if (compileonly && (list || filters.len != 0 || timeoutarg.len != 0)) { + if (!buildonly && (emitasm || libdirs.len != 0 || libs.len != 0)) { pkgusage(); return 2; }; @@ -1133,40 +1664,116 @@ export fn packagecommand(args: []str) int = { // -c publishes caller-owned sepwork artifacts; mixing that // contract with a persistent workdir is unwired — reject rather // than guess which tree the caller owns. - if (workroot.len != 0 && compileonly) { + if (workroot.len != 0 && compileonly && !buildonly) { pkgputln(os.STDERR_FILENO, "wwtest package: -w conflicts with -c"); return 2; }; - // Go's ./... form: a trailing "..." path element walks the tree - // rooted at the prefix instead of one explicit directory. - let discoverroot: str = roots[0]; - let recurse: bool = false; - if (strings.compare(discoverroot, "...") == 0) { - discoverroot = "."; - recurse = true; - } else if (strings.hassuffix(discoverroot, "/...")) { - discoverroot = discoverroot[0:discoverroot.len - 4]; - if (discoverroot.len == 0) { discoverroot = "/"; }; - recurse = true; - }; - let ds: pkgdiscover; let emptypaths: []str; ds.paths = emptypaths; ds.errors = 0; ds.fatal = false; - pkgdiscoverdir(discoverroot, &ds, recurse); - if (ds.errors != 0) { return 1; }; - pkgsort(ds.paths); - if (ds.paths.len == 0) { - pkgfailpath(discoverroot, "directory contains no WW package sources"); - return 1; + let directroots: pkgdiscover; + let emptydirect: []str; + directroots.paths = emptydirect; + directroots.errors = 0; + directroots.fatal = false; + let anyrecurse: bool = false; + i = 0; + for (i < roots.len) { + let requested: str = roots[i]; + let cleaned: str; + if (!pkgcleanpath(requested, &cleaned)) { + return 1; + }; + let ellipsis: i32 = pkgfirstellipsis(cleaned); + let recurse: bool = ellipsis >= 0; + let discoverroot: str = cleaned; + let matcher: pkgmatcher; + if (recurse) { + let slash: i32 = ellipsis - 1; + for (slash >= 0 && cleaned[slash] != '/') { slash -= 1; }; + if (slash < 0) { discoverroot = "."; } + else if (slash == 0) { discoverroot = "/"; } + else { discoverroot = cleaned[0:slash]; }; + if (!pkgmakematcher(cleaned, &matcher)) { + pkgputln(os.STDERR_FILENO, "wwtest package: out of memory"); + return 1; + }; + }; + if (recurse) { anyrecurse = true; }; + let canonical: str; + let canonicaloom: bool = false; + if (!pkgcanonicaldir(discoverroot, &canonical, &canonicaloom)) { + if (canonicaloom) { return 1; }; + pkgfailpath(discoverroot, "cannot canonicalize package directory"); + ds.errors += 1; + if (recurse) { pkgfreematcher(&matcher); }; + i += 1; + continue; + }; + if (!recurse && !pkgappenddiscovered(&directroots, canonical)) { + return 1; + }; + let before: i32 = ds.paths.len; + let errorsbefore: i32 = ds.errors; + if (!recurse) { + pkgdiscoverdir(discoverroot, &ds, nil); + } else { + // MatchDirs applies .*, _*, and testdata pruning to the walk root + // itself as well as descendants. Literal direct selection stays legal. + if (!pkgexcluded(pkgbase(discoverroot))) { + pkgdiscoverdir(discoverroot, &ds, &matcher); + }; + pkgfreematcher(&matcher); + }; + if (recurse && ds.paths.len == before && ds.errors == errorsbefore) { + pkgput(os.STDERR_FILENO, "ww: warning: "); + pkgputquoted(os.STDERR_FILENO, requested); + pkgputln(os.STDERR_FILENO, " matched no packages"); + } else if (!recurse && ds.paths.len == before + && ds.errors == errorsbefore) { + pkgfailpath(discoverroot, "directory contains no WW package sources"); + ds.errors += 1; + }; + i += 1; }; - let canonicalroot: str; - if (!pkgcanonicaldir(discoverroot, &canonicalroot)) { - pkgfailpath(discoverroot, "cannot canonicalize package directory"); + if (ds.errors != 0) { return 1; }; + pkgsortstrings(directroots.paths); + directroots.paths = pkgdedup(directroots.paths); + i = 0; + for (i < ds.paths.len) { + let canonicaldir: str; + let canonicaloom: bool = false; + if (!pkgcanonicaldir(pkgdirname(ds.paths[i]), &canonicaldir, + &canonicaloom)) { + if (canonicaloom) { return 1; }; + pkgfailpath(ds.paths[i], "cannot canonicalize package directory"); + return 1; + }; + let canonicalsource: str; + if (!pkgstring(&canonicalsource, canonicaldir, "/", + pkgbase(ds.paths[i]))) { return 1; }; + ds.paths[i] = canonicalsource; + i += 1; + }; + pkgsort(ds.paths); + ds.paths = pkgdedup(ds.paths); + if (ds.paths.len == 0) { + if (buildonly) { + if (explicitout && pkgoutputdir(outname)) { + pkgputln(os.STDERR_FILENO, "ww: no main packages to build"); + return 1; + }; + if (explicitout) { + pkgputln(os.STDERR_FILENO, "ww: no packages to build"); + return 1; + }; + return 0; + }; + pkgputln(os.STDERR_FILENO, "ww test: no packages to test"); return 1; }; @@ -1190,8 +1797,8 @@ export fn packagecommand(args: []str) int = { }; let s: pkgsource; s.path = ds.paths[i]; - s.dir = strings.dup(pkgdirname(ds.paths[i])); - s.pkg = strings.dup(pn); + if (!pkgstring(&s.dir, pkgdirname(ds.paths[i])) + || !pkgstring(&s.pkg, pn)) { return 1; }; s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww"); append(srcs, s); i += 1; @@ -1237,9 +1844,36 @@ export fn packagecommand(args: []str) int = { i = 0; for (i < folders.len) { let f: pkgfolder = folders[i]; + if (buildonly) { + if (f.prodpkg.len != 0) { + let g: pkggroup; + g.dir = f.path; + g.pkg = f.prodpkg; + g.external = false; + g.production = true; + append(groups, g); + } else { + let explicit: bool = false; + let di: i32 = 0; + for (di < directroots.paths.len) { + if (strings.compare(directroots.paths[di], f.path) == 0) { + explicit = true; + break; + }; + di += 1; + }; + if (explicit) { + pkgfailpath(f.path, + "directory contains no WW package sources"); + return 1; + }; + }; + i += 1; + continue; + }; let externalpkg: str = ""; if (f.prodpkg.len != 0) { - externalpkg = strings.concat(f.prodpkg, "_test"); + if (!pkgstring(&externalpkg, f.prodpkg, "_test")) { return 1; }; }; let standalonepkg: str = ""; let sawtestfile: bool = false; @@ -1297,15 +1931,94 @@ export fn packagecommand(args: []str) int = { }; i += 1; }; - if (groups.len == 0) { return 0; }; + if (groups.len == 0) { + if (buildonly) { + if (explicitout && pkgoutputdir(outname)) { + pkgputln(os.STDERR_FILENO, "ww: no main packages to build"); + return 1; + }; + if (explicitout) { + pkgputln(os.STDERR_FILENO, "ww: no packages to build"); + return 1; + }; + return 0; + }; + pkgputln(os.STDERR_FILENO, "ww test: no packages to test"); + return 1; + }; pkgsortgroups(groups); - // One caller-owned name cannot fan out (Go: `go test -o` with - // multiple packages is an error). - if (outname.len != 0 && groups.len > 1) { + let defaultout: bool = false; + if (buildonly && outname.len == 0 && folders.len == 1 + && groups.len == 1 && strings.compare(groups[0].pkg, "main") == 0) { + outname = pkgbase(groups[0].dir); + defaultout = true; + }; + if (defaultout && pkgstatdir(outname)) { + pkgput(os.STDERR_FILENO, "ww: build output \""); + pkgput(os.STDERR_FILENO, outname); + pkgputln(os.STDERR_FILENO, "\" already exists and is a directory"); + return 1; + }; + let outputdir: bool = buildonly && explicitout && pkgoutputdir(outname); + if (outputdir) { + let mainpackages: i32 = 0; + i = 0; + for (i < groups.len) { + if (strings.compare(groups[i].pkg, "main") == 0) { + mainpackages += 1; + }; + i += 1; + }; + if (mainpackages == 0) { + pkgputln(os.STDERR_FILENO, "ww: no main packages to build"); + return 1; + }; + i = 0; + for (i < groups.len) { + if (strings.compare(groups[i].pkg, "main") == 0) { + let slash: i64 = 1i64; + if (strings.hassuffix(outname, "/")) { slash = 0i64; }; + let outputlen: i64 = outname.len: i64 + slash + + pkgbase(groups[i].dir).len: i64; + if (outputlen + 1i64 > os.PATH_MAX: i64) { + pkgputln(os.STDERR_FILENO, + "ww: command output path is too long"); + return 1; + }; + let j: i32 = 0; + for (j < i) { + if (strings.compare(groups[j].pkg, "main") == 0 + && strings.compare(pkgbase(groups[j].dir), + pkgbase(groups[i].dir)) == 0) { + pkgput(os.STDERR_FILENO, + "ww: multiple commands produce output basename "); + pkgputquoted(os.STDERR_FILENO, pkgbase(groups[i].dir)); + pkgput(os.STDERR_FILENO, " in directory "); + pkgputquoted(os.STDERR_FILENO, outname); + pkgputln(os.STDERR_FILENO, ""); + return 1; + }; + j += 1; + }; + }; + i += 1; + }; + }; + // A non-directory caller-owned name cannot fan out. A directory output + // publishes each selected command under its canonical directory basename. + if (outname.len != 0 && !outputdir && groups.len > 1) { pkgputln(os.STDERR_FILENO, "wwtest package: cannot use -o with multiple packages"); return 2; }; + // Recursive/multi-root -S needs a caller-owned artifact tree. Without -w + // every assembly file would otherwise live only in the coordinator's + // temporary plan and disappear on successful return. + if (buildonly && emitasm && workroot.len == 0) { + pkgputln(os.STDERR_FILENO, + "wwtest package: recursive -S needs -w"); + return 2; + }; let planallocation: ([]pkgplan | nomem) = pkgallocplans(1); let plans: []pkgplan; match (planallocation) { @@ -1316,22 +2029,25 @@ export fn packagecommand(args: []str) int = { }; }; let plan: pkgplan; - plan.dir = discoverroot; - plan.sourceroot = canonicalroot; - plan.identity = requestidentity; + plan.dir = groups[0].dir; + plan.identity = ""; + if (roots.len == 1 && !anyrecurse) { plan.identity = requestidentity; }; plan.start = 0; plan.end = groups.len; plan.state = PKGQUEUED; + plan.buildonly = buildonly; + plan.publish = buildonly && explicitout && outname.len != 0 + && groups.len == 1 + && strings.compare(groups[0].pkg, "main") != 0; + plan.emitasm = emitasm; append(plans, plan); - let borrowed: str = temp.dir(); - let tmproot: str = strings.dup(borrowed); + let tmproot: str = temp.dir(); let failed: i32 = 0; i = 0; for (i < plans.len) { if (!pkgsetplanpaths(&plans[i], groups, tmproot, i, - compileonly, outname, workroot)) { - pkgfailpath(tmproot, "cannot create directory-plan temporary path"); + compileonly, buildonly, outname, outputdir, workroot)) { if (!pkgremoveall(tmproot)) { pkgput(os.STDERR_FILENO, "wwtest package: cleanup failed; retained "); @@ -1407,7 +2123,7 @@ export fn packagecommand(args: []str) int = { let g: *pkggroup = &groups[gi]; if (g.state == PKGQUEUED && plans[g.plan].state == PKGDONE) { - if (!pkgproductbuilt(g)) { + if (!pkgproductbuilt(g, buildonly)) { g.state = PKGDONE; productcompleted += 1; } else { @@ -1429,7 +2145,7 @@ export fn packagecommand(args: []str) int = { }; if (!filling && planlaunched < plans.len) { if (!pkgstartbuild(&plans[planlaunched], groups, builder, - includes, &handles[planlaunched])) { + includes, libdirs, libs, &handles[planlaunched])) { if (!pkgremoveall(tmproot)) { pkgput(os.STDERR_FILENO, "wwtest package: cleanup failed; retained "); @@ -1472,16 +2188,7 @@ export fn packagecommand(args: []str) int = { i = 0; for (i < plans.len) { - failed += pkgemitplan(&plans[i], groups, compileonly); - // The driver commits its tool stamp only after semantic preflight. If - // this request minted the persistent directory and rejection left it - // unstamped, reclaim it only if it is still empty. A concurrent or - // interrupted writer's contents are never recursively removed. - if (plans[i].workdircreated - && !os.exists(strings.concat(plans[i].workdir, "/.wwtool.stamp"))) { - let rr: i32 = os.rmdir(plans[i].workdir); - if (rr != 0 && rr != -2) { failed += 1; }; - }; + failed += pkgemitplan(&plans[i], groups, compileonly, buildonly); i += 1; }; if (!pkgremoveall(tmproot)) { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 72995eea..1cd0aa93 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -31,6 +31,8 @@ def SEP_LOCAL_IMPORT_PREFIX: str = "__wwlocal"; let selfpath: *u8; let sepfatalallocation: bool; +@symbol("rt_envp") fn rawenvp() **u8; + // Tool-local (NOT a lib wrapper): messages are built from many // fragments and we route through os.write to avoid libc stdio. // .len replaces the error-prone hand-counted byte literals these sites @@ -77,6 +79,28 @@ fn cstrlen(p: *u8) u64 = { return n; }; +fn rawenvvalue(name: str) *u8 = { + let env: **u8 = rawenvp(); + let i: i32 = 0; + for (env[i] != nil) { + let entry: *u8 = env[i]; + let j: i32 = 0; + for (j < name.len && entry[j] == name[j]) { j += 1; }; + if (j == name.len && entry[j] == '=': u8) { + return entry + ((j + 1): u64); + }; + i += 1; + }; + return nil; +}; + +fn clipathfits(command: str, flag: str, value: *u8) bool = { + if (cstrlen(value) < os.PATH_MAX: u64) { return true; }; + cerr("ww "); cerr(command); cerr(": "); cerr(flag); + cerr(" path is too long\n"); + return false; +}; + // Bridges the driver's argv-style *u8 paths to lib/os entrypoints // (str post-task-#23). fn pathstr(p: *u8) str = { @@ -242,21 +266,39 @@ fn toolpath(selfdir: *u8, envvar: str, name: str) *u8 = { // package.ww roots stay on runsingletest, which is the recursion boundary when // wwtest builds its already-aggregated package binaries. fn execpackagetests(selfdir: *u8, argv: **u8, argc: i32, start: i32, - targetindex: i32, resolved: *u8, rootidentity: *u8, adddot: bool) i32 = { - let prog: *u8 = joinpathlit(selfdir, "wwtest"); - match (os.getenv("WW_WWTEST")) { - case let p: str => { - if (p.len != 0) { prog = owncstr(p); }; - }; - case void => void; + targetindex: i32, resolved: *u8, rootidentity: *u8, adddot: bool, + buildonly: bool) i32 = { + let prog: *u8 = rawenvvalue("WW_WWTEST"); + if (prog == nil || prog[0u64] == 0u8) { + prog = sepjoinpathlit(selfdir, "wwtest"); + if (prog == nil) { return 1; }; }; - let cap: i32 = argc - start + 8; - let execargv: []*u8 = alloc([], cap: u64)!; - execargv.len = cap; + if (start < 0 || start > argc || argc - start > SEP_COUNT_MAX - 10) { + sepfailsize(); + return 1; + }; + let cap: i32 = argc - start + 10; + let execargv: []*u8; + let argvallocation: ([]*u8 | nomem) = sepallocptrs(cap); + match (argvallocation) { + case let value: []*u8 => { execargv = value; execargv.len = cap; }; + case nomem => { + if (buildonly) { + cerr("ww build: cannot allocate package coordinator arguments\n"); + } else { + cerr("ww test: cannot allocate package coordinator arguments\n"); + }; + return 1; + }; + }; let n: i32 = 0; execargv[n] = prog; n += 1; execargv[n] = "package".ptr; n += 1; + if (buildonly) { + execargv[n] = "--ww-operation".ptr; n += 1; + execargv[n] = "build".ptr; n += 1; + }; execargv[n] = "--ww-driver".ptr; n += 1; execargv[n] = selfpath; n += 1; if (rootidentity != nil) { @@ -278,14 +320,9 @@ fn execpackagetests(selfdir: *u8, argv: **u8, argc: i32, start: i32, if (adddot && !dotted) { execargv[n] = ".".ptr; n += 1; }; execargv[n] = nil; - let env: []str = os.getenvs(); - let envp: []*u8 = alloc([], (env.len + 1): u64)!; - envp.len = env.len + 1; - i = 0; - for (i < env.len) { envp[i] = owncstr(env[i]); i += 1; }; - envp[env.len] = nil; - os.execve(pathstr(prog), execargv.ptr, envp.ptr); - cerr("ww test: cannot exec package test coordinator\n"); + os.execve(pathstr(prog), execargv.ptr, rawenvp()); + if (buildonly) { cerr("ww build: cannot exec package coordinator\n"); } + else { cerr("ww test: cannot exec package coordinator\n"); }; return 1; }; @@ -534,6 +571,7 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64, // nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but // would pass strings.hassuffix(".ww"); preserves cstage parity. if (nlen <= 3u64) { return 0; }; + if (name[0] == '.' || name[0] == '_') { return 0; }; let s: str; s.ptr = name; s.len = nlen: i32; @@ -545,13 +583,27 @@ fn dirfileclass(dirpath: *u8, name: *u8, nlen: u64, let fi: os.filestat; let sr: (void | os.oserror) = os.lstat(&fi, pathstr(source)); let regular: bool = false; + let directory: bool = false; match (sr) { case void => { let t: u32 = (fi.mode: u32) & 61440u32; - if (t == os.mode.REG: u32) { regular = true; }; + if (t == os.mode.REG: u32) { regular = true; } + else if (t == os.mode.LINK: u32) { + let target: os.filestat; + match (os.stat(&target, pathstr(source))) { + case void => { + let tt: u32 = (target.mode: u32) & 61440u32; + if (tt == os.mode.REG: u32) { regular = true; }; + if (tt == os.mode.DIR: u32) { directory = true; }; + }; + case let e: os.oserror => void; + }; + }; }; case let e: os.oserror => void; }; + // go/build ignores a source-shaped symlink to a directory. + if (directory) { return 0; }; if (!regular) { cerr("ww: "); cerr(pathstr(source)); cerr(": package source is not a regular file\n"); @@ -4265,6 +4317,14 @@ fn validatepackageoutputpath(out: *u8) i32 = { return 0; }; +fn validatecommandoutputpath(out: *u8) i32 = { + if (out == nil || cstrlen(out) + 1u64 > os.PATH_MAX: u64) { + cerr("ww: command output path is too long\n"); + return -1; + }; + return 0; +}; + // The stamp pins the non-content build inputs a unit compare cannot see: // the -T/-S/root-action shape of the producer pass and the artifact protocol // revision (bump "fmt" when the unit/archive/commit format changes). @@ -4374,6 +4434,84 @@ fn invalidateworkdirunits(scratch: *u8) i32 = { return rc; }; +type sepcreateddirs = struct { + path: [4096]u8, + offset: [2048]u16, + n: i32, +}; + +fn seprollbackdirs(created: *sepcreateddirs) void = { + for (created.n > 0) { + created.n -= 1; + let at: u64 = created.offset[created.n]: u64; + let saved: u8 = created.path[at]; + created.path[at] = 0u8; + let ignored: i32 = os.rmdir(pathstr(&created.path[0])); + created.path[at] = saved; + }; +}; + +// Keep the prefixes created after semantic preflight separately from +// pre-existing caller state so a later setup failure can reclaim only them. +fn sepmkdirsrecord(path: *u8, mode: i32, created: *sepcreateddirs) i32 = { + created.n = 0; + let n: u64 = cstrlen(path); + if (n == 0u64 || n >= os.PATH_MAX: u64) { return -1; }; + let buf: [4096]u8; + bytecpy(&buf[0], path, n + 1u64); + for (n > 1u64 && buf[n - 1u64] == 47u8) { + n -= 1u64; + buf[n] = 0u8; + }; + bytecpy(&created.path[0], &buf[0], n + 1u64); + let i: u64 = 0u64; + if (buf[0] == 47u8) { i = 1u64; }; + for (i <= n) { + if (buf[i] != 47u8 && buf[i] != 0u8) { + i += 1u64; + continue; + }; + let saved: u8 = buf[i]; + buf[i] = 0u8; + if (buf[0] != 0u8) { + let fi: os.filestat; + let missing: bool = false; + match (os.stat(&fi, pathstr(&buf[0]))) { + case void => { + let typ: u32 = (fi.mode: u32) & 61440u32; + if (typ != os.mode.DIR: u32) { + buf[i] = saved; + seprollbackdirs(created); + return -1; + }; + }; + case let e: os.oserror => { + if ((e: i64) == -2i64) { missing = true; } + else { + buf[i] = saved; + seprollbackdirs(created); + return -1; + }; + }; + }; + if (missing) { + if (created.n >= 2048 + || os.mkdir(pathstr(&buf[0]), mode) != 0) { + buf[i] = saved; + seprollbackdirs(created); + return -1; + }; + created.offset[created.n] = i: u16; + created.n += 1; + }; + }; + buf[i] = saved; + if (saved == 0u8) { break; }; + i += 1u64; + }; + return 0; +}; + // cerrpath — the "ww: \n" diagnostic shape shared by the // workdir error sites; byte-identical wording to the cstage twin's // fprintf(..., "%s", path) forms. @@ -4387,7 +4525,8 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32, requirecommand: i32, istest: i32, products: *sepproduct, nproducts: i32, emitasm: i32, - workdir: *u8, scratchout: **u8, + workdir: *u8, createworkdir: bool, createoutputdir: *u8, + scratchout: **u8, graphout: **sepgraph) i32 = { sepfatalallocation = false; if (nproducts < 1) { return 1; }; @@ -4461,6 +4600,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, let effstem: *u8 = stem; if (objstem != nil) { effstem = objstem; }; let warm: bool = false; + let workdirexists: bool = false; if (workdir != nil) { if (workdir[0u64] != 0u8) { warm = true; }; }; @@ -4468,23 +4608,38 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (warm) { let wfi: os.filestat; let wok: bool = false; + let missing: bool = false; match (os.stat(&wfi, pathstr(workdir))) { case void => { let wt: u32 = (wfi.mode: u32) & 61440u32; // S_IFMT - if (wt == os.mode.DIR: u32) { wok = true; }; + if (wt == os.mode.DIR: u32) { + wok = true; workdirexists = true; + }; }; - case let e: os.oserror => void; + case let e: os.oserror => { + if ((e: i64) == -2i64) { missing = true; }; }; - if (!wok) { + }; + if (!wok && !(createworkdir && missing)) { cerrpath("ww: workdir ", workdir, " is not a directory\n"); return 1; }; + if (cstrlen(workdir) + 1u64 > os.PATH_MAX: u64) { + cerr("ww: workdir path is too long\n"); + return 1; + }; // The workdir is caller-owned and persistent: no acquisition, // no refusal, and scratchout stays nil so the wrapper never // cleans it. scratch = workdir; } else { + let suffix: u64 = ".sepwork".len: u64 + 1u64; + if (suffix > os.PATH_MAX: u64 + || cstrlen(effstem) > os.PATH_MAX: u64 - suffix) { + cerr("ww: scratch path is too long\n"); + return 1; + }; scratch = sepappendlit(effstem, ".sepwork"); if (scratch == nil) { return 1; }; }; @@ -4500,6 +4655,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, cerrpath("ww: cannot read driver identity ", selfpath, "\n"); return 1; }; + let toolsuffix: u64 = "/.wwtool.stamp".len: u64 + 1u64; + if (toolsuffix > os.PATH_MAX: u64 + || cstrlen(scratch) > os.PATH_MAX: u64 - toolsuffix) { + cerr("ww: workdir path is too long\n"); + return 1; + }; toolw = sepjoinpathlit(scratch, ".wwtool.ww"); toolc = sepjoinpathlit(scratch, ".wwtool.w6c"); toola = sepjoinpathlit(scratch, ".wwtool.w6a"); @@ -4753,11 +4914,21 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti += 1; }; }; - let rootpackage: bool = istest == 0 + producti = 0; + for (producti < nproducts) { + let root: i32 = products[producti].root; + if (!g.pkg[root].failed && seprootiscommand(&g.pkg[root]) + && validatecommandoutputpath(products[producti].out) < 0) { + return 1; + }; + producti += 1; + }; + let rootpackage: bool = istest == 0 && nproducts == 1 && !g.pkg[products[0].root].failed && !seprootiscommand(&g.pkg[products[0].root]); if (sepvalidateartifactpaths(g, scratch) < 0) { return 1; }; - if (warm && sepvalidateworkdirowners(g, scratch) < 0) { return 1; }; + if (warm && workdirexists + && sepvalidateworkdirowners(g, scratch) < 0) { return 1; }; let ci: i32 = 0; let order: []i32; let stack: []i32; @@ -4836,9 +5007,30 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, if (!viableproduct) { return 1; }; // Source-derived resolution and contextual legality are complete before // coordinator completion markers or persistent vouchers are changed. + let createdoutput: sepcreateddirs; + let createdwork: sepcreateddirs; + createdoutput.n = 0; + createdwork.n = 0; + if (createoutputdir != nil + && sepmkdirsrecord(createoutputdir, 448, &createdoutput) != 0) { + cerrpath("ww: cannot create build output directory ", + createoutputdir, "\n"); + return 1; + }; + if (warm && !workdirexists) { + if (!createworkdir + || sepmkdirsrecord(scratch, 448, &createdwork) != 0) { + cerrpath("ww: workdir ", scratch, " is not a directory\n"); + seprollbackdirs(&createdoutput); + return 1; + }; + workdirexists = true; + }; if (!warm) { if (os.mkdir(pathstr(scratch), 493i32) != 0) { - cerr("ww: cannot create scratch\n"); + cerrpath("ww: cannot create scratch ", scratch, "\n"); + seprollbackdirs(&createdwork); + seprollbackdirs(&createdoutput); return 1; }; // The wrapper owns only the directory this invocation acquired. @@ -5179,7 +5371,22 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, }; }; }; - if (emitasm != 0) { if (anyfailed) { return 1; }; return 0; }; + if (emitasm != 0) { + let producti: i32 = 0; + for (producti < nproducts) { + let root: i32 = products[producti].root; + if (g.pkg[root].failed) { + anyfailed = true; + } else if (recordproductstatus(products[producti].status) != 0) { + cerr("ww: cannot record package-build product\n"); + g.pkg[root].failed = true; + anyfailed = true; + }; + producti += 1; + }; + if (anyfailed) { return 1; }; + return 0; + }; if (rootpackage) { let root: i32 = products[0].root; if (g.pkg[root].failed) { return 1; }; @@ -5196,6 +5403,10 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, return 1; }; }; + if (recordproductstatus(products[0].status) != 0) { + cerr("ww: cannot record package-build product\n"); + return 1; + }; return 0; }; @@ -5220,6 +5431,15 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, producti += 1; continue; }; + if (istest == 0 && !seprootiscommand(&g.pkg[root])) { + if (recordproductstatus(products[producti].status) != 0) { + cerr("ww: cannot record package-build product\n"); + g.pkg[root].failed = true; + anyfailed = true; + }; + producti += 1; + continue; + }; ci = 0; for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; let linkorder: []i32; @@ -5369,7 +5589,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, product.support = -1; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem, incs, lf, publishpackage, requirecommand, istest, &product, 1, - emitasm, workdir, &scratch, &g); + emitasm, workdir, false, nil, &scratch, &g); sepgraphfree(g); if (keepscratch == 0 && scratch != nil) { if (cstrendswithlit(scratch, ".sepwork")) { @@ -5398,20 +5618,21 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, // Build every selected directory/variant root inside one command-owned package // universe. The first output owns the shared cold sepwork tree. fn buildpackagetests(selfdir: *u8, src: *u8, rootidentity: *u8, - incs: *u8, workdir: *u8, products: *sepproduct, nproducts: i32) i32 = { + incs: *u8, workdir: *u8, products: *sepproduct, nproducts: i32, + istest: i32, publishpackage: i32, lf: *lflags, emitasm: i32, + createworkdir: bool, createoutputdir: *u8) i32 = { let scratch: *u8 = nil; let g: *sepgraph = nil; - let lf: lflags; - lf.libdirs = nil; lf.nlibdirs = 0; - lf.libs = nil; lf.nlibs = 0; let i: i32 = 0; for (i < nproducts) { products[i].identity = rootidentity; i += 1; }; let r: i32 = buildonesepimpl(selfdir, src, 1, - products[0].out, products[0].out, incs, &lf, 0, 0, 1, - products, nproducts, 0, workdir, &scratch, &g); + products[0].out, products[0].out, incs, lf, + publishpackage, 0, istest, + products, nproducts, emitasm, workdir, createworkdir, + createoutputdir, &scratch, &g); sepgraphfree(g); return r; }; @@ -5507,7 +5728,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = { }; fn writeusage(fd: i32) void = { - let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [-S] [-w DIR] [-I DIR] [-o FILE] [path] build a local package graph\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -o publishes a non-main archive FILE + FILE.wwi\n lib/... every package under lib, recursively (test only)\n . build the cwd's .ww\n"; + let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [-S] [-w DIR] [-I DIR] [-o FILE] [path ...] build local package graphs\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [-w DIR] [options] [path ...] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then the source library for foo.ww or foo/\n lib/foo directory: build its package sources\n -o publishes a non-main archive FILE + FILE.wwi\n lib/... every eligible package under lib, recursively\n . build the cwd's .ww\n"; os.write(fd, s.ptr, s.len: u64); }; @@ -5548,6 +5769,8 @@ fn defaultoutpath(src: *u8) *u8 = { fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; + let srcindex: i32 = -1; + let multipleroots: bool = false; let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 let workdir: *u8 = nil; // -w persistent package-artifact workdir let emitasm: i32 = 0; @@ -5564,18 +5787,19 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cstrseal(incs.ptr, 0u64); let maxlflags: i32 = 32; - let libdirs: []*u8; - if (!sepmakeptrs(maxlflags, &libdirs)) { return 1; }; + let libdirs: [32]*u8; let nlibdirs: i32 = 0; - let libs: []*u8; - if (!sepmakeptrs(maxlflags, &libs)) { return 1; }; + let libs: [32]*u8; let nlibs: i32 = 0; let i: i32 = start; for (i < argc) { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { // '-' - if (cstreqlit(p, "-S")) { + if (cstreqlit(p, "--")) { + multipleroots = true; + break; + } else { if (cstreqlit(p, "-S")) { emitasm = 1; } else { if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; @@ -5632,17 +5856,18 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { libs[nlibs] = nm; nlibs += 1; } else { if (p[1u64] == 111u8) { // '-o' - if (p[2u64] != 0u8) { - outflag = p + 2u64; + if (p[2u64] != 0u8) { + outflag = p + 2u64; } else { if (i + 1 >= argc) { cerr("ww build: -o needs an argument\n"); return 2; }; i += 1; - outflag = argv[i]; - }; - } else { if (p[1u64] == 119u8) { // '-w' + outflag = argv[i]; + }; + if (!clipathfits("build", "-o", outflag)) { return 2; }; + } else { if (p[1u64] == 119u8) { // '-w' if (p[2u64] != 0u8) { workdir = p + 2u64; } else { @@ -5651,14 +5876,21 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return 2; }; i += 1; - workdir = argv[i]; - }; + workdir = argv[i]; + }; + if (!clipathfits("build", "-w", workdir)) { return 2; }; } else { cerr("ww build: unknown flag\n"); return 2; - }; }; }; }; }; }; + }; }; }; }; }; }; }; } else { - if (src == nil) { src = p; }; + if (src == nil) { + src = p; + srcindex = i; + i += 1; + if (i < argc) { multipleroots = true; }; + break; + } else { multipleroots = true; break; }; }; i += 1; }; @@ -5667,6 +5899,10 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let dot: [2]u8 = ['.': u8, 0u8]; src = &dot[0]; }; + if (multipleroots || strings.contains(pathstr(src), "...")) { + return execpackagetests(selfdir, argv, argc, start, srcindex, + nil, nil, false, true); + }; let requestedliteral: bool = false; let requestedstat: os.filestat; match (os.stat(&requestedstat, pathstr(src))) { @@ -5704,9 +5940,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { out = defaultoutpath(resolved); }; }; let lf: lflags; - lf.libdirs = libdirs.ptr; + lf.libdirs = &libdirs[0]; lf.nlibdirs = nlibdirs; - lf.libs = libs.ptr; + lf.libs = &libs[0]; lf.nlibs = nlibs; let rootidentity: *u8 = nil; if (!requestedliteral && isdir != 0) { rootidentity = src; }; @@ -6085,6 +6321,18 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let requestidentity: *u8 = nil; let products: []sepproduct; let packageopts: bool = false; + let packagebuild: bool = false; + let packagepublish: bool = false; + let packagecreateworkdir: bool = false; + let packagecreateoutputdir: *u8 = nil; + let maxpackagelflags: i32 = 32; + let packagelibdirs: [32]*u8; + let packagelibs: [32]*u8; + let packagelinks: lflags; + packagelinks.libdirs = &packagelibdirs[0]; + packagelinks.nlibdirs = 0; + packagelinks.libs = &packagelibs[0]; + packagelinks.nlibs = 0; let afterdash: bool = false; let i: i32 = start; for (i < argc) { @@ -6105,6 +6353,44 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { i += 2; continue; }; + if (cstreqlit(p, "--ww-package-build")) { + if (packagebuild) { + cerr("ww test: invalid --ww-package-build\n"); + return 2; + }; + packagebuild = true; + compileonly = 1; + i += 1; + continue; + }; + if (cstreqlit(p, "--ww-package-publish")) { + if (packagepublish) { + cerr("ww test: invalid --ww-package-publish\n"); + return 2; + }; + packagepublish = true; + i += 1; + continue; + }; + if (cstreqlit(p, "--ww-create-workdir")) { + if (packagecreateworkdir) { + cerr("ww test: invalid --ww-create-workdir\n"); + return 2; + }; + packagecreateworkdir = true; + i += 1; + continue; + }; + if (cstreqlit(p, "--ww-create-output-dir")) { + if (i + 1 >= argc || packagecreateoutputdir != nil + || argv[i + 1][0u64] == 0u8) { + cerr("ww test: invalid --ww-create-output-dir\n"); + return 2; + }; + packagecreateoutputdir = argv[i + 1]; + i += 2; + continue; + }; if (cstreqlit(p, "--ww-package-test")) { if (i + 5 >= argc) { cerr("ww test: --ww-package-test needs kind, package, directory, output, and status\n"); @@ -6178,9 +6464,49 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; if (cstreqlit(p, "-c")) { compileonly = 1; i += 1; continue; }; if (cstreqlit(p, "-S")) { emitasm = 1; i += 1; continue; }; - if (cstreqlit(p, "-list")) { + if (!packagebuild && cstreqlit(p, "-list")) { packageopts = true; i += 1; continue; }; + if (packagebuild && p[1u64] == 76u8) { // '-L' + let dir: *u8 = nil; + if (p[2u64] != 0u8) { + dir = p + 2u64; + } else { + if (i + 1 >= argc) { + cerr("ww test: -L needs an argument\n"); + return 2; + }; + i += 1; + dir = argv[i]; + }; + if (packagelinks.nlibdirs >= maxpackagelflags) { + cerr("ww test: too many -L\n"); + return 2; + }; + packagelibdirs[packagelinks.nlibdirs] = dir; + packagelinks.nlibdirs += 1; + i += 1; continue; + }; + if (packagebuild && p[1u64] == 108u8) { // '-l' + let name: *u8 = nil; + if (p[2u64] != 0u8) { + name = p + 2u64; + } else { + if (i + 1 >= argc) { + cerr("ww test: -l needs an argument\n"); + return 2; + }; + i += 1; + name = argv[i]; + }; + if (packagelinks.nlibs >= maxpackagelflags) { + cerr("ww test: too many -l\n"); + return 2; + }; + packagelibs[packagelinks.nlibs] = name; + packagelinks.nlibs += 1; + i += 1; continue; + }; if (cstreqlit(p, "-j") || cstreqlit(p, "-run") || cstreqlit(p, "-filter")) { if (i + 1 >= argc) { @@ -6203,9 +6529,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return 2; }; i += 1; - outstem = argv[i]; - }; - i += 1; continue; + outstem = argv[i]; + }; + if (!clipathfits("test", "-o", outstem)) { return 2; }; + i += 1; continue; }; if (p[1u64] == 119u8) { // '-w' if (p[2u64] != 0u8) { @@ -6216,8 +6543,9 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return 2; }; i += 1; - workdir = argv[i]; - }; + workdir = argv[i]; + }; + if (!clipathfits("test", "-w", workdir)) { return 2; }; i += 1; continue; }; cerr("ww test: unknown flag\n"); return 2; @@ -6231,7 +6559,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let dot: [2]u8 = ['.': u8, 0u8]; target = &dot[0]; }; - if (emitasm != 0 && outstem == nil) { + if (emitasm != 0 && outstem == nil && products.len == 0) { cerr("ww test: -S needs -o\n"); return 2; }; @@ -6267,22 +6595,82 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: package-test variant rejects package options\n"); return 2; }; + if (packagebuild && products.len == 0) { + cerr("ww test: --ww-package-build needs products\n"); + return 2; + }; + if (packagepublish && (!packagebuild || products.len != 1)) { + cerr("ww test: invalid --ww-package-publish\n"); + return 2; + }; + if (packagecreateoutputdir != nil && !packagebuild) { + cerr("ww test: invalid private directory creation\n"); + return 2; + }; + if (packagecreateworkdir && products.len == 0) { + cerr("ww test: invalid private directory creation\n"); + return 2; + }; + if (packagecreateworkdir && workdir == nil) { + cerr("ww test: --ww-create-workdir needs -w\n"); + return 2; + }; + if (!packagebuild && (packagelinks.nlibdirs != 0 + || packagelinks.nlibs != 0)) { + cerr("ww test: unknown flag\n"); + return 2; + }; + if (packagebuild) { + producti = 0; + for (producti < products.len) { + if (products[producti].variant != SEP_VARIANT_PRODUCTION) { + cerr("ww test: package-build products must be production\n"); + return 2; + }; + producti += 1; + }; + }; + if (patarg != nil) { + let first: os.filestat; + let firstregular: bool = false; + let firstfound: bool = false; + match (os.stat(&first, pathstr(target))) { + case void => { + firstfound = true; + firstregular = (((first.mode: u32) & 61440u32) + == (os.mode.REG: u32)); + }; + case let e: os.oserror => void; + }; + if (!firstregular) { + let firstresolved: *u8 = nil; + let firstisdir: i32 = 0; + if (!firstfound) { + firstresolved = resolvemodule(selfdir, target, incs.ptr, + &firstisdir); + }; + if (firstfound || firstresolved == nil || firstisdir != 0) { + let replacement: *u8 = nil; + let identity: *u8 = requestidentity; + if (firstisdir != 0) { + replacement = firstresolved; + identity = target; + }; + return execpackagetests(selfdir, argv, argc, start, + targetindex, replacement, identity, false, false); + }; + // A logical import resolving to one source retains patarg as its + // historical test-name filter and follows the single-file path. + }; + }; if (products.len != 0 && outstem != nil) { cerr("ww test: package-test products reject -o\n"); return 2; }; - // Go's ./... form: a trailing "..." element is a package-tree - // request for the coordinator, never a literal path — recognized - // before stat, with the directory-mode rejects. - let tlen: u64 = cstrlen(target); - let istree: bool = cstreqlit(target, "..."); - if (!istree && tlen >= 4u64) { - istree = target[tlen - 4u64] == '/' - && target[tlen - 3u64] == '.' - && target[tlen - 2u64] == '.' - && target[tlen - 1u64] == '.'; - }; + // A local spelling containing "..." enters request selection before + // literal-path or logical-import resolution. + let istree: bool = strings.contains(pathstr(target), "..."); if (istree) { if (products.len != 0) { cerr("ww test: package-test variant needs one directory\n"); @@ -6298,14 +6686,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: -o needs -c for a package target\n"); return 2; }; - if (patarg != nil) { - cerr("ww test: pattern needs a single test file\n"); - return 2; - }; - // -w forwards: the coordinator keys one persistent driver workdir - // for the complete selected test request. + // -w forwards one caller-owned semantic-action store shared by + // the complete selected package universe. return execpackagetests(selfdir, argv, argc, start, - targetindex, nil, nil, false); + targetindex, nil, nil, false, false); }; let resolved: *u8 = target; @@ -6345,7 +6729,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { emitasm, outstem, workdir, patarg); }; - if (emitasm != 0) { + if (emitasm != 0 && products.len == 0) { cerr("ww test: -S needs a single test file\n"); return 2; }; @@ -6353,23 +6737,25 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: -o needs -c for a package target\n"); return 2; }; - if (patarg != nil) { - cerr("ww test: pattern needs a single test file\n"); - return 2; - }; if (products.len != 0) { if (compileonly == 0) { cerr("ww test: package-test products need -c\n"); return 2; }; + let packagemode: i32 = 1; + if (packagebuild) { packagemode = 0; }; + let publishmode: i32 = 0; + if (packagepublish) { publishmode = 1; }; return buildpackagetests(selfdir, resolved, rootidentity, incs.ptr, workdir, - products.ptr, products.len); + products.ptr, products.len, packagemode, publishmode, + &packagelinks, emitasm, packagecreateworkdir, + packagecreateoutputdir); }; let replacement: *u8 = nil; if (resolved != target) { replacement = resolved; }; return execpackagetests(selfdir, argv, argc, start, targetindex, - replacement, rootidentity, targetindex < 0); + replacement, rootidentity, targetindex < 0, false); }; export fn main(argc: i32, argv: **u8) i32 = {