3476 lines
109 KiB
C
3476 lines
109 KiB
C
/*
|
|
* ww — the user-facing driver. Plan 9 cc(1) / Hare hare(1) analogue.
|
|
*
|
|
* Tool paths default to siblings of $0 (so a fresh build runs out of
|
|
* out/bin/), and can be overridden with WW_W6C / WW_W6A / WW_W6L.
|
|
*/
|
|
#define _XOPEN_SOURCE 700
|
|
#include "ww.h"
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <libgen.h>
|
|
|
|
static const char *usage =
|
|
"usage: ww [-V] <subcommand> [args...]\n"
|
|
" -V print version and exit\n"
|
|
" build [-p] [-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"
|
|
" -p emits a non-main archive FILE + FILE.wwi\n"
|
|
" lib/... every package under lib, recursively (test only)\n"
|
|
" . build the cwd's <basename>.ww\n";
|
|
|
|
static char *self_dir;
|
|
static const char *self_path;
|
|
|
|
static const char *
|
|
envpath(const char *name)
|
|
{
|
|
const char *p = getenv(name);
|
|
return p && p[0] ? p : NULL;
|
|
}
|
|
|
|
static const char *
|
|
toolpath(const char *envvar, const char *name)
|
|
{
|
|
const char *p = envpath(envvar);
|
|
if (p) return p;
|
|
static char buf[1024];
|
|
snprintf(buf, sizeof buf, "%s/%s", self_dir, name);
|
|
return strdup(buf);
|
|
}
|
|
|
|
static int
|
|
run_argv(const char *prog, char *const argv[])
|
|
{
|
|
pid_t pid = fork();
|
|
if (pid < 0) { perror("ww: fork"); return -1; }
|
|
if (pid == 0) {
|
|
execv(prog, argv);
|
|
static const char msg[] = "ww: execve failed\n";
|
|
(void)write(2, msg, sizeof msg - 1);
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
pid_t got;
|
|
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
|
|
if (got < 0) { perror("ww: waitpid"); return -1; }
|
|
if (WIFEXITED(status)) return WEXITSTATUS(status);
|
|
return 1;
|
|
}
|
|
|
|
/* run_test_bin — exec the built test binary with an optional name-filter
|
|
* pattern as argv[1] (lib/test run() reads it via os.args). fork+execv
|
|
* (not system()) so glob metacharacters in the pattern reach the binary
|
|
* verbatim instead of being expanded by the shell. Mirrors the wwstage
|
|
* twin (selfhost/cmd/ww/main.ww runsingletest, which passes the same
|
|
* argv to os.exec.runstdio). #17 fnmatch filter. */
|
|
static int
|
|
run_test_bin(const char *bin, const char *pattern)
|
|
{
|
|
pid_t pid = fork();
|
|
if (pid < 0) { perror("ww: fork"); return -1; }
|
|
if (pid == 0) {
|
|
char *xargv[3];
|
|
xargv[0] = (char *)bin;
|
|
if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; }
|
|
else { xargv[1] = NULL; }
|
|
execv(bin, xargv);
|
|
perror("ww: exec");
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
/* the do_run twin's EINTR discipline: an interrupted wait left
|
|
* status==0, so WIFEXITED(0)/WEXITSTATUS(0) reported a false
|
|
* test PASS. */
|
|
pid_t got;
|
|
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
|
|
if (got < 0) { perror("ww: waitpid"); return -1; }
|
|
if (WIFEXITED(status)) return WEXITSTATUS(status);
|
|
return 1;
|
|
}
|
|
|
|
/* Delegate package/directory testing to the native WW coordinator. Keep the
|
|
* old single-file path in this driver: wwtest itself builds each generated
|
|
* 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)
|
|
{
|
|
const char *override = getenv("WW_WWTEST");
|
|
char fallback[1024];
|
|
const char *prog = override && override[0] ? override : fallback;
|
|
if (prog == fallback)
|
|
snprintf(fallback, sizeof fallback, "%s/wwtest", self_dir);
|
|
char **xargv = calloc((size_t)argc + 8, sizeof *xargv);
|
|
if (xargv == NULL) {
|
|
fputs("ww test: cannot allocate package coordinator arguments\n",
|
|
stderr);
|
|
return 1;
|
|
}
|
|
int n = 0, dotted = 0;
|
|
xargv[n++] = (char *)prog;
|
|
xargv[n++] = "package";
|
|
xargv[n++] = "--ww-driver";
|
|
xargv[n++] = (char *)self_path;
|
|
if (root_identity != NULL) {
|
|
xargv[n++] = "--ww-root-identity";
|
|
xargv[n++] = (char *)root_identity;
|
|
}
|
|
for (int i = 0; i < argc; i++) {
|
|
if (add_dot && !dotted && strcmp(argv[i], "--") == 0) {
|
|
xargv[n++] = ".";
|
|
dotted = 1;
|
|
}
|
|
xargv[n++] = (resolved && argv[i] == target)
|
|
? (char *)resolved : argv[i];
|
|
}
|
|
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);
|
|
free(xargv);
|
|
return 1;
|
|
}
|
|
|
|
/* Breaks cycles in `use` resolution. Linear because typical imports are
|
|
* a handful per build. */
|
|
struct ImportSet {
|
|
char **paths;
|
|
int n, cap;
|
|
};
|
|
|
|
#define SEP_LOCAL_IMPORT_PREFIX "__wwlocal"
|
|
|
|
static int
|
|
import_seen(struct ImportSet *s, const char *path)
|
|
{
|
|
for (int i = 0; i < s->n; i++)
|
|
if (strcmp(s->paths[i], path) == 0) return 1;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
import_add(struct ImportSet *s, const char *path)
|
|
{
|
|
if (s->n + 1 > s->cap) {
|
|
s->cap = s->cap ? s->cap * 2 : 8;
|
|
s->paths = realloc(s->paths, s->cap * sizeof *s->paths);
|
|
}
|
|
s->paths[s->n++] = strdup(path);
|
|
}
|
|
|
|
/* `encoding.utf8` → `encoding/utf8`. Mirrors Hare hare(1)'s
|
|
* use-path → fs-path mapping (ref/hare/hare/module/srcs.ha:78
|
|
* builds the same shape via path::push per ident part). */
|
|
static void
|
|
import_path_form(const char *name, char *out, size_t outsz)
|
|
{
|
|
size_t i;
|
|
for (i = 0; i + 1 < outsz && name[i] != '\0'; i++)
|
|
out[i] = (name[i] == '.') ? '/' : name[i];
|
|
out[i] = '\0';
|
|
}
|
|
|
|
static int
|
|
reserved_import_path(const char *name)
|
|
{
|
|
size_t n = strlen(SEP_LOCAL_IMPORT_PREFIX);
|
|
return strncmp(name, SEP_LOCAL_IMPORT_PREFIX, n) == 0
|
|
&& (name[n] == '\0' || name[n] == '.');
|
|
}
|
|
|
|
/* An import path names one directory package. There is deliberately no
|
|
* <dir>/<path>.ww branch here: literal or searched single-file roots are a
|
|
* CLI compatibility concern handled by locate_module, never an import edge. */
|
|
static int
|
|
locate_import_in(const char *dir, const char *path_form, char *out,
|
|
size_t outsz)
|
|
{
|
|
struct stat st;
|
|
snprintf(out, outsz, "%s/%s", dir, path_form);
|
|
if (stat(out, &st) == 0 && S_ISDIR(st.st_mode))
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
/* Walk every ordered root for <root>/<path>/ only. A decoy
|
|
* <earlier-root>/<path>.ww is neither a match nor a shadow: source imports
|
|
* always create canonical directory-package nodes. */
|
|
static int
|
|
locate_import(const char *dirs, const char *path_form, char *out,
|
|
size_t outsz)
|
|
{
|
|
const char *p = dirs;
|
|
while (*p) {
|
|
const char *e = strchr(p, ':');
|
|
size_t n = e ? (size_t)(e - p) : strlen(p);
|
|
if (n > 0 && n < outsz) {
|
|
char dir[1024];
|
|
if (n >= sizeof dir) n = sizeof dir - 1;
|
|
memcpy(dir, p, n);
|
|
dir[n] = '\0';
|
|
if (locate_import_in(dir, path_form, out, outsz))
|
|
return 1;
|
|
}
|
|
if (!e) break;
|
|
p = e + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* CLI target compatibility: directory packages still win globally, then a
|
|
* bare target may resolve to <root>/<path>.ww. This function is never used
|
|
* while loading a source import. */
|
|
static int
|
|
locate_module(const char *dirs, const char *path_form, char *out,
|
|
size_t outsz, int *is_dir)
|
|
{
|
|
if (locate_import(dirs, path_form, out, outsz)) {
|
|
*is_dir = 1;
|
|
return 1;
|
|
}
|
|
const char *p = dirs;
|
|
while (*p) {
|
|
const char *e = strchr(p, ':');
|
|
size_t n = e ? (size_t)(e - p) : strlen(p);
|
|
if (n > 0 && n < outsz) {
|
|
char dir[1024];
|
|
if (n >= sizeof dir) n = sizeof dir - 1;
|
|
memcpy(dir, p, n);
|
|
dir[n] = '\0';
|
|
snprintf(out, outsz, "%s/%s.ww", dir, path_form);
|
|
if (access(out, 0) == 0) {
|
|
*is_dir = 0;
|
|
return 1;
|
|
}
|
|
}
|
|
if (!e) break;
|
|
p = e + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Byte-wise total order is locale-independent; rule-10 byte-id requires
|
|
* the two stages sort the same way. strcmp diverges from Hare's memcmp
|
|
* (ref/hare/sort/cmp/cmp.ha:9); the order is identical for NUL-free
|
|
* filenames. */
|
|
static int
|
|
strs_cmp(const void *a, const void *b)
|
|
{
|
|
const char *sa = *(const char *const *)a;
|
|
const char *sb = *(const char *const *)b;
|
|
return strcmp(sa, sb);
|
|
}
|
|
|
|
static int sep_slurp(const char*, char**, u64*);
|
|
|
|
/* Go's contract: only *_test.ww is a test source. Ask the compiler parser,
|
|
* rather than a textual attribute scan, whether a production source contains
|
|
* @test; otherwise valid whitespace/comments could silently drop a test. */
|
|
static int
|
|
source_has_test_decl(const char *path)
|
|
{
|
|
char *buf;
|
|
u64 len;
|
|
if (sep_slurp(path, &buf, &len) < 0) return -1;
|
|
/* Keep directory-loader package-clause diagnostics stable. The full
|
|
* parser reports its language-level "missing package clause" first;
|
|
* the imports-only pass owns the package-loader wording and also avoids
|
|
* stage-specific recovery diagnostics for a malformed clause. */
|
|
Arena *ia = newarena();
|
|
Lex il;
|
|
Parser ip;
|
|
lexinit(&il, ia, path, buf, len);
|
|
parserinit(&ip, ia, &il);
|
|
Node *imports = parseimports(&ip);
|
|
if (il.errs || ip.errs) {
|
|
freearena(ia);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
if (imports->module == NULL) {
|
|
Pos pp = { path, 1, 1 };
|
|
errorf(pp, "invalid or missing package clause");
|
|
freearena(ia);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
freearena(ia);
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
lexinit(&l, a, path, buf, len);
|
|
parserinit(&p, a, &l);
|
|
Node *file = parsefile(&p);
|
|
if (l.errs || p.errs) {
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
int found = 0;
|
|
for (Node *d = file->list; d != NULL && !found; d = d->next)
|
|
if (d->kind == N_FNDECL)
|
|
for (Node *at = d->attr; at != NULL; at = at->next)
|
|
if (at->str != NULL && strcmp(at->str, "test") == 0) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
freearena(a);
|
|
free(buf);
|
|
return found;
|
|
}
|
|
|
|
#define SEP_VARIANT_PRODUCTION 0
|
|
#define SEP_VARIANT_SAME_TEST 1
|
|
#define SEP_VARIANT_EXTERNAL 2
|
|
#define SEP_VARIANT_TEST_MAIN 3
|
|
#define SEP_ROLE_NORMAL 0
|
|
#define SEP_ROLE_TEST_SUPPORT 1
|
|
#define SEP_ROLE_GENERATED_MAIN 2
|
|
#define SEP_TEST_SUPPORT_MODULE "__wwtest"
|
|
#define SEP_IMPORT_PATH_MAX 256
|
|
#define SEP_MAXPRODUCT 256
|
|
#define SEP_MAXCONTEXT (SEP_MAXPRODUCT + 1)
|
|
|
|
/* Test-file package classification uses the compiler's imports-only parser.
|
|
* The coordinator chooses variants, but the command owns which real source
|
|
* paths enter a package compilation. */
|
|
static int
|
|
source_package_name(const char *path, char *out, size_t outsz)
|
|
{
|
|
char *buf;
|
|
u64 len;
|
|
if (sep_slurp(path, &buf, &len) < 0) {
|
|
fprintf(stderr, "ww: cannot read %s\n", path);
|
|
return -1;
|
|
}
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
lexinit(&l, a, path, buf, len);
|
|
parserinit(&p, a, &l);
|
|
Node *imports = parseimports(&p);
|
|
if (l.errs || p.errs) {
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
if (imports->module == NULL) {
|
|
Pos pp = { path, 1, 1 };
|
|
errorf(pp, "invalid or missing package clause");
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
if (strlen(imports->module) >= outsz) {
|
|
errorf(imports->pos, "package name is too long");
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
snprintf(out, outsz, "%s", imports->module);
|
|
freearena(a);
|
|
free(buf);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
source_list_add(char ***list, int *n, int *cap, const char *path)
|
|
{
|
|
if (*n + 1 > *cap) {
|
|
int ncap = *cap ? *cap * 2 : 8;
|
|
char **next = realloc(*list, (size_t)ncap * sizeof *next);
|
|
if (next == NULL) return -1;
|
|
*list = next;
|
|
*cap = ncap;
|
|
}
|
|
char *copy = strdup(path);
|
|
if (copy == NULL) return -1;
|
|
(*list)[(*n)++] = copy;
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
source_list_free(char **list, int n)
|
|
{
|
|
for (int i = 0; i < n; i++) free(list[i]);
|
|
free(list);
|
|
}
|
|
|
|
/* Production packages select production files only. A same-package test root
|
|
* selects production files followed by matching same-package test files; an
|
|
* external root selects only matching external-test files. Each partition is
|
|
* byte-sorted so compiler test discovery is deterministic without generated
|
|
* package amalgamation. */
|
|
static int
|
|
enumerate_dir_ww(const char *dirpath, int variant, const char *test_package,
|
|
char ***out_files)
|
|
{
|
|
DIR *d = opendir(dirpath);
|
|
if (d == NULL) { *out_files = NULL; return -1; }
|
|
char **prod = NULL, **tests = NULL;
|
|
int nprod = 0, capprod = 0, ntests = 0, captests = 0;
|
|
struct dirent *ent;
|
|
while ((ent = readdir(d)) != NULL) {
|
|
const char *nm = ent->d_name;
|
|
size_t nl = strlen(nm);
|
|
if (nl <= 3) 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;
|
|
if (variant == SEP_VARIANT_EXTERNAL && !is_test) continue;
|
|
char path[2048];
|
|
snprintf(path, sizeof path, "%s/%s", dirpath, nm);
|
|
struct stat st;
|
|
if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
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;
|
|
}
|
|
int has_test = !is_test ? source_has_test_decl(path) : 0;
|
|
if (has_test < 0) {
|
|
source_list_free(prod, nprod);
|
|
source_list_free(tests, ntests);
|
|
closedir(d);
|
|
*out_files = NULL;
|
|
return -2;
|
|
}
|
|
if (has_test > 0) {
|
|
fprintf(stderr,
|
|
"ww: %s: @test declaration outside *_test.ww\n",
|
|
path);
|
|
source_list_free(prod, nprod);
|
|
source_list_free(tests, ntests);
|
|
closedir(d);
|
|
*out_files = NULL;
|
|
return -2;
|
|
}
|
|
if (is_test) {
|
|
char package[256];
|
|
if (source_package_name(path, package, sizeof package) < 0) {
|
|
source_list_free(prod, nprod);
|
|
source_list_free(tests, ntests);
|
|
closedir(d);
|
|
*out_files = NULL;
|
|
return -2;
|
|
}
|
|
if (test_package == NULL || strcmp(package, test_package) != 0)
|
|
continue;
|
|
}
|
|
char ***list = is_test ? &tests : ∏
|
|
int *n = is_test ? &ntests : &nprod;
|
|
int *cap = is_test ? &captests : &capprod;
|
|
if (source_list_add(list, n, cap, path) < 0) {
|
|
source_list_free(prod, nprod);
|
|
source_list_free(tests, ntests);
|
|
closedir(d);
|
|
*out_files = NULL;
|
|
return -2;
|
|
}
|
|
}
|
|
closedir(d);
|
|
if (nprod > 1) qsort(prod, (size_t)nprod, sizeof *prod, strs_cmp);
|
|
if (ntests > 1) qsort(tests, (size_t)ntests, sizeof *tests, strs_cmp);
|
|
int total = nprod + ntests;
|
|
char **all = total ? malloc((size_t)total * sizeof *all) : NULL;
|
|
if (total && all == NULL) {
|
|
source_list_free(prod, nprod);
|
|
source_list_free(tests, ntests);
|
|
*out_files = NULL;
|
|
return -2;
|
|
}
|
|
for (int i = 0; i < nprod; i++) all[i] = prod[i];
|
|
for (int i = 0; i < ntests; i++) all[nprod + i] = tests[i];
|
|
free(prod);
|
|
free(tests);
|
|
*out_files = all;
|
|
return total;
|
|
}
|
|
|
|
/* ww build — separate-compilation driver (task #46/c3).
|
|
*
|
|
* This is the SOLE build path (E3-C1 flip, task #87): the legacy
|
|
* single-file amalgamator is gone. Each imported
|
|
* package's `.wwi` interface is materialized and every package is
|
|
* compiled on its own (`w6c -c`), then the `.o` set is flat-linked.
|
|
*
|
|
* Each w6c pass is BOTH consumer (reads each direct dep `.wwi` through a
|
|
* separate --import argument) AND producer (writes this package's `.wwi`
|
|
* for its importers via -I). Reverse-topo order guarantees a package's
|
|
* deps' `.wwi` exist before it compiles.
|
|
*
|
|
* Only DIRECT dependency artifacts enter a compile action. The canonical
|
|
* import path paired with each `.wwi` preserves qualified symbol identity;
|
|
* a `.wwi` relocates the public foreign type/const facts required by its own
|
|
* API. Full transitive reachability remains a linker concern.
|
|
*/
|
|
#define SEP_MAXPKG 256
|
|
|
|
struct seppkg {
|
|
char path[512]; /* compiler/import identity; derived from import_base */
|
|
char import_base[SEP_IMPORT_PATH_MAX]; /* canonical directory import identity */
|
|
char entry[1024]; /* resolved package dir (or file, for a file root) */
|
|
char canon[1024]; /* canonical location; never package identity */
|
|
char artifact[512]; /* stable non-importable variant artifact key */
|
|
char name[256]; /* validated declared name; directory packages only */
|
|
char test_package[256]; /* selected test package; root variants only */
|
|
char **sources; /* owned, byte-sorted selected paths; dirs only */
|
|
int nsources;
|
|
int is_dir;
|
|
int variant; /* SEP_VARIANT_*; dependencies are production */
|
|
int role; /* normal, reserved test support, or generated main */
|
|
int root; /* requested usage; never package-action identity */
|
|
int link_entry; /* package supplies the executable's bare main */
|
|
int generated_main; /* compiler-owned generated test-main package */
|
|
int failed; /* discovery/compile failure reaches this action */
|
|
int test_support; /* compiler-generated -T support package */
|
|
int loaded; /* directory membership/name loaded exactly once */
|
|
int export_changed; /* staged export differs from committed export */
|
|
int emit_context; /* first verified resolution context */
|
|
unsigned char context_state[SEP_MAXCONTEXT]; /* 0 new, 1 active, 2 checked */
|
|
struct ImportSet bindings; /* first context's canonical import bindings */
|
|
int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */
|
|
int ndeps;
|
|
int color; /* tri-color DFS: 0 white, 1 gray, 2 black */
|
|
};
|
|
|
|
struct sepcontext {
|
|
char root[1024]; /* selected entry directory; diagnostic identity */
|
|
char searchpath[8192]; /* root : explicit -I roots : toolchain source */
|
|
};
|
|
|
|
struct sepgraph {
|
|
struct seppkg pkg[SEP_MAXPKG];
|
|
int n;
|
|
struct sepcontext context[SEP_MAXCONTEXT];
|
|
int ncontext;
|
|
int support_context;
|
|
int identity_failed; /* command-global canonical identity collision */
|
|
};
|
|
|
|
struct sepproduct {
|
|
const char *dir;
|
|
const char *out;
|
|
const char *identity; /* explicit canonical lookup identity, if any */
|
|
const char *test_package;
|
|
const char *status;
|
|
char artifact[64];
|
|
int variant;
|
|
int context;
|
|
int root;
|
|
int variant_root; /* production-plus-test or external test package */
|
|
};
|
|
|
|
#define SEP_MAXLFLAGS 32
|
|
#define SEP_ARTIFACT_MAX 1024
|
|
struct seplinkflags {
|
|
const char *libdirs[SEP_MAXLFLAGS];
|
|
int nlibdirs;
|
|
const char *libs[SEP_MAXLFLAGS];
|
|
int nlibs;
|
|
};
|
|
|
|
static int
|
|
sep_directory_variant(int variant)
|
|
{
|
|
return variant == SEP_VARIANT_PRODUCTION
|
|
|| variant == SEP_VARIANT_SAME_TEST
|
|
|| variant == SEP_VARIANT_EXTERNAL;
|
|
}
|
|
|
|
static int
|
|
sep_variant_path(int variant, const char *base, char *out, size_t outsz)
|
|
{
|
|
int n;
|
|
if (variant == SEP_VARIANT_EXTERNAL)
|
|
n = snprintf(out, outsz, "%s_test", base);
|
|
else
|
|
n = snprintf(out, outsz, "%s", base);
|
|
return n >= 0 && (size_t)n < outsz ? 0 : -1;
|
|
}
|
|
|
|
static void
|
|
sep_diag_path_locations(const char *path, const char *a, const char *b)
|
|
{
|
|
if (strcmp(a, b) > 0) { const char *t = a; a = b; b = t; }
|
|
fprintf(stderr, "ww: package %s resolves to directories %s and %s\n",
|
|
path, a, b);
|
|
}
|
|
|
|
static void
|
|
sep_diag_directory_identities(const char *entry, const char *a, const char *b)
|
|
{
|
|
if (strcmp(a, b) > 0) { const char *t = a; a = b; b = t; }
|
|
fprintf(stderr, "ww: package directory %s has import identities %s and %s\n",
|
|
entry, a, b);
|
|
}
|
|
|
|
static int sep_import_component(const char *, size_t);
|
|
|
|
static int
|
|
sep_import_base_valid(const char *path)
|
|
{
|
|
const char *p = path;
|
|
while (*p != '\0') {
|
|
const char *dot = strchr(p, '.');
|
|
size_t n = dot != NULL ? (size_t)(dot - p) : strlen(p);
|
|
if (!sep_import_component(p, n)) return 0;
|
|
if (dot == NULL) return 1;
|
|
p = dot + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Bind the canonical ordinary import identity of one provisional directory
|
|
* action. The action's compiler path is derived from that base and its semantic
|
|
* variant; neither requested-root state nor artifact naming participates. */
|
|
static int
|
|
sep_bind_import_base(struct sepgraph *g, int pi, const char *base)
|
|
{
|
|
struct seppkg *p = &g->pkg[pi];
|
|
if (base == NULL || base[0] == '\0'
|
|
|| strlen(base) >= SEP_IMPORT_PATH_MAX) {
|
|
fprintf(stderr, "ww: package path is too long (limit %d bytes)\n",
|
|
SEP_IMPORT_PATH_MAX - 1);
|
|
return -1;
|
|
}
|
|
if (!reserved_import_path(base) && !sep_import_base_valid(base)) {
|
|
fprintf(stderr, "ww: invalid package path %s\n", base);
|
|
return -1;
|
|
}
|
|
if (p->import_base[0] != '\0') {
|
|
if (strcmp(p->import_base, base) == 0) return 0;
|
|
g->identity_failed = 1;
|
|
sep_diag_directory_identities(p->entry, p->import_base, base);
|
|
return -1;
|
|
}
|
|
char path[sizeof p->path];
|
|
if (sep_variant_path(p->variant, base, path, sizeof path) < 0) {
|
|
fprintf(stderr, "ww: package variant path is too long\n");
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < g->n; i++) {
|
|
if (i == pi || !g->pkg[i].is_dir || g->pkg[i].generated_main)
|
|
continue;
|
|
int same_location = strcmp(g->pkg[i].canon, p->canon) == 0;
|
|
int support_alias = p->role == SEP_ROLE_TEST_SUPPORT
|
|
|| g->pkg[i].role == SEP_ROLE_TEST_SUPPORT;
|
|
if (!support_alias && !same_location
|
|
&& g->pkg[i].import_base[0] != '\0'
|
|
&& strcmp(g->pkg[i].import_base, base) == 0) {
|
|
g->identity_failed = 1;
|
|
sep_diag_path_locations(base, g->pkg[i].entry, p->entry);
|
|
return -1;
|
|
}
|
|
if (same_location && !support_alias
|
|
&& g->pkg[i].import_base[0] != '\0'
|
|
&& strcmp(g->pkg[i].import_base, base) != 0) {
|
|
g->identity_failed = 1;
|
|
sep_diag_directory_identities(p->entry,
|
|
g->pkg[i].import_base, base);
|
|
return -1;
|
|
}
|
|
if (g->pkg[i].path[0] != '\0'
|
|
&& strcmp(g->pkg[i].path, path) == 0) {
|
|
if (!same_location) {
|
|
g->identity_failed = 1;
|
|
sep_diag_path_locations(path, g->pkg[i].entry, p->entry);
|
|
return -1;
|
|
}
|
|
if (support_alias
|
|
|| g->pkg[i].variant == p->variant) {
|
|
g->identity_failed = 1;
|
|
fprintf(stderr,
|
|
"ww: package action identity collision for %s in %s\n",
|
|
path, p->entry);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
snprintf(p->import_base, sizeof p->import_base, "%s", base);
|
|
snprintf(p->path, sizeof p->path, "%s", path);
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
sep_find_or_add_variant(struct sepgraph *g, const char *path,
|
|
const char *entry, int is_dir, int variant, const char *test_package,
|
|
int role, const char *artifact, int root)
|
|
{
|
|
if (path != NULL && strlen(path) >= SEP_IMPORT_PATH_MAX) {
|
|
fprintf(stderr, "ww: package path is too long (limit %d bytes)\n",
|
|
SEP_IMPORT_PATH_MAX - 1);
|
|
return -1;
|
|
}
|
|
char *canon = realpath(entry, NULL);
|
|
if (canon == NULL) {
|
|
fprintf(stderr, "ww: cannot canonicalize package %s\n", entry);
|
|
return -1;
|
|
}
|
|
if (strlen(canon) >= sizeof g->pkg[0].canon) {
|
|
fprintf(stderr, "ww: canonical package path is too long\n");
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
const char *base = path ? path : "";
|
|
char incoming_path[sizeof g->pkg[0].path];
|
|
incoming_path[0] = '\0';
|
|
if (is_dir && base[0] != '\0'
|
|
&& sep_variant_path(variant, base, incoming_path,
|
|
sizeof incoming_path) < 0) {
|
|
fprintf(stderr, "ww: package variant path is too long\n");
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < g->n; i++) {
|
|
struct seppkg *q = &g->pkg[i];
|
|
int same_location = strcmp(q->canon, canon) == 0;
|
|
if (is_dir && q->is_dir && !q->generated_main) {
|
|
int support_alias = role == SEP_ROLE_TEST_SUPPORT
|
|
|| q->role == SEP_ROLE_TEST_SUPPORT;
|
|
if (!support_alias && !same_location && base[0] != '\0'
|
|
&& q->import_base[0] != '\0'
|
|
&& strcmp(base, q->import_base) == 0) {
|
|
g->identity_failed = 1;
|
|
sep_diag_path_locations(base, q->entry, entry);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
if (incoming_path[0] != '\0' && q->path[0] != '\0'
|
|
&& strcmp(incoming_path, q->path) == 0
|
|
&& !same_location) {
|
|
g->identity_failed = 1;
|
|
sep_diag_path_locations(incoming_path, q->entry, entry);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
if (same_location && q->variant == variant && q->role == role) {
|
|
if (variant != SEP_VARIANT_PRODUCTION
|
|
&& strcmp(q->test_package,
|
|
test_package ? test_package : "") != 0) {
|
|
fprintf(stderr,
|
|
"ww: incompatible package-test roots %s\n", entry);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
if (base[0] != '\0'
|
|
&& sep_bind_import_base(g, i, base) < 0) {
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
q->root = q->root || root;
|
|
free(canon);
|
|
return i;
|
|
}
|
|
if (same_location) {
|
|
if (support_alias) continue;
|
|
if (q->import_base[0] != '\0' && base[0] != '\0'
|
|
&& strcmp(q->import_base, base) != 0) {
|
|
g->identity_failed = 1;
|
|
sep_diag_directory_identities(entry,
|
|
q->import_base, base);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
if (sep_directory_variant(q->variant)
|
|
&& sep_directory_variant(variant))
|
|
continue;
|
|
fprintf(stderr,
|
|
"ww: package directory %s has incompatible variants\n",
|
|
entry);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
continue;
|
|
}
|
|
if (same_location && strcmp(q->path, base) == 0
|
|
&& q->variant == variant && q->role == role) {
|
|
q->root = q->root || root;
|
|
free(canon);
|
|
return i;
|
|
}
|
|
}
|
|
if (g->n >= SEP_MAXPKG) {
|
|
fprintf(stderr, "ww: too many packages (limit %d)\n",
|
|
SEP_MAXPKG);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
int ni = g->n++;
|
|
struct seppkg *p = &g->pkg[ni];
|
|
memset(p, 0, sizeof *p);
|
|
snprintf(p->entry, sizeof p->entry, "%s", entry);
|
|
snprintf(p->canon, sizeof p->canon, "%s", canon);
|
|
free(canon);
|
|
p->is_dir = is_dir;
|
|
p->variant = variant;
|
|
p->role = role;
|
|
p->root = root;
|
|
p->emit_context = -1;
|
|
if (test_package != NULL)
|
|
snprintf(p->test_package, sizeof p->test_package, "%s",
|
|
test_package);
|
|
if (is_dir) {
|
|
const char *inherited = base;
|
|
if (inherited[0] == '\0')
|
|
for (int i = 0; i < ni; i++)
|
|
if (g->pkg[i].is_dir && !g->pkg[i].generated_main
|
|
&& g->pkg[i].role != SEP_ROLE_TEST_SUPPORT
|
|
&& role != SEP_ROLE_TEST_SUPPORT
|
|
&& strcmp(g->pkg[i].canon, p->canon) == 0
|
|
&& g->pkg[i].import_base[0] != '\0') {
|
|
inherited = g->pkg[i].import_base;
|
|
break;
|
|
}
|
|
if (inherited[0] != '\0'
|
|
&& sep_bind_import_base(g, ni, inherited) < 0) {
|
|
g->n--;
|
|
return -1;
|
|
}
|
|
} else {
|
|
snprintf(p->path, sizeof p->path, "%s", base);
|
|
if (artifact != NULL)
|
|
snprintf(p->artifact, sizeof p->artifact, "%s", artifact);
|
|
}
|
|
return ni;
|
|
}
|
|
|
|
static int
|
|
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
|
|
int is_dir)
|
|
{
|
|
return sep_find_or_add_variant(g, path, entry, is_dir,
|
|
SEP_VARIANT_PRODUCTION, NULL, SEP_ROLE_NORMAL, NULL, 0);
|
|
}
|
|
|
|
static int
|
|
sep_find_or_add_role(struct sepgraph *g, const char *path, const char *entry,
|
|
int is_dir, int role, const char *artifact)
|
|
{
|
|
return sep_find_or_add_variant(g, path, entry, is_dir,
|
|
SEP_VARIANT_PRODUCTION, NULL, role, artifact, 0);
|
|
}
|
|
|
|
/* Release the one package-owned directory-membership list. Every graph exit
|
|
* funnels through this function; regular-file nodes own no source list. */
|
|
static void
|
|
sep_graph_free(struct sepgraph *g)
|
|
{
|
|
if (g == NULL) return;
|
|
for (int i = 0; i < g->n; i++) {
|
|
for (int j = 0; j < g->pkg[i].nsources; j++)
|
|
free(g->pkg[i].sources[j]);
|
|
free(g->pkg[i].sources);
|
|
for (int j = 0; j < g->pkg[i].bindings.n; j++)
|
|
free(g->pkg[i].bindings.paths[j]);
|
|
free(g->pkg[i].bindings.paths);
|
|
}
|
|
free(g);
|
|
}
|
|
|
|
/* One selected directory owns one import-resolution context. Same/external
|
|
* variants of that directory share it; different directories never acquire
|
|
* precedence merely from their request order. */
|
|
static int
|
|
sep_context_for(struct sepgraph *g, const char *root,
|
|
const char *extra_includes, const char *toolsrcdir)
|
|
{
|
|
char searchpath[8192];
|
|
int n;
|
|
if (extra_includes != NULL && extra_includes[0] != '\0')
|
|
n = snprintf(searchpath, sizeof searchpath, "%s:%s:%s",
|
|
root, extra_includes, toolsrcdir);
|
|
else
|
|
n = snprintf(searchpath, sizeof searchpath, "%s:%s",
|
|
root, toolsrcdir);
|
|
if (n < 0 || (size_t)n >= sizeof searchpath) {
|
|
fprintf(stderr, "ww: package import search path is too long\n");
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < g->ncontext; i++)
|
|
if (strcmp(g->context[i].searchpath, searchpath) == 0)
|
|
return i;
|
|
if (g->ncontext >= SEP_MAXCONTEXT) {
|
|
fprintf(stderr, "ww: too many package import contexts\n");
|
|
return -1;
|
|
}
|
|
struct sepcontext *c = &g->context[g->ncontext];
|
|
if (snprintf(c->root, sizeof c->root, "%s", root)
|
|
>= (int)sizeof c->root) {
|
|
fprintf(stderr, "ww: package root path is too long\n");
|
|
return -1;
|
|
}
|
|
snprintf(c->searchpath, sizeof c->searchpath, "%s", searchpath);
|
|
return g->ncontext++;
|
|
}
|
|
|
|
/* Dots stay (legal in filenames). Product roots have distinct artifact names
|
|
* even though each compiler unit resets to the bare executable namespace. */
|
|
static void
|
|
sep_fname(const struct sepgraph *g, int pi, const char *scratch,
|
|
const char *suffix, char *out, size_t outsz)
|
|
{
|
|
const char *base = g->pkg[pi].path;
|
|
if (g->pkg[pi].artifact[0] != '\0') {
|
|
base = g->pkg[pi].artifact;
|
|
} else if (g->pkg[pi].root) {
|
|
if (base[0] == '\0') base = "__root";
|
|
} else if (base[0] == '\0') {
|
|
base = "__root";
|
|
}
|
|
snprintf(out, outsz, "%s/%s%s", scratch, base, suffix);
|
|
}
|
|
|
|
/* All later artifact construction uses fixed SEP_ARTIFACT_MAX buffers. Check
|
|
* the longest suffix once, before any file is opened, so truncation can never
|
|
* collapse two command-owned action identities onto one path. */
|
|
static int
|
|
sep_validate_artifact_paths(const struct sepgraph *g, const char *scratch)
|
|
{
|
|
for (int i = 0; i < g->n; i++) {
|
|
if (g->pkg[i].failed || !g->pkg[i].loaded) continue;
|
|
const char *base = g->pkg[i].artifact[0] != '\0'
|
|
? g->pkg[i].artifact
|
|
: (g->pkg[i].path[0] != '\0' ? g->pkg[i].path : "__root");
|
|
size_t need = strlen(scratch) + 1 + strlen(base)
|
|
+ strlen(".unit.new") + 1;
|
|
if (need > SEP_ARTIFACT_MAX) {
|
|
fprintf(stderr, "ww: package artifact path is too long\n");
|
|
return -1;
|
|
}
|
|
for (int j = i + 1; j < g->n; j++) {
|
|
if (g->pkg[j].failed || !g->pkg[j].loaded) continue;
|
|
const char *other = g->pkg[j].artifact[0] != '\0'
|
|
? g->pkg[j].artifact
|
|
: (g->pkg[j].path[0] != '\0'
|
|
? g->pkg[j].path : "__root");
|
|
if (strcmp(base, other) == 0) {
|
|
fprintf(stderr,
|
|
"ww: package actions share artifact identity %s\n",
|
|
base);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
sep_slurp(const char *path, char **out, u64 *len)
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
if (f == NULL) return -1;
|
|
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; }
|
|
long n = ftell(f);
|
|
if (n < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
char *buf = malloc((size_t)n + 1);
|
|
if (buf == NULL) { fclose(f); return -1; }
|
|
if (fread(buf, 1, (size_t)n, f) != (size_t)n) {
|
|
free(buf);
|
|
fclose(f);
|
|
return -1;
|
|
}
|
|
buf[n] = '\0';
|
|
if (fclose(f) != 0) {
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
*out = buf;
|
|
*len = (u64)n;
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
use_node_cmp(const void *a, const void *b)
|
|
{
|
|
const Node *x = *(Node *const *)a;
|
|
const Node *y = *(Node *const *)b;
|
|
const char *xp = x->usepath ? x->usepath : x->str;
|
|
const char *yp = y->usepath ? y->usepath : y->str;
|
|
int r = strcmp(xp, yp);
|
|
if (r != 0) return r;
|
|
if (x->pos.line != y->pos.line) return x->pos.line - y->pos.line;
|
|
return x->pos.col - y->pos.col;
|
|
}
|
|
|
|
static int
|
|
sep_external_production_name(const struct seppkg *pkg, const char *path,
|
|
int leaf_only)
|
|
{
|
|
if (pkg->variant != SEP_VARIANT_EXTERNAL
|
|
|| pkg->test_package[0] == '\0')
|
|
return 0;
|
|
const char *name = path;
|
|
if (leaf_only) {
|
|
const char *dot = strrchr(path, '.');
|
|
if (dot != NULL) name = dot + 1;
|
|
}
|
|
size_t n = strlen(name);
|
|
size_t tn = strlen(pkg->test_package);
|
|
return tn == n + 5 && strncmp(pkg->test_package, name, n) == 0
|
|
&& strcmp(pkg->test_package + n, "_test") == 0;
|
|
}
|
|
|
|
/* Canonical bindings make a shared package independent of which selected
|
|
* root reaches it first. Directory bindings and the raw-file inline
|
|
* compatibility binding are part of the package action's source meaning. */
|
|
static int
|
|
sep_binding_add(struct ImportSet *bindings, char kind, const char *name,
|
|
const char *target)
|
|
{
|
|
size_t nn = strlen(name), tn = target ? strlen(target) : 0;
|
|
char *binding = malloc(nn + tn + 4);
|
|
if (binding == NULL) return -1;
|
|
binding[0] = kind;
|
|
binding[1] = ':';
|
|
memcpy(binding + 2, name, nn);
|
|
binding[2 + nn] = ':';
|
|
if (target != NULL) memcpy(binding + 3 + nn, target, tn);
|
|
binding[3 + nn + tn] = '\0';
|
|
if (import_seen(bindings, binding)) {
|
|
free(binding);
|
|
return 0;
|
|
}
|
|
if (bindings->n == bindings->cap) {
|
|
int cap = bindings->cap ? bindings->cap * 2 : 8;
|
|
char **paths = realloc(bindings->paths,
|
|
(size_t)cap * sizeof *paths);
|
|
if (paths == NULL) { free(binding); return -1; }
|
|
bindings->paths = paths;
|
|
bindings->cap = cap;
|
|
}
|
|
bindings->paths[bindings->n++] = binding;
|
|
return 0;
|
|
}
|
|
|
|
/* A DIRECTORY import is a package boundary: add it as a direct dep of pkg
|
|
* `pi`. A FILE import is an intra-package split — fold its imports into
|
|
* `pi` (its bytes join pi's body at emit time). Collects package PATHS
|
|
* rather than concatenating bytes the way the legacy amalgamator did
|
|
* (§1.1). */
|
|
static int
|
|
sep_scan_file(struct sepgraph *g, int pi, const char *file,
|
|
const char *searchpath, struct ImportSet *filevisit,
|
|
struct ImportSet *bindings, int owned_source)
|
|
{
|
|
if (import_seen(filevisit, file)) return 0;
|
|
import_add(filevisit, file);
|
|
char *buf;
|
|
u64 len;
|
|
if (sep_slurp(file, &buf, &len) < 0) {
|
|
fprintf(stderr, "ww: cannot read %s\n", file);
|
|
return -1;
|
|
}
|
|
Arena *a = newarena();
|
|
Lex l;
|
|
Parser p;
|
|
lexinit(&l, a, file, buf, len);
|
|
parserinit(&p, a, &l);
|
|
Node *imports = parseimports(&p);
|
|
if (l.errs || p.errs) {
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
if (imports->module == NULL && owned_source) {
|
|
Pos pp = { file, 1, 1 };
|
|
errorf(pp, "invalid or missing package clause");
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
|
|
if (imports->module != NULL
|
|
&& (owned_source || g->pkg[pi].name[0] == '\0')) {
|
|
const char *declared = imports->module;
|
|
if (strlen(declared) >= sizeof g->pkg[pi].name) {
|
|
errorf(imports->pos, "package name is too long");
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
struct seppkg *pkg = &g->pkg[pi];
|
|
if (pkg->name[0] == '\0')
|
|
snprintf(pkg->name, sizeof pkg->name, "%s", declared);
|
|
else if (strcmp(pkg->name, declared) != 0) {
|
|
errorf(imports->pos,
|
|
"conflicting package names %s and %s in %s",
|
|
pkg->name, declared, pkg->entry);
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
}
|
|
if (owned_source && g->pkg[pi].is_dir) {
|
|
for (Node *package = imports->body; package; package = package->next) {
|
|
if (strcmp(package->module, g->pkg[pi].name) != 0) {
|
|
errorf(package->pos,
|
|
"conflicting package names %s and %s in %s",
|
|
g->pkg[pi].name, package->module, g->pkg[pi].entry);
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
int nuse = 0;
|
|
for (Node *u = imports->list; u; u = u->next)
|
|
if (u->kind == N_USE) nuse++;
|
|
Node **uses = nuse ? malloc((size_t)nuse * sizeof *uses) : NULL;
|
|
if (nuse && uses == NULL) {
|
|
freearena(a);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
int ui = 0;
|
|
for (Node *u = imports->list; u; u = u->next)
|
|
if (u->kind == N_USE) uses[ui++] = u;
|
|
if (nuse > 1) qsort(uses, (size_t)nuse, sizeof *uses, use_node_cmp);
|
|
|
|
int rc = 0;
|
|
const char *previous = NULL;
|
|
for (int i = 0; i < nuse && rc == 0; i++) {
|
|
Node *u = uses[i];
|
|
const char *name = u->usepath ? u->usepath : u->str;
|
|
if (previous && strcmp(previous, name) == 0) continue;
|
|
previous = name;
|
|
if (reserved_import_path(name)) {
|
|
errorf(u->pos, "package path %s is reserved", name);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
if (strlen(name) >= sizeof g->pkg[0].path) {
|
|
errorf(u->pos, "import path is too long (limit %zu bytes)",
|
|
sizeof g->pkg[0].path - 1);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
char path_form[1024];
|
|
import_path_form(name, path_form, sizeof path_form);
|
|
if (strlen(name) + 1 > sizeof path_form) {
|
|
errorf(u->pos, "import path is too long");
|
|
rc = -1;
|
|
break;
|
|
}
|
|
char ipath[1024];
|
|
int external_production = 0;
|
|
/* A selected external logical root is already an exact resolved
|
|
* path/directory pair. Its source import of that same full ordinary
|
|
* identity reuses the colocated production action. No declaration leaf
|
|
* or unrelated cached package can override normal context lookup. */
|
|
const char *bound = g->pkg[pi].variant == SEP_VARIANT_EXTERNAL
|
|
&& g->pkg[pi].import_base[0] != '\0'
|
|
&& strcmp(g->pkg[pi].import_base, name) == 0
|
|
? g->pkg[pi].canon : NULL;
|
|
int located = bound != NULL;
|
|
if (located)
|
|
snprintf(ipath, sizeof ipath, "%s", bound);
|
|
else
|
|
located = locate_import(searchpath, path_form, ipath,
|
|
sizeof ipath);
|
|
if (!located) {
|
|
const char *dot = strrchr(name, '.');
|
|
const char *leaf = dot ? dot + 1 : name;
|
|
int inline_package = 0;
|
|
if (!g->pkg[pi].is_dir)
|
|
for (Node *package = imports->body; package;
|
|
package = package->next)
|
|
if (strcmp(package->module, leaf) == 0) {
|
|
inline_package = 1;
|
|
break;
|
|
}
|
|
if (inline_package) {
|
|
if (sep_binding_add(bindings, 'I', name, NULL) < 0)
|
|
rc = -1;
|
|
continue;
|
|
}
|
|
errorf(u->pos, "cannot find package %s", name);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
{
|
|
char *canon = realpath(ipath, NULL);
|
|
if (canon == NULL) {
|
|
errorf(u->pos, "cannot canonicalize package '%s'", name);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
if (sep_binding_add(bindings, 'D', name, canon) < 0) {
|
|
free(canon);
|
|
rc = -1;
|
|
break;
|
|
}
|
|
int self = strcmp(canon, g->pkg[pi].canon) == 0;
|
|
free(canon);
|
|
if (self && sep_external_production_name(&g->pkg[pi], name, 1))
|
|
external_production = 1;
|
|
if (self && !external_production) {
|
|
const char *owner = g->pkg[pi].path[0]
|
|
? g->pkg[pi].path : g->pkg[pi].canon;
|
|
errorf(u->pos, "self-import: package '%s' cannot import itself",
|
|
owner[0] ? owner : "(root)");
|
|
rc = -1;
|
|
break;
|
|
}
|
|
/* The external package's self-production import is the ordinary
|
|
* canonical production action for this directory. Discovery role and
|
|
* the external product's artifact name never create another action. */
|
|
int di = sep_find_or_add(g, name, ipath, 1);
|
|
if (di < 0) { rc = -1; break; }
|
|
int seen = 0;
|
|
for (int k = 0; k < g->pkg[pi].ndeps; k++)
|
|
if (g->pkg[pi].deps[k] == di) { seen = 1; break; }
|
|
if (!seen) {
|
|
if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; }
|
|
g->pkg[pi].deps[g->pkg[pi].ndeps++] = di;
|
|
}
|
|
}
|
|
}
|
|
free(uses);
|
|
freearena(a);
|
|
free(buf);
|
|
return rc;
|
|
}
|
|
|
|
static void
|
|
sep_import_set_free(struct ImportSet *s)
|
|
{
|
|
for (int i = 0; i < s->n; i++) free(s->paths[i]);
|
|
free(s->paths);
|
|
s->paths = NULL;
|
|
s->n = s->cap = 0;
|
|
}
|
|
|
|
static int
|
|
sep_dep_cmp(const struct sepgraph *g, int a, int b)
|
|
{
|
|
int r = strcmp(g->pkg[a].path, g->pkg[b].path);
|
|
if (r != 0) return r;
|
|
if (g->pkg[a].variant != g->pkg[b].variant)
|
|
return g->pkg[a].variant - g->pkg[b].variant;
|
|
if (g->pkg[a].role != g->pkg[b].role)
|
|
return g->pkg[a].role - g->pkg[b].role;
|
|
return strcmp(g->pkg[a].canon, g->pkg[b].canon);
|
|
}
|
|
|
|
/* Add the compiler-owned test main as a real package action. Its identity is a
|
|
* pure function of the selected variant (and its one command-global support
|
|
* edge), never a product ordinal. Equivalent products therefore reuse it. */
|
|
static int
|
|
sep_add_generated_main(struct sepgraph *g, struct sepproduct *product,
|
|
int ordinal, int support)
|
|
{
|
|
(void)ordinal;
|
|
int variant = product->variant_root;
|
|
if (variant < 0 || variant >= g->n) return -1;
|
|
const char *kind = "production";
|
|
if (g->pkg[variant].variant == SEP_VARIANT_SAME_TEST)
|
|
kind = "internal";
|
|
else if (g->pkg[variant].variant == SEP_VARIANT_EXTERNAL)
|
|
kind = "external";
|
|
char path[sizeof g->pkg[0].path];
|
|
char artifact[sizeof g->pkg[0].artifact];
|
|
char canon[sizeof g->pkg[0].canon];
|
|
char entry[sizeof g->pkg[0].entry];
|
|
snprintf(entry, sizeof entry, "%s", g->pkg[variant].entry);
|
|
const char *variant_artifact = g->pkg[variant].artifact[0]
|
|
? g->pkg[variant].artifact : g->pkg[variant].path;
|
|
int pn = snprintf(path, sizeof path, "__wwtestmain.%s.%s.main",
|
|
g->pkg[variant].path, kind);
|
|
int an = snprintf(artifact, sizeof artifact, "%s-main", variant_artifact);
|
|
int cn = snprintf(canon, sizeof canon, "%s#%s-test-main",
|
|
g->pkg[variant].canon, kind);
|
|
if (pn < 0 || (size_t)pn >= sizeof path
|
|
|| an < 0 || (size_t)an >= sizeof artifact
|
|
|| cn < 0 || (size_t)cn >= sizeof canon) {
|
|
fprintf(stderr, "ww: generated test-main identity is too long\n");
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < g->n; i++) {
|
|
if (strcmp(g->pkg[i].path, path) != 0) continue;
|
|
if (g->pkg[i].generated_main) {
|
|
int wants_support = support >= 0 && support != variant;
|
|
int has_variant = 0, has_support = 0;
|
|
for (int k = 0; k < g->pkg[i].ndeps; k++) {
|
|
if (g->pkg[i].deps[k] == variant) has_variant = 1;
|
|
if (wants_support && g->pkg[i].deps[k] == support)
|
|
has_support = 1;
|
|
}
|
|
if (has_variant && has_support == wants_support
|
|
&& g->pkg[i].ndeps == 1 + wants_support)
|
|
return i;
|
|
}
|
|
fprintf(stderr,
|
|
"ww: generated test-main package identity collides with source import %s\n",
|
|
path);
|
|
return -1;
|
|
}
|
|
if (g->n >= SEP_MAXPKG) {
|
|
fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG);
|
|
return -1;
|
|
}
|
|
struct seppkg *p = &g->pkg[g->n];
|
|
memset(p, 0, sizeof *p);
|
|
snprintf(p->path, sizeof p->path, "%s", path);
|
|
snprintf(p->artifact, sizeof p->artifact, "%s", artifact);
|
|
snprintf(p->canon, sizeof p->canon, "%s", canon);
|
|
snprintf(p->entry, sizeof p->entry, "%s", entry);
|
|
snprintf(p->name, sizeof p->name, "main");
|
|
p->variant = SEP_VARIANT_TEST_MAIN;
|
|
p->role = SEP_ROLE_GENERATED_MAIN;
|
|
p->root = 1;
|
|
p->link_entry = 1;
|
|
p->generated_main = 1;
|
|
p->loaded = 1;
|
|
p->emit_context = product->context;
|
|
p->context_state[product->context] = 2;
|
|
p->deps[p->ndeps++] = variant;
|
|
if (support >= 0 && support != variant)
|
|
p->deps[p->ndeps++] = support;
|
|
for (int i = 1; i < p->ndeps; i++) {
|
|
int v = p->deps[i];
|
|
int j = i;
|
|
while (j > 0 && sep_dep_cmp(g, p->deps[j - 1], v) > 0) {
|
|
p->deps[j] = p->deps[j - 1];
|
|
j--;
|
|
}
|
|
p->deps[j] = v;
|
|
}
|
|
return g->n++;
|
|
}
|
|
|
|
/* Load one canonical package under one selected-root resolution context.
|
|
* Source membership is owned once, but a shared package's imports are checked
|
|
* under every context that reaches it. The first canonical binding set owns
|
|
* file-body composition; every later set must be identical. */
|
|
static int
|
|
sep_load_pkg(struct sepgraph *g, int pi, int context)
|
|
{
|
|
if (context < 0 || context >= g->ncontext) return -1;
|
|
if (g->pkg[pi].test_support
|
|
&& g->support_context >= 0)
|
|
context = g->support_context;
|
|
if (g->pkg[pi].context_state[context] == 2)
|
|
return g->pkg[pi].failed ? -1 : 0;
|
|
if (g->pkg[pi].context_state[context] == 1)
|
|
return 0;
|
|
g->pkg[pi].context_state[context] = 1;
|
|
const char *searchpath = g->context[context].searchpath;
|
|
struct ImportSet fv = {0}, bindings = {0};
|
|
int rc = 0;
|
|
if (!g->pkg[pi].loaded) {
|
|
g->pkg[pi].loaded = 1;
|
|
if (g->pkg[pi].is_dir) {
|
|
const char *test_package = g->pkg[pi].test_package[0]
|
|
? g->pkg[pi].test_package : NULL;
|
|
g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry,
|
|
g->pkg[pi].variant, test_package,
|
|
&g->pkg[pi].sources);
|
|
if (g->pkg[pi].nsources == -2) {
|
|
rc = -1; /* diagnosed in enumerate_dir_ww */
|
|
} else if (g->pkg[pi].nsources < 0) {
|
|
fprintf(stderr, "ww: cannot read directory %s\n",
|
|
g->pkg[pi].entry);
|
|
rc = -1;
|
|
} else if (g->pkg[pi].nsources == 0) {
|
|
fprintf(stderr,
|
|
"ww: %s: directory contains no WW package sources\n",
|
|
g->pkg[pi].entry);
|
|
rc = -1;
|
|
}
|
|
}
|
|
}
|
|
if (g->pkg[pi].is_dir) {
|
|
for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++)
|
|
rc = sep_scan_file(g, pi, g->pkg[pi].sources[i],
|
|
searchpath, &fv, &bindings, 1);
|
|
if (rc == 0 && g->pkg[pi].path[0] != '\0'
|
|
&& !g->pkg[pi].test_support) {
|
|
const char *dot = strrchr(g->pkg[pi].path, '.');
|
|
const char *leaf = dot ? dot + 1 : g->pkg[pi].path;
|
|
if (strcmp(g->pkg[pi].name, leaf) != 0) {
|
|
fprintf(stderr,
|
|
"ww: package %s does not match import path %s\n",
|
|
g->pkg[pi].name, g->pkg[pi].path);
|
|
rc = -1;
|
|
}
|
|
}
|
|
} else if (rc == 0) {
|
|
rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath,
|
|
&fv, &bindings, 0);
|
|
}
|
|
sep_import_set_free(&fv);
|
|
if (bindings.n > 1)
|
|
qsort(bindings.paths, (size_t)bindings.n,
|
|
sizeof *bindings.paths, strs_cmp);
|
|
if (rc == 0 && g->pkg[pi].emit_context < 0) {
|
|
g->pkg[pi].bindings = bindings;
|
|
bindings.paths = NULL;
|
|
bindings.n = bindings.cap = 0;
|
|
g->pkg[pi].emit_context = context;
|
|
} else if (rc == 0) {
|
|
struct ImportSet *want = &g->pkg[pi].bindings;
|
|
if (want->n != bindings.n) rc = -1;
|
|
for (int i = 0; i < want->n && rc == 0; i++)
|
|
if (strcmp(want->paths[i], bindings.paths[i]) != 0)
|
|
rc = -1;
|
|
if (rc < 0) {
|
|
fprintf(stderr,
|
|
"ww: package %s resolves imports differently in %s and %s\n",
|
|
g->pkg[pi].path[0] ? g->pkg[pi].path : g->pkg[pi].canon,
|
|
g->context[g->pkg[pi].emit_context].root,
|
|
g->context[context].root);
|
|
}
|
|
}
|
|
sep_import_set_free(&bindings);
|
|
if (rc < 0) {
|
|
g->pkg[pi].context_state[context] = 2;
|
|
g->pkg[pi].failed = 1;
|
|
return -1;
|
|
}
|
|
for (int i = 1; i < g->pkg[pi].ndeps; i++) {
|
|
int v = g->pkg[pi].deps[i];
|
|
int j = i;
|
|
while (j > 0 && sep_dep_cmp(g, g->pkg[pi].deps[j - 1], v) > 0) {
|
|
g->pkg[pi].deps[j] = g->pkg[pi].deps[j - 1];
|
|
j--;
|
|
}
|
|
g->pkg[pi].deps[j] = v;
|
|
}
|
|
/* Mark before recursion so a source cycle terminates here; topo emits the
|
|
* stable cycle diagnostic after all direct bindings are known. */
|
|
g->pkg[pi].context_state[context] = 2;
|
|
for (int k = 0; k < g->pkg[pi].ndeps; k++)
|
|
if (sep_load_pkg(g, g->pkg[pi].deps[k], context) < 0) {
|
|
g->pkg[pi].failed = 1;
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
sep_import_component(const char *s, size_t n)
|
|
{
|
|
if (n == 0 || !((s[0] >= 'a' && s[0] <= 'z')
|
|
|| (s[0] >= 'A' && s[0] <= 'Z') || s[0] == '_'))
|
|
return 0;
|
|
for (size_t i = 1; i < n; i++)
|
|
if (!((s[i] >= 'a' && s[i] <= 'z')
|
|
|| (s[i] >= 'A' && s[i] <= 'Z')
|
|
|| (s[i] >= '0' && s[i] <= '9') || s[i] == '_'))
|
|
return 0;
|
|
return kwlookup(s, (u64)n) == TK_NONE;
|
|
}
|
|
|
|
static int
|
|
sep_import_path_from_relative(const char *rel, char *out, size_t outsz)
|
|
{
|
|
size_t off = 0;
|
|
const char *p = rel;
|
|
while (*p != '\0') {
|
|
const char *slash = strchr(p, '/');
|
|
size_t n = slash ? (size_t)(slash - p) : strlen(p);
|
|
if (!sep_import_component(p, n)) return 0;
|
|
if (off + n + (slash != NULL) + 1 > outsz) return -1;
|
|
memcpy(out + off, p, n);
|
|
off += n;
|
|
if (slash == NULL) break;
|
|
out[off++] = '.';
|
|
p = slash + 1;
|
|
}
|
|
out[off] = '\0';
|
|
return off != 0;
|
|
}
|
|
|
|
/* A reverse candidate is authoritative only when the normal ordered forward
|
|
* lookup selects this exact canonical directory. This prevents a later or
|
|
* nested source root from manufacturing an alias shadowed by an earlier root. */
|
|
static int
|
|
sep_reverse_import_base(const struct sepgraph *g, const struct seppkg *pkg,
|
|
int context, char *out, size_t outsz)
|
|
{
|
|
const char *searchpath = g->context[context].searchpath;
|
|
const char *p = searchpath;
|
|
while (*p != '\0') {
|
|
const char *e = strchr(p, ':');
|
|
size_t n = e ? (size_t)(e - p) : strlen(p);
|
|
char *root = malloc(n + 1);
|
|
if (root == NULL) return -1;
|
|
memcpy(root, p, n);
|
|
root[n] = '\0';
|
|
char *canon = n == 0 ? NULL : realpath(root, NULL);
|
|
free(root);
|
|
if (canon != NULL) {
|
|
size_t rn = strlen(canon);
|
|
const char *rel = NULL;
|
|
if (rn == 1 && canon[0] == '/' && pkg->canon[0] == '/'
|
|
&& pkg->canon[1] != '\0')
|
|
rel = pkg->canon + 1;
|
|
else if (strncmp(pkg->canon, canon, rn) == 0
|
|
&& pkg->canon[rn] == '/' && pkg->canon[rn + 1] != '\0')
|
|
rel = pkg->canon + rn + 1;
|
|
if (rel != NULL) {
|
|
int ir = sep_import_path_from_relative(rel, out, outsz);
|
|
if (ir < 0) {
|
|
fprintf(stderr,
|
|
"ww: package path is too long (limit %d bytes)\n",
|
|
SEP_IMPORT_PATH_MAX - 1);
|
|
free(canon);
|
|
return -1;
|
|
}
|
|
if (ir > 0 && reserved_import_path(out)) ir = 0;
|
|
if (ir > 0) {
|
|
char located[1024];
|
|
if (locate_import(searchpath, rel, located,
|
|
sizeof located)) {
|
|
char *selected = realpath(located, NULL);
|
|
int same = selected != NULL
|
|
&& strcmp(selected, pkg->canon) == 0;
|
|
free(selected);
|
|
if (same) { free(canon); return 1; }
|
|
}
|
|
}
|
|
}
|
|
free(canon);
|
|
}
|
|
if (!e) break;
|
|
p = e + 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
sep_ordinary_declared_name(const struct seppkg *p, char *out, size_t outsz)
|
|
{
|
|
if (p->name[0] == '\0') return -1;
|
|
size_t n = strlen(p->name);
|
|
if (p->variant == SEP_VARIANT_EXTERNAL) {
|
|
if (n <= 5 || strcmp(p->name + n - 5, "_test") != 0) {
|
|
fprintf(stderr,
|
|
"ww: package-test selector does not name an external package\n");
|
|
return -1;
|
|
}
|
|
n -= 5;
|
|
}
|
|
if (n + 1 > outsz) return -1;
|
|
memcpy(out, p->name, n);
|
|
out[n] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
/* The reserved local namespace is reversible, so filesystem identity never
|
|
* depends on a hash, request order, output name, or another selected package. */
|
|
static int
|
|
sep_local_import_base(const struct seppkg *p, char *out, size_t outsz)
|
|
{
|
|
char leaf[sizeof p->name];
|
|
if (sep_ordinary_declared_name(p, leaf, sizeof leaf) < 0) return -1;
|
|
size_t off = 0;
|
|
int n = snprintf(out, outsz, "%s.p", SEP_LOCAL_IMPORT_PREFIX);
|
|
if (n < 0 || (size_t)n >= outsz) return -1;
|
|
off = (size_t)n;
|
|
static const char hex[] = "0123456789abcdef";
|
|
for (const unsigned char *s = (const unsigned char *)p->canon;
|
|
*s != '\0'; s++) {
|
|
unsigned char c = *s;
|
|
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
|
|| (c >= '0' && c <= '9')) {
|
|
if (off + 1 >= outsz) return -1;
|
|
out[off++] = (char)c;
|
|
} else if (c == '_' || c == '/') {
|
|
if (off + 2 >= outsz) return -1;
|
|
out[off++] = '_';
|
|
out[off++] = c == '_' ? 'u' : 's';
|
|
} else {
|
|
if (off + 4 >= outsz) return -1;
|
|
out[off++] = '_';
|
|
out[off++] = 'x';
|
|
out[off++] = hex[c >> 4];
|
|
out[off++] = hex[c & 15];
|
|
}
|
|
}
|
|
size_t ln = strlen(leaf);
|
|
if (off + 1 + ln + 1 > outsz) return -1;
|
|
out[off++] = '.';
|
|
memcpy(out + off, leaf, ln + 1);
|
|
return 0;
|
|
}
|
|
|
|
/* Finalization verifies every reached context before generated-main creation.
|
|
* Source bindings and explicit lookup identities remain authoritative; a
|
|
* literal root either round-trips through an active root or receives the
|
|
* reserved reversible local identity. */
|
|
static int
|
|
sep_finalize_directory_identities(struct sepgraph *g)
|
|
{
|
|
for (int pi = 0; pi < g->n; pi++) {
|
|
struct seppkg *p = &g->pkg[pi];
|
|
if (!p->is_dir || p->generated_main || p->failed || !p->loaded
|
|
|| p->role == SEP_ROLE_TEST_SUPPORT
|
|
|| p->import_base[0] != '\0')
|
|
continue;
|
|
for (int ci = 0; ci < g->ncontext; ci++) {
|
|
if (p->context_state[ci] != 2) continue;
|
|
char candidate[SEP_IMPORT_PATH_MAX];
|
|
int found = sep_reverse_import_base(g, p, ci, candidate,
|
|
sizeof candidate);
|
|
if (found < 0) return -1;
|
|
if (found > 0 && sep_bind_import_base(g, pi, candidate) < 0)
|
|
return -1;
|
|
}
|
|
}
|
|
for (int pi = 0; pi < g->n; pi++) {
|
|
struct seppkg *p = &g->pkg[pi];
|
|
if (!p->is_dir || p->generated_main || p->failed || !p->loaded
|
|
|| p->import_base[0] != '\0')
|
|
continue;
|
|
const char *base = NULL;
|
|
for (int i = 0; i < g->n; i++) {
|
|
if (i == pi || !g->pkg[i].is_dir || g->pkg[i].generated_main
|
|
|| g->pkg[i].role == SEP_ROLE_TEST_SUPPORT
|
|
|| p->role == SEP_ROLE_TEST_SUPPORT
|
|
|| strcmp(g->pkg[i].canon, p->canon) != 0
|
|
|| g->pkg[i].import_base[0] == '\0')
|
|
continue;
|
|
base = g->pkg[i].import_base;
|
|
break;
|
|
}
|
|
char local[SEP_IMPORT_PATH_MAX];
|
|
if (base == NULL) {
|
|
if (sep_local_import_base(p, local, sizeof local) < 0) {
|
|
fprintf(stderr,
|
|
"ww: local package identity is too long (limit %d bytes)\n",
|
|
SEP_IMPORT_PATH_MAX - 1);
|
|
return -1;
|
|
}
|
|
base = local;
|
|
}
|
|
if (sep_bind_import_base(g, pi, base) < 0) return -1;
|
|
}
|
|
for (int pi = 0; pi < g->n; pi++) {
|
|
struct seppkg *p = &g->pkg[pi];
|
|
if (!p->is_dir || p->generated_main || p->failed || !p->loaded)
|
|
continue;
|
|
const char *dot = strrchr(p->path, '.');
|
|
const char *leaf = dot ? dot + 1 : p->path;
|
|
int support_alias = p->role == SEP_ROLE_TEST_SUPPORT
|
|
&& strcmp(p->path, SEP_TEST_SUPPORT_MODULE) == 0
|
|
&& strcmp(p->name, "test") == 0;
|
|
if (!support_alias && strcmp(p->name, leaf) != 0) {
|
|
fprintf(stderr, "ww: package %s does not match import path %s\n",
|
|
p->name, p->path);
|
|
return -1;
|
|
}
|
|
p->artifact[0] = '\0';
|
|
int n = 0;
|
|
char action_path[sizeof p->path];
|
|
snprintf(action_path, sizeof action_path, "%s", p->path);
|
|
if (p->variant == SEP_VARIANT_SAME_TEST)
|
|
n = snprintf(p->artifact, sizeof p->artifact,
|
|
"%s-internal-test", action_path);
|
|
else if (p->variant == SEP_VARIANT_EXTERNAL)
|
|
n = snprintf(p->artifact, sizeof p->artifact,
|
|
"%s-external-test", action_path);
|
|
if (n < 0 || (size_t)n >= sizeof p->artifact) {
|
|
fprintf(stderr, "ww: package variant artifact identity is too long\n");
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* DFS post-order over the dep DAG → reverse-topo (deps before importer),
|
|
* cite Hare gather (deps.ha:123). Tri-color: a gray back-edge is a loud
|
|
* dep-cycle reject naming the chain (Hare deps.ha:243); `stack[0..depth)`
|
|
* is the live DFS path, so the cycle runs from pi's first occurrence on
|
|
* it to the top, closing back on pi. */
|
|
static int
|
|
sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no,
|
|
int *stack, int depth)
|
|
{
|
|
if (g->pkg[pi].color == 2) return 0;
|
|
if (g->pkg[pi].color == 1) {
|
|
int j = 0;
|
|
while (j < depth && stack[j] != pi) j++;
|
|
fprintf(stderr, "ww: dependency cycle: ");
|
|
for (int s = j; s < depth; s++)
|
|
fprintf(stderr, "%s -> ", g->pkg[stack[s]].path[0]
|
|
? g->pkg[stack[s]].path : "(root)");
|
|
fprintf(stderr, "%s\n",
|
|
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
|
return -1;
|
|
}
|
|
g->pkg[pi].color = 1;
|
|
stack[depth] = pi;
|
|
for (int k = 0; k < g->pkg[pi].ndeps; k++)
|
|
if (sep_topo_visit(g, g->pkg[pi].deps[k], order, no,
|
|
stack, depth + 1) < 0)
|
|
return -1;
|
|
g->pkg[pi].color = 2;
|
|
order[(*no)++] = pi;
|
|
return 0;
|
|
}
|
|
|
|
/* Internal test variants replace their colocated production action in the
|
|
* corresponding test link closure. Canonical package identity has already
|
|
* made every remaining compiler qualifier globally unambiguous. */
|
|
static int
|
|
sep_internal_replaces_production(const struct sepgraph *g, int a, int b)
|
|
{
|
|
const struct seppkg *internal = &g->pkg[a];
|
|
const struct seppkg *production = &g->pkg[b];
|
|
if (internal->variant != SEP_VARIANT_SAME_TEST) {
|
|
internal = &g->pkg[b];
|
|
production = &g->pkg[a];
|
|
}
|
|
return internal->variant == SEP_VARIANT_SAME_TEST
|
|
&& production->variant == SEP_VARIANT_PRODUCTION
|
|
&& production->role != SEP_ROLE_TEST_SUPPORT
|
|
&& strcmp(internal->canon, production->canon) == 0;
|
|
}
|
|
|
|
static int
|
|
sep_validate_module_closure(struct sepgraph *g, const int *order, int n,
|
|
int include_root)
|
|
{
|
|
for (int i = 0; i < n; i++) {
|
|
int a = order[i];
|
|
if ((!include_root && g->pkg[a].root)
|
|
|| g->pkg[a].path[0] == '\0') continue;
|
|
for (int j = i + 1; j < n; j++) {
|
|
int b = order[j];
|
|
if (!include_root && g->pkg[b].root) continue;
|
|
if (strcmp(g->pkg[a].path, g->pkg[b].path) == 0
|
|
&& !sep_internal_replaces_production(g, a, b)) {
|
|
fprintf(stderr,
|
|
"ww: product closure contains multiple packages named %s\n",
|
|
g->pkg[a].path);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Emit one of pi's own source files into the sep-unit under the
|
|
* //ww:module-reset primary boundary. No source or export outside pi's
|
|
* sorted owned-source set may enter this unit. */
|
|
static int
|
|
sep_emit_body(FILE *out, const char *path, const char *modpath)
|
|
{
|
|
char *buf;
|
|
u64 len;
|
|
if (sep_slurp(path, &buf, &len) < 0) {
|
|
fprintf(stderr, "ww: cannot read %s\n", path);
|
|
return -1;
|
|
}
|
|
/* #57: tag the primary body by its full dotted import path so the
|
|
* definer mangles == the importer reference; a root build (path "")
|
|
* stays a bare reset (keeps bare main). */
|
|
int bad = 0;
|
|
if (modpath != NULL && modpath[0] != '\0') {
|
|
if (fprintf(out, "//ww:module-reset %s\n", modpath) < 0)
|
|
bad = 1;
|
|
} else if (fputs("//ww:module-reset\n", out) == EOF) {
|
|
bad = 1;
|
|
}
|
|
if (fwrite(buf, 1, (size_t)len, out) != (size_t)len
|
|
|| fputc('\n', out) == EOF)
|
|
bad = 1;
|
|
free(buf);
|
|
if (bad) {
|
|
fprintf(stderr, "ww: cannot write package unit\n");
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Compose pi's sep-unit at `unitf` from only pi's byte-sorted sources.
|
|
* Direct exports are separate compiler inputs; the linker separately retains
|
|
* the reachable archive closure. */
|
|
static int
|
|
sep_compose_unit(struct sepgraph *g, int pi, const char *unitf)
|
|
{
|
|
if (g->pkg[pi].emit_context < 0
|
|
|| g->pkg[pi].emit_context >= g->ncontext)
|
|
return -1;
|
|
FILE *u = fopen(unitf, "wb");
|
|
if (u == NULL) {
|
|
fprintf(stderr, "ww: cannot open %s\n", unitf);
|
|
return -1;
|
|
}
|
|
int bodyrc = 0;
|
|
if (g->pkg[pi].generated_main) {
|
|
if (fprintf(u, "//ww:module-reset %s\npackage main;\n",
|
|
g->pkg[pi].path) < 0)
|
|
bodyrc = -1;
|
|
for (int i = 0; i < g->pkg[pi].ndeps && bodyrc == 0; i++)
|
|
if (fprintf(u, "import %s;\n",
|
|
g->pkg[g->pkg[pi].deps[i]].path) < 0)
|
|
bodyrc = -1;
|
|
} else if (g->pkg[pi].is_dir) {
|
|
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
|
|
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i],
|
|
g->pkg[pi].path);
|
|
} else {
|
|
bodyrc = sep_emit_body(u, g->pkg[pi].entry, g->pkg[pi].path);
|
|
}
|
|
if (fclose(u) != 0) {
|
|
fprintf(stderr, "ww: cannot close package unit %s\n", unitf);
|
|
return -1;
|
|
}
|
|
return bodyrc;
|
|
}
|
|
|
|
/* archive_o — write a deterministic single-member SysV ar archive at
|
|
* `apath` wrapping the object at `objpath`. No armap / long-name table:
|
|
* w6l reads each member's ELF .symtab directly (obj.c elf_globals) and
|
|
* skips '/'-named members, so a package `.a` needs only the global magic,
|
|
* one 60-byte member header, and the `.o` bytes (newline-padded to even).
|
|
* Zeroed mtime/uid/gid + fixed mode + a fixed member name make the bytes
|
|
* a pure function of the `.o` content → cstage `.a` == wwstage `.a`
|
|
* (rule 10). The wwstage twin is archiveo (selfhost/cmd/ww/main.ww). */
|
|
static int
|
|
archive_o(const char *objpath, const char *apath)
|
|
{
|
|
FILE *in = fopen(objpath, "rb");
|
|
if (in == NULL) {
|
|
fprintf(stderr, "ww: cannot read %s\n", objpath);
|
|
return -1;
|
|
}
|
|
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
|
|
long n = ftell(in);
|
|
if (n < 0 || fseek(in, 0, SEEK_SET) != 0) {
|
|
fclose(in);
|
|
return -1;
|
|
}
|
|
unsigned char *buf = malloc((size_t)n);
|
|
if (buf == NULL) { fclose(in); return -1; }
|
|
if (fread(buf, 1, (size_t)n, in) != (size_t)n) {
|
|
free(buf); fclose(in); return -1;
|
|
}
|
|
if (fclose(in) != 0) { free(buf); return -1; }
|
|
|
|
FILE *out = fopen(apath, "wb");
|
|
if (out == NULL) {
|
|
fprintf(stderr, "ww: cannot open %s\n", apath);
|
|
free(buf);
|
|
return -1;
|
|
}
|
|
int bad = fwrite("!<arch>\n", 1, 8, out) != 8;
|
|
/* ar(5) fixes each member header at 60 bytes; the offsets below
|
|
* address fields in that serialized header. */
|
|
char hdr[60];
|
|
memset(hdr, ' ', sizeof hdr);
|
|
memcpy(hdr + 0, "pkg.o/", 6); /* GNU short-name '/' terminator */
|
|
hdr[16] = '0'; /* mtime (zeroed → determinism) */
|
|
hdr[28] = '0'; /* uid (zeroed) */
|
|
hdr[34] = '0'; /* gid (zeroed) */
|
|
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
|
|
char sz[12];
|
|
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
|
|
if (szn <= 0 || szn > 10) bad = 1;
|
|
else memcpy(hdr + 48, sz, (size_t)szn);
|
|
hdr[58] = 0x60; /* member-header magic byte */
|
|
hdr[59] = 0x0a;
|
|
if (fwrite(hdr, 1, sizeof hdr, out) != sizeof hdr
|
|
|| fwrite(buf, 1, (size_t)n, out) != (size_t)n)
|
|
bad = 1;
|
|
if ((n & 1) && fputc('\n', out) == EOF) bad = 1;
|
|
if (fclose(out) != 0) bad = 1;
|
|
free(buf);
|
|
if (bad) {
|
|
fprintf(stderr, "ww: cannot write archive %s\n", apath);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* -w workdir freshness: a `-w DIR` workdir is a caller-owned persistent
|
|
* package-artifact tree that replaces the fresh `.sepwork` scratch.
|
|
* Staleness is pure content identity, never mtime: a package is reused only
|
|
* when its freshly composed unit byte-equals the committed unit, no direct
|
|
* dependency emitted a changed export, AND the driver/tool copies recorded in
|
|
* the dir byte-equal the live executables — every decision is reproducible by
|
|
* hand with cmp(1) against plain files. Artifacts commit
|
|
* via temp + rename with the unit renamed last, so a killed build can
|
|
* never leave a committed unit vouching for uncommitted artifacts. The
|
|
* caller serializes invocations per workdir (Make target = one workdir)
|
|
* and `make clean` reclaims the state; the wwstage twin is the
|
|
* fileequal/copyfileatomic/workdirstamp group in selfhost/cmd/ww/main.ww. */
|
|
|
|
/* `.s`/`.wwi` may be legitimately empty (an FFI-only package like rt
|
|
* emits no text), so committed presence is their freshness test; the
|
|
* rename-commit protocol owns integrity. `.o`/`.a` are never empty
|
|
* (ELF/ar headers), so a zero size there is always a torn write. */
|
|
static int
|
|
file_is_reg(const char *path)
|
|
{
|
|
struct stat st;
|
|
return stat(path, &st) == 0 && S_ISREG(st.st_mode);
|
|
}
|
|
|
|
static int
|
|
file_size_nonzero(const char *path)
|
|
{
|
|
struct stat st;
|
|
return stat(path, &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0;
|
|
}
|
|
|
|
/* Byte equality of two files; absence or IO error is inequality. */
|
|
static int
|
|
file_equal(const char *a, const char *b)
|
|
{
|
|
FILE *fa = fopen(a, "rb");
|
|
if (fa == NULL) return 0;
|
|
FILE *fb = fopen(b, "rb");
|
|
if (fb == NULL) { fclose(fa); return 0; }
|
|
static char ba[65536], bb[65536];
|
|
int eq = 1;
|
|
for (;;) {
|
|
size_t na = fread(ba, 1, sizeof ba, fa);
|
|
size_t nb = fread(bb, 1, sizeof bb, fb);
|
|
if (na != nb || memcmp(ba, bb, na) != 0) { eq = 0; break; }
|
|
if (na < sizeof ba) {
|
|
if (ferror(fa) || ferror(fb)) eq = 0;
|
|
break;
|
|
}
|
|
}
|
|
fclose(fa); fclose(fb);
|
|
return eq;
|
|
}
|
|
|
|
/* Replace dst with src's bytes via temp + rename, so a torn write can
|
|
* never masquerade as a committed tool copy. */
|
|
static int
|
|
copy_file_atomic(const char *src, const char *dst)
|
|
{
|
|
char tmp[1100];
|
|
snprintf(tmp, sizeof tmp, "%s.new", dst);
|
|
FILE *in = fopen(src, "rb");
|
|
if (in == NULL) return -1;
|
|
FILE *out = fopen(tmp, "wb");
|
|
if (out == NULL) { fclose(in); return -1; }
|
|
static char buf[65536];
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof buf, in)) > 0)
|
|
if (fwrite(buf, 1, n, out) != n) {
|
|
fclose(in); fclose(out); return -1;
|
|
}
|
|
int bad = ferror(in);
|
|
fclose(in);
|
|
if (fclose(out) != 0 || bad) return -1;
|
|
return rename(tmp, dst);
|
|
}
|
|
|
|
/* A coordinator-private completion marker distinguishes a newly linked
|
|
* product from a caller-owned binary left behind by an earlier invocation. */
|
|
static int
|
|
record_product_status(const char *path)
|
|
{
|
|
if (path == NULL) return 0;
|
|
char tmp[1100];
|
|
snprintf(tmp, sizeof tmp, "%s.new", path);
|
|
FILE *f = fopen(tmp, "wb");
|
|
if (f == NULL) return -1;
|
|
int bad = fputs("ok\n", f) == EOF;
|
|
if (fclose(f) != 0) bad = 1;
|
|
if (bad) return -1;
|
|
return rename(tmp, path);
|
|
}
|
|
|
|
/* The stamp pins the non-content build inputs a unit compare cannot see:
|
|
* the -T/-S shape of the producer pass and the artifact protocol
|
|
* revision (bump "fmt" when the unit/archive/commit format changes). */
|
|
static void
|
|
workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
|
|
{
|
|
snprintf(buf, bufsz, "ww workdir fmt %d mode %s asm %d\n",
|
|
is_test ? 9 : 8, is_test ? "test" : "build", emit_asm);
|
|
}
|
|
|
|
/* A stale global builder identity invalidates every committed unit voucher in
|
|
* this driver-owned workdir before compilation starts. Artifacts may remain,
|
|
* but without their unit they cannot be reused. That makes it safe to record
|
|
* the new identity after a partial multi-root pass: successful actions have
|
|
* current units, while failed and no-longer-requested actions do not. */
|
|
static int
|
|
invalidate_workdir_units(const char *scratch)
|
|
{
|
|
DIR *d = opendir(scratch);
|
|
if (d == NULL) return -1;
|
|
struct dirent *de;
|
|
int rc = 0;
|
|
while ((de = readdir(d)) != NULL) {
|
|
size_t n = strlen(de->d_name);
|
|
if (n < 8 || strcmp(de->d_name + n - 8, ".unit.ww") != 0)
|
|
continue;
|
|
char path[SEP_ARTIFACT_MAX];
|
|
int pn = snprintf(path, sizeof path, "%s/%s", scratch,
|
|
de->d_name);
|
|
if (pn < 0 || (size_t)pn >= sizeof path
|
|
|| (unlink(path) != 0 && errno != ENOENT)) {
|
|
rc = -1;
|
|
break;
|
|
}
|
|
}
|
|
if (closedir(d) != 0) rc = -1;
|
|
if (rc != 0)
|
|
fprintf(stderr, "ww: cannot invalidate stale package units\n");
|
|
return rc;
|
|
}
|
|
|
|
/* 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
|
|
* producer loop (one `w6c -c -I` per package,
|
|
* each package `.o` wrapped in its own deterministic `.a`), then a
|
|
* reverse-topo `w6l` of each root `.a` + reachable `.a` set + libwwrt.a. Side
|
|
* files land in a cold `<stem>.sepwork` dir, or under the persistent
|
|
* `-w` workdir with content-identity package reuse. */
|
|
static int
|
|
build_one_sep_impl(const char *src, int entry_is_dir,
|
|
const char *out, const char *objstem, const char *extra_includes,
|
|
const struct seplinkflags *linkflags, int package_only, int is_test,
|
|
struct sepproduct *products, int nproducts, int emit_asm,
|
|
const char *workdir, char *scratchout, size_t scratchoutsz,
|
|
struct sepgraph **graphout)
|
|
{
|
|
if (nproducts < 1 || nproducts > SEP_MAXPRODUCT) return 1;
|
|
for (int i = 0; i < nproducts; i++)
|
|
if (products[i].status != NULL
|
|
&& unlink(products[i].status) != 0 && errno != ENOENT)
|
|
return 1;
|
|
const char *c6 = toolpath("WW_W6C", "w6c");
|
|
const char *a6 = toolpath("WW_W6A", "w6a");
|
|
const char *l6 = toolpath("WW_W6L", "w6l");
|
|
const char *libdir = envpath("WW_LIB");
|
|
if (libdir == NULL) {
|
|
static char libbuf[1024];
|
|
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
|
|
libdir = libbuf;
|
|
}
|
|
const char *srcdir = envpath("WW_SRCLIB");
|
|
static char srcbuf[1024];
|
|
if (srcdir == NULL) {
|
|
snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir);
|
|
if (access(srcbuf, 0) == 0) srcdir = srcbuf;
|
|
else if (access("lib", 0) == 0) srcdir = "lib";
|
|
else srcdir = libdir;
|
|
}
|
|
const char *toolsrcdir = srcdir;
|
|
char srcd[1024];
|
|
if (entry_is_dir) {
|
|
snprintf(srcd, sizeof srcd, "%s", src);
|
|
size_t n = strlen(srcd);
|
|
while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0';
|
|
} else {
|
|
const char *slash = strrchr(src, '/');
|
|
if (slash) {
|
|
size_t n = (size_t)(slash - src);
|
|
if (n >= sizeof srcd) n = sizeof srcd - 1;
|
|
memcpy(srcd, src, n);
|
|
srcd[n] = '\0';
|
|
} else { srcd[0] = '.'; srcd[1] = '\0'; }
|
|
}
|
|
char stem[1024];
|
|
if (entry_is_dir) {
|
|
const char *b = strrchr(srcd, '/');
|
|
const char *base = b ? b + 1 : srcd;
|
|
snprintf(stem, sizeof stem, "%s/%s", srcd, base);
|
|
} else {
|
|
snprintf(stem, sizeof stem, "%s", src);
|
|
char *dot = strrchr(stem, '.');
|
|
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
|
|
}
|
|
const char *ostem = (objstem && objstem[0]) ? objstem : stem;
|
|
int warm = workdir != NULL && workdir[0] != 0;
|
|
char scratch[1100];
|
|
if (warm) {
|
|
struct stat wst;
|
|
if (stat(workdir, &wst) != 0 || !S_ISDIR(wst.st_mode)) {
|
|
fprintf(stderr, "ww: workdir %s is not a directory\n",
|
|
workdir);
|
|
return 1;
|
|
}
|
|
/* The workdir is caller-owned and persistent: no acquisition,
|
|
* no refusal, and scratchout stays empty so the wrapper never
|
|
* cleans it. */
|
|
snprintf(scratch, sizeof scratch, "%s", workdir);
|
|
} else {
|
|
snprintf(scratch, sizeof scratch, "%s.sepwork", ostem);
|
|
if (mkdir(scratch, 0755) != 0) {
|
|
fprintf(stderr, "ww: cannot create scratch %s\n", scratch);
|
|
return 1;
|
|
}
|
|
/* Hand the scratch path back only after mkdir succeeds. The
|
|
* wrapper therefore never removes a pre-existing path that this
|
|
* build failed to acquire. */
|
|
if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch);
|
|
}
|
|
int stale_all = 0, stampok = 0;
|
|
char toolw[1200] = {0}, toolc[1200] = {0}, toola[1200] = {0};
|
|
char stampf[1200] = {0};
|
|
char stampwant[128];
|
|
if (warm) {
|
|
if (self_path == NULL || !file_is_reg(self_path)) {
|
|
fprintf(stderr, "ww: cannot read driver identity %s\n",
|
|
self_path ? self_path : "(unknown)");
|
|
return 1;
|
|
}
|
|
snprintf(toolw, sizeof toolw, "%s/.wwtool.ww", scratch);
|
|
snprintf(toolc, sizeof toolc, "%s/.wwtool.w6c", scratch);
|
|
snprintf(toola, sizeof toola, "%s/.wwtool.w6a", scratch);
|
|
snprintf(stampf, sizeof stampf, "%s/.wwtool.stamp", scratch);
|
|
workdir_stamp_text(stampwant, sizeof stampwant, is_test, emit_asm);
|
|
char got[128] = {0};
|
|
FILE *sf = fopen(stampf, "rb");
|
|
if (sf) {
|
|
size_t rn = fread(got, 1, sizeof got - 1, sf);
|
|
got[rn] = 0;
|
|
fclose(sf);
|
|
}
|
|
stampok = strcmp(stampwant, got) == 0;
|
|
if (!stampok || !file_equal(toolw, self_path)
|
|
|| !file_equal(toolc, c6)
|
|
|| (!emit_asm && !file_equal(toola, a6)))
|
|
stale_all = 1;
|
|
if (stale_all && invalidate_workdir_units(scratch) != 0)
|
|
return 1;
|
|
}
|
|
|
|
struct sepgraph *g = calloc(1, sizeof *g);
|
|
if (g == NULL) return 1;
|
|
g->support_context = -1;
|
|
if (graphout) *graphout = g;
|
|
int support_for[SEP_MAXPRODUCT];
|
|
for (int i = 0; i < nproducts; i++) support_for[i] = -1;
|
|
for (int i = 0; i < nproducts; i++) {
|
|
const char *entry = products[i].dir != NULL
|
|
? products[i].dir : src;
|
|
char contextdir[1024];
|
|
const char *contextroot = entry;
|
|
if (!entry_is_dir) {
|
|
const char *slash = strrchr(entry, '/');
|
|
if (slash != NULL) {
|
|
size_t n = (size_t)(slash - entry);
|
|
if (n >= sizeof contextdir) return 1;
|
|
memcpy(contextdir, entry, n);
|
|
contextdir[n] = '\0';
|
|
} else {
|
|
snprintf(contextdir, sizeof contextdir, ".");
|
|
}
|
|
contextroot = contextdir;
|
|
}
|
|
products[i].context = sep_context_for(g, contextroot,
|
|
extra_includes, toolsrcdir);
|
|
if (products[i].context < 0) return 1;
|
|
const char *selector = products[i].variant == SEP_VARIANT_PRODUCTION
|
|
? NULL : products[i].test_package;
|
|
const char *rootpath = products[i].identity != NULL
|
|
? products[i].identity : "";
|
|
products[i].root = sep_find_or_add_variant(g, rootpath, entry,
|
|
entry_is_dir, products[i].variant, selector,
|
|
SEP_ROLE_NORMAL, products[i].artifact, 1);
|
|
if (products[i].root < 0) return 1;
|
|
products[i].variant_root = products[i].root;
|
|
if (!is_test && !package_only)
|
|
g->pkg[products[i].root].link_entry = 1;
|
|
}
|
|
const char *test_support_module = "test";
|
|
/* -T generates a dispatcher whose support qualifier is selected by the
|
|
* command. Represent that compiler-generated requirement as a direct edge
|
|
* of the generated-main action. It normally coalesces with an explicit
|
|
* toolchain `import test`;
|
|
* when user source occupies that identity, the reserved graph alias keeps
|
|
* it distinct. The linker receives the same support archive closure. */
|
|
if (is_test) {
|
|
char tpath[1024];
|
|
int tdir = 0;
|
|
if (locate_import(toolsrcdir, "test", tpath, sizeof tpath)) {
|
|
tdir = 1;
|
|
g->support_context = sep_context_for(g, toolsrcdir, NULL,
|
|
toolsrcdir);
|
|
if (g->support_context < 0) return 1;
|
|
char *tc = realpath(tpath, NULL);
|
|
int collision = 0;
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int root_is_support = tc != NULL
|
|
&& strcmp(g->pkg[products[i].root].canon, tc) == 0;
|
|
const char *name = products[i].test_package;
|
|
if (!root_is_support && name != NULL
|
|
&& (strcmp(name, "test") == 0
|
|
|| strcmp(name, "test_test") == 0))
|
|
collision = 1;
|
|
}
|
|
for (int i = 0; i < nproducts && !collision; i++) {
|
|
char userpath[1024];
|
|
int userdir = 0;
|
|
if (locate_import(g->context[products[i].context].searchpath,
|
|
"test", userpath, sizeof userpath)) {
|
|
userdir = 1;
|
|
(void)userdir;
|
|
char *uc = realpath(userpath, NULL);
|
|
if (tc != NULL && uc != NULL
|
|
&& strcmp(tc, uc) != 0)
|
|
collision = 1;
|
|
free(uc);
|
|
}
|
|
}
|
|
if (collision) test_support_module = SEP_TEST_SUPPORT_MODULE;
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int root = products[i].root;
|
|
int root_is_support = tc != NULL
|
|
&& strcmp(g->pkg[root].canon, tc) == 0;
|
|
/* A same-test build of the runtime package already owns run
|
|
* and its source imports. An external test still needs the
|
|
* colocated production node, which is also its support dep. */
|
|
if (root_is_support
|
|
&& strcmp(test_support_module, "test") == 0
|
|
&& products[i].variant != SEP_VARIANT_EXTERNAL) {
|
|
support_for[i] = root;
|
|
continue;
|
|
}
|
|
int ti;
|
|
if (strcmp(test_support_module,
|
|
SEP_TEST_SUPPORT_MODULE) == 0)
|
|
ti = sep_find_or_add_role(g, test_support_module,
|
|
tpath, tdir, SEP_ROLE_TEST_SUPPORT, NULL);
|
|
else
|
|
ti = sep_find_or_add(g, test_support_module, tpath,
|
|
tdir);
|
|
if (ti < 0) return 1;
|
|
g->pkg[ti].test_support = 1;
|
|
support_for[i] = ti;
|
|
}
|
|
free(tc);
|
|
}
|
|
}
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int root = products[i].variant_root;
|
|
/* Raw single-file test fixtures are the one retained non-directory
|
|
* exception: keep compiler-owned test-main synthesis in that action.
|
|
* Its support export is still an exact direct input. */
|
|
if (is_test && !entry_is_dir) {
|
|
int support = support_for[i];
|
|
if (support >= 0 && support != root) {
|
|
int seen = 0;
|
|
for (int k = 0; k < g->pkg[root].ndeps; k++)
|
|
if (g->pkg[root].deps[k] == support) seen = 1;
|
|
if (!seen)
|
|
g->pkg[root].deps[g->pkg[root].ndeps++] = support;
|
|
}
|
|
g->pkg[root].link_entry = 1;
|
|
}
|
|
if (sep_load_pkg(g, root, products[i].context) < 0) {
|
|
g->pkg[root].failed = 1;
|
|
continue;
|
|
}
|
|
if (products[i].test_package != NULL
|
|
&& strcmp(g->pkg[root].name, products[i].test_package) != 0) {
|
|
fprintf(stderr,
|
|
"ww: package-test selector does not match loaded package\n");
|
|
g->pkg[root].failed = 1;
|
|
}
|
|
}
|
|
if (is_test && entry_is_dir) {
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int variant = products[i].variant_root;
|
|
int support = support_for[i];
|
|
if (support >= 0 && support != variant
|
|
&& sep_load_pkg(g, support, products[i].context) < 0)
|
|
g->pkg[variant].failed = 1;
|
|
}
|
|
}
|
|
if (g->identity_failed) return 1;
|
|
if (sep_finalize_directory_identities(g) < 0) return 1;
|
|
if (is_test && entry_is_dir) {
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int variant = products[i].variant_root;
|
|
int support = support_for[i];
|
|
if (g->pkg[variant].failed
|
|
|| (support >= 0 && g->pkg[support].failed)) {
|
|
products[i].root = variant;
|
|
continue;
|
|
}
|
|
int mainpkg = sep_add_generated_main(g, &products[i], i,
|
|
support);
|
|
if (mainpkg < 0) return 1;
|
|
products[i].root = mainpkg;
|
|
}
|
|
}
|
|
if (sep_validate_artifact_paths(g, scratch) < 0)
|
|
return 1;
|
|
int root_package = package_only;
|
|
if (root_package && !g->pkg[products[0].root].failed
|
|
&& strcmp(g->pkg[products[0].root].name, "main") == 0) {
|
|
fprintf(stderr, "ww: -p requires a non-main package\n");
|
|
return 1;
|
|
}
|
|
int *order = calloc((size_t)g->n, sizeof *order);
|
|
int *stack = calloc((size_t)g->n, sizeof *stack);
|
|
int norder = 0;
|
|
if (order == NULL || stack == NULL) {
|
|
free(stack); free(order); return 1;
|
|
}
|
|
/* Diagnose cycles per product before constructing the shared union. A
|
|
* variant-local cycle must not suppress an independent sibling root. */
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int root = products[i].root;
|
|
if (g->pkg[root].failed) continue;
|
|
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
|
|
int ignored = 0;
|
|
if (sep_topo_visit(g, root, order, &ignored, stack, 0) < 0
|
|
|| sep_validate_module_closure(g, order, ignored, 1) < 0)
|
|
g->pkg[root].failed = 1;
|
|
}
|
|
for (int pi = 0; pi < g->n; pi++) g->pkg[pi].color = 0;
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int root = products[i].root;
|
|
if (!g->pkg[root].failed
|
|
&& sep_topo_visit(g, root, order, &norder, stack, 0) < 0) {
|
|
free(stack); free(order); return 1;
|
|
}
|
|
}
|
|
free(stack);
|
|
int any_failed = 0;
|
|
for (int i = 0; i < nproducts; i++)
|
|
if (g->pkg[products[i].root].failed) any_failed = 1;
|
|
for (int oi = 0; oi < norder; oi++) {
|
|
int pi = order[oi];
|
|
for (int k = 0; k < g->pkg[pi].ndeps; k++)
|
|
if (g->pkg[g->pkg[pi].deps[k]].failed)
|
|
g->pkg[pi].failed = 1;
|
|
if (g->pkg[pi].failed) {
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
char unitf[SEP_ARTIFACT_MAX], wwi[SEP_ARTIFACT_MAX];
|
|
char asmf[SEP_ARTIFACT_MAX], obj[SEP_ARTIFACT_MAX];
|
|
char apath[SEP_ARTIFACT_MAX], unitnew[SEP_ARTIFACT_MAX];
|
|
char wwinew[SEP_ARTIFACT_MAX], asmnew[SEP_ARTIFACT_MAX];
|
|
char objnew[SEP_ARTIFACT_MAX], anew[SEP_ARTIFACT_MAX];
|
|
sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf);
|
|
sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi);
|
|
sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf);
|
|
sep_fname(g, pi, scratch, ".o", obj, sizeof obj);
|
|
sep_fname(g, pi, scratch, ".a", apath, sizeof apath);
|
|
sep_fname(g, pi, scratch, ".unit.new", unitnew, sizeof unitnew);
|
|
sep_fname(g, pi, scratch, ".wwi.new", wwinew, sizeof wwinew);
|
|
sep_fname(g, pi, scratch, ".s.new", asmnew, sizeof asmnew);
|
|
sep_fname(g, pi, scratch, ".o.new", objnew, sizeof objnew);
|
|
sep_fname(g, pi, scratch, ".a.new", anew, sizeof anew);
|
|
/* Warm mode compiles from staged `.new` paths and commits by
|
|
* rename; classic mode keeps its exact in-place paths. */
|
|
const char *cu = warm ? unitnew : unitf;
|
|
const char *cw = warm ? wwinew : wwi;
|
|
const char *cs = warm ? asmnew : asmf;
|
|
const char *co = warm ? objnew : obj;
|
|
const char *ca = warm ? anew : apath;
|
|
if (sep_compose_unit(g, pi, cu) < 0) {
|
|
g->pkg[pi].failed = 1;
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
int deps_changed = 0;
|
|
for (int k = 0; k < g->pkg[pi].ndeps; k++)
|
|
if (g->pkg[g->pkg[pi].deps[k]].export_changed)
|
|
deps_changed = 1;
|
|
if (warm && !stale_all && !deps_changed
|
|
&& file_equal(unitnew, unitf)
|
|
&& file_is_reg(asmf)
|
|
&& file_is_reg(wwi)
|
|
&& (emit_asm || (file_size_nonzero(obj)
|
|
&& file_size_nonzero(apath)))) {
|
|
if (unlink(unitnew) != 0) {
|
|
fprintf(stderr, "ww: cannot remove %s\n",
|
|
unitnew);
|
|
g->pkg[pi].failed = 1;
|
|
any_failed = 1;
|
|
}
|
|
continue;
|
|
}
|
|
size_t cargvcap = (size_t)(12 + 3 * g->pkg[pi].ndeps);
|
|
char **cargv = calloc(cargvcap, sizeof *cargv);
|
|
char (*importfiles)[SEP_ARTIFACT_MAX] = NULL;
|
|
if (g->pkg[pi].ndeps > 0)
|
|
importfiles = calloc((size_t)g->pkg[pi].ndeps,
|
|
sizeof *importfiles);
|
|
if (cargv == NULL || (g->pkg[pi].ndeps > 0
|
|
&& importfiles == NULL)) {
|
|
fprintf(stderr, "ww: out of memory\n");
|
|
free(importfiles);
|
|
free(cargv);
|
|
free(order);
|
|
return 1;
|
|
}
|
|
int cpos = 0;
|
|
cargv[cpos++] = "w6c";
|
|
if (g->pkg[pi].generated_main
|
|
|| (is_test && g->pkg[pi].root && !g->pkg[pi].is_dir)) {
|
|
cargv[cpos++] = "-T";
|
|
cargv[cpos++] = "--entry";
|
|
cargv[cpos++] = "--test-support-module";
|
|
cargv[cpos++] = (char *)test_support_module;
|
|
} else {
|
|
if (g->pkg[pi].variant == SEP_VARIANT_SAME_TEST
|
|
|| g->pkg[pi].variant == SEP_VARIANT_EXTERNAL)
|
|
cargv[cpos++] = "--test-package";
|
|
if (g->pkg[pi].link_entry)
|
|
cargv[cpos++] = "--entry";
|
|
if (g->pkg[pi].test_support) {
|
|
cargv[cpos++] = "--test-support-module";
|
|
cargv[cpos++] = (char *)test_support_module;
|
|
}
|
|
}
|
|
cargv[cpos++] = "-c";
|
|
for (int k = 0; k < g->pkg[pi].ndeps; k++) {
|
|
int dj = g->pkg[pi].deps[k];
|
|
sep_fname(g, dj, scratch, ".wwi", importfiles[k],
|
|
sizeof importfiles[k]);
|
|
cargv[cpos++] = "--import";
|
|
cargv[cpos++] = g->pkg[dj].path;
|
|
cargv[cpos++] = importfiles[k];
|
|
}
|
|
cargv[cpos++] = "-I";
|
|
cargv[cpos++] = (char *)cw;
|
|
cargv[cpos++] = "-o";
|
|
cargv[cpos++] = (char *)cs;
|
|
cargv[cpos++] = (char *)cu;
|
|
cargv[cpos] = NULL;
|
|
int compilerc = run_argv(c6, cargv);
|
|
free(importfiles);
|
|
free(cargv);
|
|
if (compilerc != 0) {
|
|
fprintf(stderr, "ww: w6c failed for %s\n",
|
|
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
|
g->pkg[pi].failed = 1;
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
g->pkg[pi].export_changed = !warm
|
|
|| !file_equal(wwinew, wwi);
|
|
if (!emit_asm) {
|
|
char *aargv[] = {"w6a", "-o", (char *)co,
|
|
(char *)cs, NULL};
|
|
if (run_argv(a6, aargv) != 0) {
|
|
fprintf(stderr, "ww: w6a failed for %s\n",
|
|
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
|
g->pkg[pi].failed = 1;
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
}
|
|
/* Every package action, including executable and generated-test roots,
|
|
* produces the existing deterministic single-member archive. */
|
|
if (!emit_asm) {
|
|
if (archive_o(co, ca) != 0) {
|
|
fprintf(stderr, "ww: archive failed for %s\n",
|
|
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
|
g->pkg[pi].failed = 1;
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
}
|
|
/* Commit order: artifacts before the unit that vouches for
|
|
* them, unit strictly last. */
|
|
if (warm) {
|
|
if (rename(wwinew, wwi) != 0
|
|
|| rename(asmnew, asmf) != 0
|
|
|| (!emit_asm && rename(objnew, obj) != 0)
|
|
|| (!emit_asm && rename(anew, apath) != 0)
|
|
|| rename(unitnew, unitf) != 0) {
|
|
fprintf(stderr, "ww: cannot commit %s\n",
|
|
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
|
|
g->pkg[pi].failed = 1;
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
/* Stale passes removed every old unit voucher before compiling. Current
|
|
* successful units are therefore safe to vouch for even when a sibling
|
|
* root failed; a killed pass leaves the old identity and forces another
|
|
* invalidating pass, never false reuse. */
|
|
if (warm) {
|
|
if (!file_equal(toolw, self_path)
|
|
&& copy_file_atomic(self_path, toolw) != 0) {
|
|
fprintf(stderr, "ww: cannot record %s\n", toolw);
|
|
free(order); return 1;
|
|
}
|
|
if (!file_equal(toolc, c6)
|
|
&& copy_file_atomic(c6, toolc) != 0) {
|
|
fprintf(stderr, "ww: cannot record %s\n", toolc);
|
|
free(order); return 1;
|
|
}
|
|
if (!emit_asm && !file_equal(toola, a6)
|
|
&& copy_file_atomic(a6, toola) != 0) {
|
|
fprintf(stderr, "ww: cannot record %s\n", toola);
|
|
free(order); return 1;
|
|
}
|
|
if (!stampok) {
|
|
char stampnew[1300];
|
|
snprintf(stampnew, sizeof stampnew, "%s.new", stampf);
|
|
FILE *sf = fopen(stampnew, "wb");
|
|
int bad = sf == NULL || fputs(stampwant, sf) == EOF;
|
|
if (sf != NULL && fclose(sf) != 0) bad = 1;
|
|
if (bad || rename(stampnew, stampf) != 0) {
|
|
fprintf(stderr, "ww: cannot record %s\n",
|
|
stampf);
|
|
free(order); return 1;
|
|
}
|
|
}
|
|
}
|
|
if (emit_asm) { free(order); return any_failed ? 1 : 0; }
|
|
if (root_package) {
|
|
int root = products[0].root;
|
|
if (g->pkg[root].failed) { free(order); return 1; }
|
|
char archive[SEP_ARTIFACT_MAX], iface[SEP_ARTIFACT_MAX];
|
|
char outiface[SEP_ARTIFACT_MAX];
|
|
sep_fname(g, root, scratch, ".a", archive, sizeof archive);
|
|
sep_fname(g, root, scratch, ".wwi", iface, sizeof iface);
|
|
snprintf(outiface, sizeof outiface, "%s.wwi", out);
|
|
if (copy_file_atomic(archive, out) != 0
|
|
|| copy_file_atomic(iface, outiface) != 0) {
|
|
fprintf(stderr, "ww: cannot write package artifact %s\n", out);
|
|
free(order);
|
|
return 1;
|
|
}
|
|
free(order);
|
|
return 0;
|
|
}
|
|
|
|
free(order);
|
|
/* Each product gets its own reverse-topological archive closure: root `.a`
|
|
* first, then every transitively reachable package `.a`, then libwwrt.a. An
|
|
* internal test variant already contains production sources, so its
|
|
* colocated production archive is omitted without dropping dependencies. */
|
|
char rtpaths[2][1024];
|
|
int nrt = 1;
|
|
snprintf(rtpaths[0], sizeof rtpaths[0], "%s/libwwrt.a", libdir);
|
|
if (access(rtpaths[0], 0) != 0) {
|
|
nrt = 2;
|
|
snprintf(rtpaths[0], sizeof rtpaths[0],
|
|
"%s/../obj/rt/start.o", self_dir);
|
|
snprintf(rtpaths[1], sizeof rtpaths[1],
|
|
"%s/../obj/rt/syscall.o", self_dir);
|
|
}
|
|
int nlibdirs = linkflags ? linkflags->nlibdirs : 0;
|
|
int nlibs = linkflags ? linkflags->nlibs : 0;
|
|
for (int i = 0; i < nproducts; i++) {
|
|
int root = products[i].root;
|
|
int variant_root = products[i].variant_root;
|
|
if (g->pkg[root].failed) { 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);
|
|
int nlink = 0;
|
|
if (linkorder == NULL || linkstack == NULL
|
|
|| sep_topo_visit(g, root, linkorder, &nlink,
|
|
linkstack, 0) < 0) {
|
|
free(linkstack); free(linkorder); return 1;
|
|
}
|
|
free(linkstack);
|
|
size_t largvcap = (size_t)(3 + nlink + nrt
|
|
+ 2 * nlibdirs + 2 * nlibs + 1);
|
|
char **largv = calloc(largvcap, sizeof *largv);
|
|
char (*linkpaths)[SEP_ARTIFACT_MAX] = calloc((size_t)nlink,
|
|
sizeof *linkpaths);
|
|
if (largv == NULL || linkpaths == NULL) {
|
|
free(linkpaths); free(largv); free(linkorder);
|
|
return 1;
|
|
}
|
|
int pos = 0, npath = 0;
|
|
largv[pos++] = "w6l";
|
|
largv[pos++] = "-o";
|
|
largv[pos++] = (char *)products[i].out;
|
|
for (int oi = nlink - 1; oi >= 0; oi--) {
|
|
int pi = linkorder[oi];
|
|
if (variant_root >= 0
|
|
&& g->pkg[variant_root].variant == SEP_VARIANT_SAME_TEST
|
|
&& pi != variant_root
|
|
&& g->pkg[pi].variant == SEP_VARIANT_PRODUCTION
|
|
&& g->pkg[pi].role != SEP_ROLE_TEST_SUPPORT
|
|
&& strcmp(g->pkg[pi].canon,
|
|
g->pkg[variant_root].canon) == 0)
|
|
continue;
|
|
sep_fname(g, pi, scratch, ".a", linkpaths[npath],
|
|
sizeof linkpaths[npath]);
|
|
largv[pos++] = linkpaths[npath++];
|
|
}
|
|
free(linkorder);
|
|
for (int ri = 0; ri < nrt; ri++) largv[pos++] = rtpaths[ri];
|
|
for (int li = 0; li < nlibdirs; li++) {
|
|
largv[pos++] = "-L";
|
|
largv[pos++] = (char *)linkflags->libdirs[li];
|
|
}
|
|
for (int li = 0; li < nlibs; li++) {
|
|
largv[pos++] = "-l";
|
|
largv[pos++] = (char *)linkflags->libs[li];
|
|
}
|
|
largv[pos] = NULL;
|
|
int linkrc = run_argv(l6, largv);
|
|
free(linkpaths);
|
|
free(largv);
|
|
if (linkrc != 0) {
|
|
fprintf(stderr, "ww: w6l failed\n");
|
|
g->pkg[root].failed = 1;
|
|
any_failed = 1;
|
|
continue;
|
|
}
|
|
if (record_product_status(products[i].status) != 0) {
|
|
fprintf(stderr, "ww: cannot record package-test product\n");
|
|
g->pkg[root].failed = 1;
|
|
any_failed = 1;
|
|
}
|
|
}
|
|
return any_failed ? 1 : 0;
|
|
}
|
|
|
|
/* build_one_sep — thin wrapper over build_one_sep_impl. `ww build` and an
|
|
* explicit `ww test -o` retain caller-visible `.sepwork` artifacts; their
|
|
* caller owns that exact tree. `ww run` and a no-output single-file test use
|
|
* internal scratch and remove it on success and failure. One cleanup site
|
|
* covers every internal-scratch impl return. The path is nonempty only after
|
|
* this invocation successfully created the exact `.sepwork` tree. */
|
|
static int
|
|
build_one_sep(const char *src, int entry_is_dir, const char *root_identity,
|
|
const char *out,
|
|
const char *objstem, const char *extra_includes,
|
|
const struct seplinkflags *linkflags, int package_only, int is_test,
|
|
int root_variant, const char *test_package, int emit_asm,
|
|
int keepscratch, const char *workdir)
|
|
{
|
|
char scratch[1100] = {0};
|
|
struct sepgraph *g = NULL;
|
|
struct sepproduct product = {
|
|
.dir = src,
|
|
.out = out,
|
|
.identity = root_identity,
|
|
.test_package = test_package,
|
|
.status = NULL,
|
|
.artifact = {0},
|
|
.variant = root_variant,
|
|
.root = -1,
|
|
.variant_root = -1,
|
|
};
|
|
if (!package_only && !entry_is_dir)
|
|
snprintf(product.artifact, sizeof product.artifact, "__root");
|
|
int r = build_one_sep_impl(src, entry_is_dir, out, objstem,
|
|
extra_includes, linkflags, package_only, is_test,
|
|
&product, 1, emit_asm, workdir, scratch,
|
|
sizeof scratch, &g);
|
|
sep_graph_free(g);
|
|
if (!keepscratch && scratch[0]) {
|
|
size_t sl = strlen(scratch);
|
|
if (sl > 8 && strcmp(scratch + sl - 8, ".sepwork") == 0) {
|
|
int cleanrc = 1;
|
|
pid_t pid = fork();
|
|
if (pid == 0) {
|
|
execl("/bin/rm", "rm", "-rf", "--", scratch,
|
|
(char *)NULL);
|
|
_exit(127);
|
|
}
|
|
if (pid > 0) {
|
|
int status = 0;
|
|
if (waitpid(pid, &status, 0) == pid &&
|
|
WIFEXITED(status))
|
|
cleanrc = WEXITSTATUS(status);
|
|
}
|
|
if (cleanrc != 0) {
|
|
fprintf(stderr, "ww: cannot remove scratch %s\n", scratch);
|
|
if (r == 0) r = 1;
|
|
}
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
/* The package coordinator submits every selected directory/variant root in one
|
|
* request. Its first output owns the shared cold sepwork tree; every product
|
|
* remains an independent root compile and link inside that tree. */
|
|
static int
|
|
build_package_tests(const char *src, const char *root_identity,
|
|
const char *extra_includes, const char *workdir,
|
|
struct sepproduct *products, int nproducts)
|
|
{
|
|
char scratch[1100] = {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, 1,
|
|
products, nproducts, 0, workdir, scratch, sizeof scratch, &g);
|
|
sep_graph_free(g);
|
|
return r;
|
|
}
|
|
|
|
static int
|
|
do_version(void)
|
|
{
|
|
printf("ww %s\n", WW_VERSION);
|
|
return 0;
|
|
}
|
|
|
|
/* Compose the standard module search path: cwd : <extra-includes> : selected
|
|
* source library. The `extra` string is colon-separated -I dirs from argv. */
|
|
static const char *
|
|
search_path(const char *extra, char *buf, size_t bufsz)
|
|
{
|
|
const char *libdir = envpath("WW_SRCLIB");
|
|
static char libbuf[1024];
|
|
if (libdir == NULL) {
|
|
libdir = envpath("WW_LIB");
|
|
}
|
|
if (libdir == NULL) {
|
|
snprintf(libbuf, sizeof libbuf, "%s/../../lib", self_dir);
|
|
if (access(libbuf, 0) == 0) libdir = libbuf;
|
|
else if (access("lib", 0) == 0) libdir = "lib";
|
|
else {
|
|
snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir);
|
|
libdir = libbuf;
|
|
}
|
|
}
|
|
if (extra && extra[0])
|
|
snprintf(buf, bufsz, ".:%s:%s", extra, libdir);
|
|
else
|
|
snprintf(buf, bufsz, ".:%s", libdir);
|
|
return buf;
|
|
}
|
|
|
|
static void
|
|
basename_no_ext(const char *path, char *out, size_t outsz)
|
|
{
|
|
const char *base = strrchr(path, '/');
|
|
base = base ? base + 1 : path;
|
|
snprintf(out, outsz, "%s", base);
|
|
char *dot = strrchr(out, '.');
|
|
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
|
|
}
|
|
|
|
static int
|
|
resolve_module(const char *name, const char *incs, char *out, size_t outsz,
|
|
int *is_dir)
|
|
{
|
|
struct stat st;
|
|
if (stat(name, &st) == 0) {
|
|
if (S_ISREG(st.st_mode)) {
|
|
snprintf(out, outsz, "%s", name);
|
|
*is_dir = 0;
|
|
return 1;
|
|
}
|
|
if (S_ISDIR(st.st_mode)) {
|
|
snprintf(out, outsz, "%s", name);
|
|
*is_dir = 1;
|
|
return 1;
|
|
}
|
|
}
|
|
if (reserved_import_path(name)) return 0;
|
|
char sp[4096];
|
|
search_path(incs, sp, sizeof sp);
|
|
char path_form[256];
|
|
import_path_form(name, path_form, sizeof path_form);
|
|
return locate_module(sp, path_form, out, outsz, is_dir);
|
|
}
|
|
|
|
/* Returns the index past the last arg consumed for positionals (so callers
|
|
* can pick up trailing args), or -1 if a flag is missing its argument
|
|
* (diagnostic already emitted). `cmd` names the subcommand for the
|
|
* diagnostic, byte-identical to the wwstage twin's per-subcommand wording
|
|
* (selfhost/cmd/ww/main.ww dobuild/dorun). */
|
|
static int
|
|
parse_build_flags(const char *cmd, int argc, char **argv,
|
|
char *incs, size_t incsz,
|
|
struct seplinkflags *linkflags,
|
|
char *outpath, size_t outsz,
|
|
char *workdir, size_t workdirsz,
|
|
const char **src_out, int *emit_asm_out, int *package_out)
|
|
{
|
|
*src_out = NULL;
|
|
if (emit_asm_out) *emit_asm_out = 0;
|
|
if (package_out) *package_out = 0;
|
|
int i = 0;
|
|
for (; i < argc; i++) {
|
|
if (strcmp(argv[i], "-S") == 0) {
|
|
if (emit_asm_out == NULL) {
|
|
fprintf(stderr, "ww %s: unknown flag\n", cmd);
|
|
return -1;
|
|
}
|
|
*emit_asm_out = 1;
|
|
} else if (strcmp(argv[i], "-p") == 0) {
|
|
if (package_out == NULL) {
|
|
fprintf(stderr, "ww %s: unknown flag\n", cmd);
|
|
return -1;
|
|
}
|
|
*package_out = 1;
|
|
} else if (strcmp(argv[i], "-w") == 0) {
|
|
if (workdir == NULL) {
|
|
fprintf(stderr, "ww %s: unknown flag\n", cmd);
|
|
return -1;
|
|
}
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww %s: -w needs an argument\n", cmd);
|
|
return -1;
|
|
}
|
|
snprintf(workdir, workdirsz, "%s", argv[++i]);
|
|
} else if (strncmp(argv[i], "-w", 2) == 0 && argv[i][2]) {
|
|
if (workdir == NULL) {
|
|
fprintf(stderr, "ww %s: unknown flag\n", cmd);
|
|
return -1;
|
|
}
|
|
snprintf(workdir, workdirsz, "%s", argv[i] + 2);
|
|
} else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) {
|
|
if (linkflags->nlibs >= SEP_MAXLFLAGS) {
|
|
fprintf(stderr, "ww %s: too many -l\n", cmd);
|
|
return -1;
|
|
}
|
|
linkflags->libs[linkflags->nlibs++] = argv[i] + 2;
|
|
} else if (strcmp(argv[i], "-l") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww %s: -l needs an argument\n", cmd);
|
|
return -1;
|
|
}
|
|
if (linkflags->nlibs >= SEP_MAXLFLAGS) {
|
|
fprintf(stderr, "ww %s: too many -l\n", cmd);
|
|
return -1;
|
|
}
|
|
linkflags->libs[linkflags->nlibs++] = argv[++i];
|
|
} else if (strcmp(argv[i], "-L") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww %s: -L needs an argument\n", cmd);
|
|
return -1;
|
|
}
|
|
if (linkflags->nlibdirs >= SEP_MAXLFLAGS) {
|
|
fprintf(stderr, "ww %s: too many -L\n", cmd);
|
|
return -1;
|
|
}
|
|
linkflags->libdirs[linkflags->nlibdirs++] = argv[++i];
|
|
} else if (strncmp(argv[i], "-L", 2) == 0 && argv[i][2]) {
|
|
if (linkflags->nlibdirs >= SEP_MAXLFLAGS) {
|
|
fprintf(stderr, "ww %s: too many -L\n", cmd);
|
|
return -1;
|
|
}
|
|
linkflags->libdirs[linkflags->nlibdirs++] = argv[i] + 2;
|
|
} else if (strcmp(argv[i], "-I") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww %s: -I needs an argument\n", cmd);
|
|
return -1;
|
|
}
|
|
size_t n = strlen(incs);
|
|
snprintf(incs + n, incsz - n,
|
|
"%s%s", n ? ":" : "", argv[++i]);
|
|
} else if (strncmp(argv[i], "-I", 2) == 0 && argv[i][2]) {
|
|
size_t n = strlen(incs);
|
|
snprintf(incs + n, incsz - n,
|
|
"%s%s", n ? ":" : "", argv[i] + 2);
|
|
} else if (strcmp(argv[i], "-o") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww %s: -o needs an argument\n", cmd);
|
|
return -1;
|
|
}
|
|
snprintf(outpath, outsz, "%s", argv[++i]);
|
|
} else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) {
|
|
snprintf(outpath, outsz, "%s", argv[i] + 2);
|
|
} else if (argv[i][0] == '-') {
|
|
fprintf(stderr, "ww %s: unknown flag\n", cmd);
|
|
return -1;
|
|
} else if (*src_out == NULL) {
|
|
*src_out = argv[i];
|
|
} else {
|
|
break; /* leave remaining argv to caller (run-args) */
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
static int
|
|
do_build(int argc, char **argv)
|
|
{
|
|
const char *src = NULL;
|
|
struct seplinkflags linkflags = {0};
|
|
char incs[2048] = {0};
|
|
char outflag[1024] = {0};
|
|
char workdir[1024] = {0};
|
|
int emit_asm = 0;
|
|
int package_only = 0;
|
|
if (parse_build_flags("build", argc, argv, incs, sizeof incs,
|
|
&linkflags,
|
|
outflag, sizeof outflag, workdir, sizeof workdir,
|
|
&src, &emit_asm, &package_only) < 0)
|
|
return 2;
|
|
if (src == NULL) src = ".";
|
|
if (package_only && emit_asm) {
|
|
fprintf(stderr, "ww build: -p and -S cannot be combined\n");
|
|
return 2;
|
|
}
|
|
struct stat requested;
|
|
int literal = stat(src, &requested) == 0;
|
|
char resolved[1024];
|
|
int is_dir = 0;
|
|
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
|
|
fprintf(stderr, "ww build: cannot find module %s\n", src);
|
|
return 1;
|
|
}
|
|
if (package_only && !is_dir) {
|
|
fprintf(stderr, "ww build: -p needs a package directory\n");
|
|
return 2;
|
|
}
|
|
char out[1024];
|
|
const char *objstem = NULL;
|
|
if (outflag[0]) {
|
|
/* -o sets both the binary path and the intermediate stem so
|
|
* artifacts land beside the requested output (T3). */
|
|
snprintf(out, sizeof out, "%s", outflag);
|
|
objstem = out;
|
|
} else if (is_dir) {
|
|
char tmp[1024];
|
|
snprintf(tmp, sizeof tmp, "%s", resolved);
|
|
size_t n = strlen(tmp);
|
|
while (n > 1 && tmp[n-1] == '/') tmp[--n] = '\0';
|
|
const char *b = strrchr(tmp, '/');
|
|
snprintf(out, sizeof out, "%s", b ? b + 1 : tmp);
|
|
} else {
|
|
basename_no_ext(resolved, out, sizeof out);
|
|
}
|
|
const char *root_identity = !literal && is_dir ? src : NULL;
|
|
return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs,
|
|
&linkflags, package_only, 0, SEP_VARIANT_PRODUCTION, NULL, emit_asm,
|
|
1, workdir);
|
|
}
|
|
|
|
static int
|
|
do_run(int argc, char **argv)
|
|
{
|
|
const char *src = NULL;
|
|
struct seplinkflags linkflags = {0};
|
|
char incs[2048] = {0};
|
|
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
|
|
int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
|
|
&linkflags,
|
|
outflag, sizeof outflag, NULL, 0, &src, NULL, NULL);
|
|
if (next < 0) return 2;
|
|
if (src == NULL) src = ".";
|
|
struct stat requested;
|
|
int literal = stat(src, &requested) == 0;
|
|
char resolved[1024];
|
|
int is_dir = 0;
|
|
if (!resolve_module(src, incs, resolved, sizeof resolved, &is_dir)) {
|
|
fprintf(stderr, "ww run: cannot find module %s\n", src);
|
|
return 1;
|
|
}
|
|
char tmpdir[1024], tmp[1024];
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_run_%d", getpid());
|
|
if (mkdir(tmpdir, 0700) != 0) {
|
|
fprintf(stderr, "ww: cannot create temporary directory %s\n", tmpdir);
|
|
return 1;
|
|
}
|
|
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
|
|
/* The freshly acquired directory owns both the executable and the
|
|
* adjacent main.sepwork tree. Nothing outside it is adopted or removed. */
|
|
const char *root_identity = !literal && is_dir ? src : NULL;
|
|
if (build_one_sep(resolved, is_dir, root_identity, tmp, tmp, incs,
|
|
&linkflags,
|
|
0, 0, SEP_VARIANT_PRODUCTION, NULL, 0, 0, NULL) != 0) {
|
|
if (unlink(tmp) != 0 && errno != ENOENT)
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (rmdir(tmpdir) != 0)
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
return 1;
|
|
}
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
perror("ww: fork");
|
|
if (unlink(tmp) != 0 && errno != ENOENT)
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (rmdir(tmpdir) != 0)
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
return 1;
|
|
}
|
|
if (pid == 0) {
|
|
int n_extra = argc - next;
|
|
char **xargv = calloc((size_t)n_extra + 2, sizeof *xargv);
|
|
xargv[0] = tmp;
|
|
for (int i = 0; i < n_extra; i++) xargv[i+1] = argv[next + i];
|
|
xargv[n_extra+1] = NULL;
|
|
execv(tmp, xargv);
|
|
perror("ww: exec");
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
pid_t got;
|
|
do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR);
|
|
int rc = got == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 1;
|
|
if (got != pid) perror("ww: waitpid");
|
|
if (unlink(tmp) != 0 && errno != ENOENT) {
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (rc == 0) rc = 1;
|
|
}
|
|
if (rmdir(tmpdir) != 0) {
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
if (rc == 0) rc = 1;
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
static int
|
|
do_test(int argc, char **argv)
|
|
{
|
|
const char *src = NULL;
|
|
struct sepproduct products[SEP_MAXPRODUCT];
|
|
int nproducts = 0;
|
|
char incs[2048] = {0};
|
|
/* -c (Go's `go test -c`) builds the test binary without running it.
|
|
* -S + -o <stem> stops after the lib/test-inclusive package `.s`
|
|
* outputs are emitted. Both routes use build_one_sep's is_test bundle
|
|
* and T3 objstem redirect.
|
|
* -T stays internal to w6c; the driver never sees it. -l/-L carry no
|
|
* meaning for a test build, so they (and any unknown flag) are rejected
|
|
* rather than silently swallowed — byte-identical wording to the
|
|
* wwstage twin (selfhost/cmd/ww/main.ww dotest). */
|
|
int compileonly = 0;
|
|
int emit_asm = 0;
|
|
char outstem[1024] = {0};
|
|
char workdir[1024] = {0};
|
|
int packageopts = 0;
|
|
int afterdash = 0;
|
|
const char *request_identity = NULL;
|
|
/* #17: an optional second positional after the target is a fnmatch
|
|
* name-filter pattern, forwarded to the test binary as argv[1]. Only
|
|
* meaningful for a single test file/module — rejected in dir mode. */
|
|
const char *pattern = NULL;
|
|
for (int i = 0; i < argc; i++) {
|
|
if (afterdash) continue;
|
|
if (argv[i][0] == '-') {
|
|
if (strcmp(argv[i], "--") == 0) {
|
|
packageopts = 1;
|
|
afterdash = 1;
|
|
continue;
|
|
}
|
|
if (argv[i][1] == 'I') {
|
|
const char *dir;
|
|
if (argv[i][2]) {
|
|
dir = argv[i] + 2;
|
|
} else {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww test: -I needs an argument\n");
|
|
return 2;
|
|
}
|
|
dir = argv[++i];
|
|
}
|
|
size_t n = strlen(incs);
|
|
snprintf(incs + n, sizeof incs - n,
|
|
"%s%s", n ? ":" : "", dir);
|
|
} else if (strcmp(argv[i], "-c") == 0) {
|
|
compileonly = 1;
|
|
} else if (strcmp(argv[i], "--ww-root-identity") == 0) {
|
|
if (i + 1 >= argc || request_identity != NULL
|
|
|| argv[i + 1][0] == '\0'
|
|
|| reserved_import_path(argv[i + 1])) {
|
|
fprintf(stderr,
|
|
"ww test: invalid --ww-root-identity\n");
|
|
return 2;
|
|
}
|
|
request_identity = argv[++i];
|
|
} else if (strcmp(argv[i], "--ww-package-test") == 0) {
|
|
if (i + 5 >= argc || nproducts >= SEP_MAXPRODUCT) {
|
|
fprintf(stderr,
|
|
"ww test: --ww-package-test needs kind, package, directory, output, and status\n");
|
|
return 2;
|
|
}
|
|
const char *kind = argv[++i];
|
|
const char *name = argv[++i];
|
|
const char *dir = argv[++i];
|
|
const char *output = argv[++i];
|
|
const char *status = argv[++i];
|
|
size_t pn = strlen(name);
|
|
int variant = SEP_VARIANT_EXTERNAL;
|
|
if (strcmp(kind, "production") == 0)
|
|
variant = SEP_VARIANT_PRODUCTION;
|
|
else if (strcmp(kind, "same") == 0)
|
|
variant = SEP_VARIANT_SAME_TEST;
|
|
if ((strcmp(kind, "production") != 0
|
|
&& strcmp(kind, "same") != 0
|
|
&& strcmp(kind, "external") != 0)
|
|
|| pn == 0 || pn >= sizeof ((struct seppkg *)0)->name
|
|
|| dir[0] == '\0' || output[0] == '\0'
|
|
|| status[0] == '\0'
|
|
|| (strcmp(kind, "external") == 0
|
|
&& (pn <= 5
|
|
|| strcmp(name + pn - 5,
|
|
"_test") != 0))) {
|
|
fprintf(stderr,
|
|
"ww test: invalid --ww-package-test variant\n");
|
|
return 2;
|
|
}
|
|
products[nproducts].dir = dir;
|
|
products[nproducts].out = output;
|
|
products[nproducts].test_package = name;
|
|
products[nproducts].status = status;
|
|
products[nproducts].artifact[0] = '\0';
|
|
products[nproducts].variant = variant;
|
|
products[nproducts].root = -1;
|
|
products[nproducts].variant_root = -1;
|
|
nproducts++;
|
|
} else if (strcmp(argv[i], "-S") == 0) {
|
|
emit_asm = 1;
|
|
} else if (strcmp(argv[i], "-list") == 0) {
|
|
packageopts = 1;
|
|
} else if (strcmp(argv[i], "-j") == 0 ||
|
|
strcmp(argv[i], "-run") == 0 ||
|
|
strcmp(argv[i], "-filter") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr, "ww test: %s needs an argument\n",
|
|
argv[i]);
|
|
return 2;
|
|
}
|
|
packageopts = 1;
|
|
i++;
|
|
} else if (strncmp(argv[i], "-timeout-ms=", 12) == 0 &&
|
|
argv[i][12] != '\0') {
|
|
packageopts = 1;
|
|
} else if (strcmp(argv[i], "-o") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww test: -o needs an argument\n");
|
|
return 2;
|
|
}
|
|
snprintf(outstem, sizeof outstem, "%s", argv[++i]);
|
|
} else if (argv[i][1] == 'o' && argv[i][2]) {
|
|
snprintf(outstem, sizeof outstem, "%s", argv[i] + 2);
|
|
} else if (strcmp(argv[i], "-w") == 0) {
|
|
if (i + 1 >= argc) {
|
|
fprintf(stderr,
|
|
"ww test: -w needs an argument\n");
|
|
return 2;
|
|
}
|
|
snprintf(workdir, sizeof workdir, "%s", argv[++i]);
|
|
} else if (argv[i][1] == 'w' && argv[i][2]) {
|
|
snprintf(workdir, sizeof workdir, "%s", argv[i] + 2);
|
|
} else {
|
|
fprintf(stderr, "ww test: unknown flag\n");
|
|
return 2;
|
|
}
|
|
} else if (src == NULL) {
|
|
src = argv[i];
|
|
} else if (pattern == NULL) {
|
|
pattern = argv[i];
|
|
}
|
|
}
|
|
const char *target = src ? src : ".";
|
|
if (emit_asm && !outstem[0]) {
|
|
fprintf(stderr, "ww test: -S needs -o\n");
|
|
return 2;
|
|
}
|
|
for (int i = 1; i < nproducts; i++) {
|
|
struct sepproduct p = products[i];
|
|
int j = i;
|
|
while (j > 0 && (strcmp(products[j - 1].dir, p.dir) > 0
|
|
|| (strcmp(products[j - 1].dir, p.dir) == 0
|
|
&& products[j - 1].variant > p.variant))) {
|
|
products[j] = products[j - 1];
|
|
j--;
|
|
}
|
|
products[j] = p;
|
|
}
|
|
for (int i = 0; i < nproducts; i++) {
|
|
if (i > 0 && strcmp(products[i - 1].dir, products[i].dir) == 0
|
|
&& products[i - 1].variant == products[i].variant) {
|
|
fprintf(stderr,
|
|
"ww test: duplicate --ww-package-test variant for directory\n");
|
|
return 2;
|
|
}
|
|
/* Artifact identity is derived from canonical package identity after
|
|
* discovery; product position is deliberately not an action key. */
|
|
products[i].artifact[0] = '\0';
|
|
}
|
|
if (nproducts != 0 && packageopts) {
|
|
fprintf(stderr,
|
|
"ww test: package-test variant rejects package options\n");
|
|
return 2;
|
|
}
|
|
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)) {
|
|
if (nproducts != 0) {
|
|
fprintf(stderr,
|
|
"ww test: package-test variant needs one directory\n");
|
|
return 2;
|
|
}
|
|
if (emit_asm) {
|
|
fprintf(stderr,
|
|
"ww test: -S needs a single test file\n");
|
|
return 2;
|
|
}
|
|
/* -c -o forwards: the coordinator names the single
|
|
* package's artifact and rejects a multi-package fan-out. */
|
|
if (outstem[0] && !compileonly) {
|
|
fprintf(stderr,
|
|
"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);
|
|
}
|
|
struct stat st;
|
|
if (stat(target, &st) != 0) {
|
|
/* not a literal path — try module resolution and run as
|
|
* a single test program. */
|
|
char resolved[1024];
|
|
int is_dir = 0;
|
|
if (!resolve_module(target, incs, resolved, sizeof resolved,
|
|
&is_dir)) {
|
|
fprintf(stderr, "ww test: cannot find %s\n", target);
|
|
return 1;
|
|
}
|
|
if (is_dir) {
|
|
if (emit_asm) {
|
|
fprintf(stderr,
|
|
"ww test: -S needs a single test file\n");
|
|
return 2;
|
|
}
|
|
if (outstem[0] && !compileonly) {
|
|
fprintf(stderr,
|
|
"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,
|
|
"ww test: package-test products need -c\n");
|
|
return 2;
|
|
}
|
|
return build_package_tests(resolved, request_identity,
|
|
incs, workdir,
|
|
products, nproducts);
|
|
}
|
|
return exec_package_tests(argc, argv, src, resolved,
|
|
request_identity != NULL ? request_identity : target, 0);
|
|
}
|
|
if (packageopts) {
|
|
fprintf(stderr,
|
|
"ww test: package options need a directory\n");
|
|
return 2;
|
|
}
|
|
if (nproducts != 0) {
|
|
fprintf(stderr,
|
|
"ww test: package-test variant needs one directory\n");
|
|
return 2;
|
|
}
|
|
char tmpdir[1024] = {0}, tmp[1024];
|
|
const char *outp;
|
|
int owntmp = !outstem[0] && !workdir[0];
|
|
if (outstem[0]) outp = outstem;
|
|
else if (workdir[0]) {
|
|
/* The workdir owns the persistent test binary the same
|
|
* way it owns the package artifacts. */
|
|
snprintf(tmp, sizeof tmp, "%s/main", workdir);
|
|
outp = tmp;
|
|
} else {
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
|
|
if (mkdir(tmpdir, 0700) != 0) {
|
|
fprintf(stderr, "ww: cannot create temporary directory %s\n",
|
|
tmpdir);
|
|
return 1;
|
|
}
|
|
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
|
|
outp = tmp;
|
|
}
|
|
/* No-o redirects internal scratch to /tmp rather than beside the
|
|
* source. An explicit -o names the caller-owned artifact stem. */
|
|
int br = build_one_sep(resolved, is_dir, NULL, outp,
|
|
outstem[0] ? outstem : tmp, incs, NULL, 0, 1,
|
|
SEP_VARIANT_PRODUCTION, NULL, emit_asm,
|
|
outstem[0] ? 1 : 0, workdir);
|
|
if (br != 0) {
|
|
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (owntmp && rmdir(tmpdir) != 0)
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
return 1;
|
|
}
|
|
if (compileonly || emit_asm) {
|
|
int cleanfail = 0;
|
|
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
cleanfail = 1;
|
|
}
|
|
if (owntmp && rmdir(tmpdir) != 0) {
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
cleanfail = 1;
|
|
}
|
|
return cleanfail ? 1 : 0;
|
|
}
|
|
int rc = run_test_bin(outp, pattern);
|
|
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (rc == 0) rc = 1;
|
|
}
|
|
if (owntmp && rmdir(tmpdir) != 0) {
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
if (rc == 0) rc = 1;
|
|
}
|
|
return rc;
|
|
}
|
|
if (S_ISREG(st.st_mode)) {
|
|
if (nproducts != 0) {
|
|
fprintf(stderr,
|
|
"ww test: package-test variant needs one directory\n");
|
|
return 2;
|
|
}
|
|
if (packageopts) {
|
|
fprintf(stderr,
|
|
"ww test: package options need a directory\n");
|
|
return 2;
|
|
}
|
|
char tmpdir[1024] = {0}, tmp[1024];
|
|
const char *outp;
|
|
int owntmp = !outstem[0] && !workdir[0];
|
|
if (outstem[0]) outp = outstem;
|
|
else if (workdir[0]) {
|
|
snprintf(tmp, sizeof tmp, "%s/main", workdir);
|
|
outp = tmp;
|
|
} else {
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid());
|
|
if (mkdir(tmpdir, 0700) != 0) {
|
|
fprintf(stderr, "ww: cannot create temporary directory %s\n",
|
|
tmpdir);
|
|
return 1;
|
|
}
|
|
snprintf(tmp, sizeof tmp, "%s/main", tmpdir);
|
|
outp = tmp;
|
|
}
|
|
/* See module-mode note: no-o scratch is redirected to /tmp. */
|
|
int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp,
|
|
incs, NULL, 0, 1, SEP_VARIANT_PRODUCTION, NULL, emit_asm,
|
|
outstem[0] ? 1 : 0, workdir);
|
|
if (br != 0) {
|
|
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (owntmp && rmdir(tmpdir) != 0)
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
return 1;
|
|
}
|
|
if (compileonly || emit_asm) {
|
|
int cleanfail = 0;
|
|
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
cleanfail = 1;
|
|
}
|
|
if (owntmp && rmdir(tmpdir) != 0) {
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
cleanfail = 1;
|
|
}
|
|
return cleanfail ? 1 : 0;
|
|
}
|
|
int rc = run_test_bin(outp, pattern);
|
|
if (owntmp && unlink(outp) != 0 && errno != ENOENT) {
|
|
fputs("ww: cannot remove temporary output\n", stderr);
|
|
if (rc == 0) rc = 1;
|
|
}
|
|
if (owntmp && rmdir(tmpdir) != 0) {
|
|
fputs("ww: cannot remove temporary directory\n", stderr);
|
|
if (rc == 0) rc = 1;
|
|
}
|
|
return rc;
|
|
}
|
|
if (!S_ISDIR(st.st_mode)) {
|
|
fprintf(stderr, "ww test: %s is neither file nor directory\n", target);
|
|
return 1;
|
|
}
|
|
if (emit_asm) {
|
|
fprintf(stderr, "ww test: -S needs a single test file\n");
|
|
return 2;
|
|
}
|
|
if (outstem[0] && !compileonly) {
|
|
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,
|
|
"ww test: package-test products need -c\n");
|
|
return 2;
|
|
}
|
|
return build_package_tests(target, request_identity, incs, workdir,
|
|
products, nproducts);
|
|
}
|
|
return exec_package_tests(argc, argv, src, NULL, request_identity,
|
|
src == NULL);
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
if (argc >= 1) {
|
|
self_path = argv[0];
|
|
char buf[1024];
|
|
snprintf(buf, sizeof buf, "%s", argv[0]);
|
|
self_dir = strdup(dirname(buf));
|
|
}
|
|
if (argc < 2) { fputs(usage, stderr); return 2; }
|
|
const char *cmd = argv[1];
|
|
if (strcmp(cmd, "-V") == 0 || strcmp(cmd, "version") == 0)
|
|
return do_version();
|
|
if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) {
|
|
fputs(usage, stdout); return 0;
|
|
}
|
|
if (strcmp(cmd, "build") == 0) return do_build(argc - 2, argv + 2);
|
|
if (strcmp(cmd, "run") == 0) return do_run(argc - 2, argv + 2);
|
|
if (strcmp(cmd, "test") == 0) return do_test(argc - 2, argv + 2);
|
|
fprintf(stderr, "ww: unknown subcommand: %s\n", cmd);
|
|
fputs(usage, stderr);
|
|
return 2;
|
|
}
|