cmd: build local package graphs

This commit is contained in:
2026-08-11 22:26:07 +09:00
parent db96422f74
commit 52f6d6f0d4
6 changed files with 1316 additions and 510 deletions

View File

@@ -4,6 +4,7 @@
* 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>
@@ -17,7 +18,7 @@
static const char *usage =
"usage: ww [-V] <subcommand> [args...]\n"
" -V print version and exit\n"
" build [-S] [-w DIR] [-o FILE] [path] compile module; -S stops after package asm\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"
@@ -26,6 +27,7 @@ static const char *usage =
" foo.ww literal file\n"
" foo search cwd, -I dirs, then the source library for foo.ww or foo/\n"
" lib/foo directory: build its package sources\n"
" -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";
@@ -279,6 +281,16 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
snprintf(path, sizeof path, "%s/%s", dirpath, nm);
if (nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0)
continue;
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);
for (int i = 0; i < n; i++) free(arr[i]);
free(arr);
closedir(d);
*out_files = NULL;
return -2;
}
if (file_has_line_test(path)) {
fprintf(stderr,
"ww: %s: @test declaration outside *_test.ww\n",
@@ -324,15 +336,16 @@ enumerate_dir_ww(const char *dirpath, char ***out_files)
* interface can name a transitive dep's type (os exposes time.instant),
* so the consuming unit needs the whole closure for name RESOLUTION —
* direct-deps-only does not type-check. This mirrors harec reading the
* transitive `.td` closure and is consistent with the current flat-unit
* transitive-namespace model (the visibility tighten is task #45,
* deferred post-M4).
* transitive `.td` closure. The checker still scopes qualifier lookup to
* each source package's direct N_USE declarations, so carrying those type
* facts does not make a transitive package source-visible.
*/
#define SEP_MAXPKG 256
struct seppkg {
char path[256]; /* dotted import path; "" == root/primary */
char entry[1024]; /* resolved package dir (or file, for a file root) */
char canon[1024]; /* canonical location; never package identity */
char name[256]; /* validated declared name; directory packages only */
char **sources; /* owned, byte-sorted production paths; dirs only */
int nsources;
@@ -351,16 +364,54 @@ static int
sep_find_or_add(struct sepgraph *g, const char *path, const char *entry,
int is_dir)
{
for (int i = 0; i < g->n; i++)
if (strcmp(g->pkg[i].path, path) == 0) return i;
if (strlen(path) >= sizeof g->pkg[0].path) {
fprintf(stderr, "ww: package path is too long (limit %zu bytes)\n",
sizeof g->pkg[0].path - 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;
}
for (int i = 0; i < g->n; i++) {
if (strcmp(g->pkg[i].path, path) == 0) {
if (strcmp(g->pkg[i].canon, canon) != 0) {
fprintf(stderr,
"ww: package %s resolves to both %s and %s\n",
path[0] ? path : "(root)", g->pkg[i].entry,
entry);
free(canon);
return -1;
}
free(canon);
return i;
}
if (strcmp(g->pkg[i].canon, canon) == 0) {
fprintf(stderr,
"ww: package directory %s has identities %s and %s\n",
entry, g->pkg[i].path[0] ? g->pkg[i].path : "(root)",
path[0] ? path : "(root)");
free(canon);
return -1;
}
}
if (g->n >= SEP_MAXPKG) {
fprintf(stderr, "ww: too many packages (limit %d)\n",
SEP_MAXPKG);
free(canon);
return -1;
}
struct seppkg *p = &g->pkg[g->n];
snprintf(p->path, sizeof p->path, "%s", path);
snprintf(p->entry, sizeof p->entry, "%s", entry);
snprintf(p->canon, sizeof p->canon, "%s", canon);
free(canon);
p->is_dir = is_dir;
p->name[0] = '\0';
p->sources = NULL;
@@ -393,106 +444,45 @@ sep_fname(const struct sepgraph *g, int pi, const char *scratch,
snprintf(out, outsz, "%s/%s%s", scratch, base, suffix);
}
/* unit_has_package — does `path` declare `package <leaf>;` ANYWHERE?
* #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import
* from one satisfied by an INLINE package in the same unit. Unlike a
* first-package-decl scan, this checks every line — single-file
* multi-package fixtures carry several `package` decls. The
* comment-skip line scan + name match are uncapped. The
* wwstage twin unithaspackage must stay byte-identical (rule 10). */
static int
unit_has_package(const char *path, const char *leaf)
sep_slurp(const char *path, char **out, u64 *len)
{
FILE *in = fopen(path, "rb");
if (in == NULL) return 0;
char line[2048];
int found = 0;
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (p[0] == '/' && p[1] == '/') continue;
if (strncmp(p, "package ", 8) != 0
&& strncmp(p, "package\t", 8) != 0) continue;
p += 8;
while (*p == ' ' || *p == '\t') p++;
size_t i = 0;
while (leaf[i] != '\0' && leaf[i] == p[i]) i++;
if (leaf[i] == '\0') {
char c = p[i];
if (c == ';' || c == ' ' || c == '\t'
|| c == '\n' || c == '\0') { found = 1; break; }
}
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;
}
fclose(in);
return found;
}
static int
sep_ident_start(int c)
{
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_';
}
static int
sep_ident_continue(int c)
{
return sep_ident_start(c) || (c >= '0' && c <= '9');
}
/* Deliberately only the loader's small header grammar for the leading
* package clause, not a second compiler lexer. */
static int
sep_skip_space(const char *src, size_t n, size_t *off)
{
size_t i = *off;
for (;;) {
while (i < n && (src[i] == ' ' || src[i] == '\t'
|| src[i] == '\r' || src[i] == '\n'))
i++;
if (i + 1 < n && src[i] == '/' && src[i + 1] == '/') {
i += 2;
while (i < n && src[i] != '\n') i++;
continue;
}
if (i + 1 < n && src[i] == '/' && src[i + 1] == '*') {
i += 2;
while (i + 1 < n && !(src[i] == '*' && src[i + 1] == '/'))
i++;
if (i + 1 >= n) return -1;
i += 2;
continue;
}
break;
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;
}
*off = i;
buf[n] = '\0';
if (fclose(f) != 0) {
free(buf);
return -1;
}
*out = buf;
*len = (u64)n;
return 0;
}
static int
sep_package_clause(const char *src, size_t n, char *name, size_t namesz)
use_node_cmp(const void *a, const void *b)
{
static const char kw[] = "package";
size_t i = 0;
if (sep_skip_space(src, n, &i) < 0
|| i + sizeof kw - 1 >= n
|| memcmp(src + i, kw, sizeof kw - 1) != 0)
return -1;
i += sizeof kw - 1;
if (i >= n || (src[i] != ' ' && src[i] != '\t'
&& src[i] != '\r' && src[i] != '\n'))
return -1;
if (sep_skip_space(src, n, &i) < 0 || i >= n
|| !sep_ident_start((unsigned char)src[i]))
return -1;
size_t begin = i++;
while (i < n && sep_ident_continue((unsigned char)src[i])) i++;
size_t len = i - begin;
if (len + 1 > namesz || sep_skip_space(src, n, &i) < 0
|| i >= n || src[i] != ';')
return -1;
memcpy(name, src + begin, len);
name[len] = '\0';
return 0;
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;
}
/* A DIRECTORY import is a package boundary: add it as a direct dep of pkg
@@ -506,33 +496,37 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
{
if (import_seen(filevisit, file)) return 0;
import_add(filevisit, file);
FILE *in = fopen(file, "rb");
if (in == NULL) {
char *buf;
u64 len;
if (sep_slurp(file, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", file);
return -1;
}
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long flen = ftell(in);
if (flen < 0 || fseek(in, 0, SEEK_SET) != 0) {
fclose(in);
return -1;
}
char *buf = malloc((size_t)flen + 1);
if (buf == NULL) { fclose(in); return -1; }
if (fread(buf, 1, (size_t)flen, in) != (size_t)flen) {
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);
fclose(in);
return -1;
}
buf[flen] = '\0';
fclose(in);
if (owned_source) {
char declared[256];
if (sep_package_clause(buf, (size_t)flen, declared,
sizeof declared) < 0) {
fprintf(stderr, "ww: %s: invalid or missing package clause\n",
file);
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;
}
@@ -540,60 +534,97 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
if (pkg->name[0] == '\0')
snprintf(pkg->name, sizeof pkg->name, "%s", declared);
else if (strcmp(pkg->name, declared) != 0) {
fprintf(stderr,
"ww: %s: conflicting package names %s and %s\n",
pkg->entry, pkg->name, declared);
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;
for (size_t off = 0; off < (size_t)flen && rc == 0;) {
size_t end = off;
while (end < (size_t)flen && buf[end] != '\n') end++;
const char *p = buf + off;
const char *lineend = buf + end;
while (p < lineend && (*p == ' ' || *p == '\t')) p++;
if ((size_t)(lineend - p) < 7
|| (memcmp(p, "import ", 7) != 0
&& memcmp(p, "import\t", 7) != 0)) {
off = end < (size_t)flen ? end + 1 : end;
continue;
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 (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;
}
p += 7;
while (p < lineend && (*p == ' ' || *p == '\t')) p++;
char name[256] = {0};
int j = 0;
while (p < lineend && ((*p >= 'a' && *p <= 'z')
|| (*p >= 'A' && *p <= 'Z') || *p == '_' || *p == '.'
|| (*p >= '0' && *p <= '9')))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) {
off = end < (size_t)flen ? end + 1 : end;
continue;
}
char path_form[256];
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 is_dir = 0;
/* #16 ENFORCE-driver: an unresolvable import is a hard error,
* not a silent skip — EXCEPT when the package is defined INLINE
* in the same unit (single-file multi-package; leaf = the last
* dotted component). E3-C1 (#87): the legacy amalgamator that
* used to own the genuine-missing case is gone, so the sep
* producer enforces it here (INV-2, by construction). */
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir)) {
const char *dot = strrchr(name, '.');
const char *leaf = dot ? dot + 1 : name;
if (unit_has_package(file, leaf))
goto next_line; /* inline-satisfied */
fprintf(stderr, "ww: cannot find package %s\n", name);
int inline_package = 0;
for (Node *package = imports->body; package;
package = package->next)
if (strcmp(package->module, leaf) == 0) {
inline_package = 1;
break;
}
if (inline_package)
continue;
errorf(u->pos, "cannot find package %s", name);
rc = -1;
goto next_line;
break;
}
if (is_dir) {
char *canon = realpath(ipath, NULL);
if (canon == NULL) {
errorf(u->pos, "cannot canonicalize package '%s'", name);
rc = -1;
break;
}
int self = strcmp(canon, g->pkg[pi].canon) == 0;
free(canon);
if (self) {
const char *owner = g->pkg[pi].path[0]
? g->pkg[pi].path : g->pkg[pi].name;
errorf(u->pos, "self-import: package '%s' cannot import itself",
owner[0] ? owner : "(root)");
rc = -1;
break;
}
int di = sep_find_or_add(g, name, ipath, 1);
if (di < 0) { rc = -1; break; }
int seen = 0;
@@ -606,9 +637,9 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file,
} else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) {
rc = -1; break;
}
next_line:
off = end < (size_t)flen ? end + 1 : end;
}
free(uses);
freearena(a);
free(buf);
return rc;
}
@@ -659,6 +690,25 @@ sep_load_pkg(struct sepgraph *g, int pi, const char *searchpath)
for (int i = 0; i < fv.n; i++) free(fv.paths[i]);
free(fv.paths);
if (rc < 0) return rc;
/* Give a non-main root its declared identity before recursively loading
* dependencies. A back-edge can then reuse node 0 and reach the normal
* cycle detector instead of looking like a location alias. Executable
* roots are reset to the bare-root identity after loading. */
if (pi == 0 && g->pkg[pi].path[0] == '\0'
&& g->pkg[pi].name[0] != '\0') {
size_t n = strlen(g->pkg[pi].name);
memcpy(g->pkg[pi].path, g->pkg[pi].name, n + 1);
}
for (int i = 1; i < g->pkg[pi].ndeps; i++) {
int v = g->pkg[pi].deps[i];
int j = i;
while (j > 0 && strcmp(g->pkg[g->pkg[pi].deps[j - 1]].path,
g->pkg[v].path) > 0) {
g->pkg[pi].deps[j] = g->pkg[pi].deps[j - 1];
j--;
}
g->pkg[pi].deps[j] = v;
}
/* recurse into freshly-added deps (sep_find_or_add may have grown
* g->n during the scan; iterate by index). */
for (int k = 0; k < g->pkg[pi].ndeps; k++)
@@ -712,53 +762,81 @@ sep_mark_deps(struct sepgraph *g, int pi, char *inset)
* //ww:module-reset primary boundary (so -c emits its decls, imported
* ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the
* body); FILE imports fold in (intra-package split). */
static void
static int
sep_emit_body(FILE *out, const char *path, struct ImportSet *visited,
const char *searchpath, const char *modpath)
{
if (import_seen(visited, path)) return;
if (import_seen(visited, path)) return 0;
import_add(visited, path);
FILE *in = fopen(path, "rb");
if (in == NULL) {
char *buf;
u64 len;
if (sep_slurp(path, &buf, &len) < 0) {
fprintf(stderr, "ww: cannot read %s\n", path);
return;
return -1;
}
char line[2048];
while (fgets(line, sizeof line, in)) {
const char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0)
continue;
p += 7;
while (*p == ' ' || *p == '\t') p++;
char name[256] = {0};
int j = 0;
while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
|| *p == '_' || *p == '.' || (*p >= '0' && *p <= '9'))
if (j + 1 < (int)sizeof name) name[j++] = *p++;
if (j == 0) continue;
char path_form[256];
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;
}
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);
for (int i = 0; i < nuse; i++) {
Node *u = uses[i];
const char *name = u->usepath ? u->usepath : u->str;
char path_form[1024];
import_path_form(name, path_form, sizeof path_form);
char ipath[1024];
int is_dir = 0;
if (!locate_import(searchpath, path_form, ipath, sizeof ipath,
&is_dir))
continue;
if (!is_dir)
sep_emit_body(out, ipath, visited, searchpath, modpath);
if (!is_dir && sep_emit_body(out, ipath, visited, searchpath,
modpath) < 0) {
free(uses);
freearena(a);
free(buf);
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). */
if (modpath != NULL && modpath[0] != '\0')
fprintf(out, "//ww:module-reset %s\n", modpath);
else
fputs("//ww:module-reset\n", out);
rewind(in);
int ch;
while ((ch = fgetc(in)) != EOF) fputc(ch, out);
fputc('\n', out);
fclose(in);
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(uses);
freearena(a);
free(buf);
if (bad) {
fprintf(stderr, "ww: cannot write package unit\n");
return -1;
}
return 0;
}
/* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s
@@ -787,25 +865,35 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch,
fclose(u);
return -1;
}
fprintf(u, "//ww:module %s\n", g->pkg[dj].path);
int bad = fprintf(u, "//ww:module %s\n", g->pkg[dj].path) < 0;
int ch;
while ((ch = fgetc(wf)) != EOF) fputc(ch, u);
fputc('\n', u);
fclose(wf);
while (!bad && (ch = fgetc(wf)) != EOF)
if (fputc(ch, u) == EOF) bad = 1;
if (ferror(wf) || fputc('\n', u) == EOF) bad = 1;
if (fclose(wf) != 0) bad = 1;
if (bad) {
fprintf(stderr, "ww: cannot compose package unit %s\n", unitf);
fclose(u);
return -1;
}
}
struct ImportSet bodyvisit = {0};
int bodyrc = 0;
if (g->pkg[pi].is_dir) {
for (int i = 0; i < g->pkg[pi].nsources; i++)
sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit,
for (int i = 0; i < g->pkg[pi].nsources && bodyrc == 0; i++)
bodyrc = sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit,
searchpath, g->pkg[pi].path);
} else {
sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath,
bodyrc = sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath,
g->pkg[pi].path);
}
for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]);
free(bodyvisit.paths);
fclose(u);
return 0;
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
@@ -824,16 +912,18 @@ archive_o(const char *objpath, const char *apath)
fprintf(stderr, "ww: cannot read %s\n", objpath);
return -1;
}
fseek(in, 0, SEEK_END);
if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; }
long n = ftell(in);
fseek(in, 0, SEEK_SET);
if (n < 0) { fclose(in); return -1; }
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;
}
fclose(in);
if (fclose(in) != 0) { free(buf); return -1; }
FILE *out = fopen(apath, "wb");
if (out == NULL) {
@@ -841,7 +931,7 @@ archive_o(const char *objpath, const char *apath)
free(buf);
return -1;
}
fwrite("!<arch>\n", 1, 8, out);
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];
@@ -853,14 +943,20 @@ archive_o(const char *objpath, const char *apath)
memcpy(hdr + 40, "100644", 6); /* mode (fixed octal) */
char sz[12];
int szn = snprintf(sz, sizeof sz, "%lu", (unsigned long)n);
memcpy(hdr + 48, sz, (size_t)szn);
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;
fwrite(hdr, 1, sizeof hdr, out);
fwrite(buf, 1, (size_t)n, out);
if (n & 1) fputc('\n', out); /* members are 2-byte aligned */
fclose(out);
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;
}
@@ -958,10 +1054,12 @@ workdir_stamp_text(char *buf, size_t bufsz, int is_test, int emit_asm)
* 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,
build_one_sep_impl(const char *src, int entry_is_dir,
const char *root_identity, const char *out,
const char *objstem, const char *extra_includes, const char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, const char *workdir,
char *scratchout, size_t scratchoutsz, struct sepgraph **graphout)
const char *extra_libdirs, int package_only, int is_test, int emit_asm,
const char *workdir, char *scratchout, size_t scratchoutsz,
struct sepgraph **graphout)
{
const char *c6 = toolpath("WW_W6C", "w6c");
const char *a6 = toolpath("WW_W6A", "w6a");
@@ -1061,7 +1159,9 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
struct sepgraph *g = calloc(1, sizeof *g);
if (g == NULL) return 1;
if (graphout) *graphout = g;
int root = sep_find_or_add(g, "", src, entry_is_dir);
const char *rootpath = package_only && root_identity
? root_identity : "";
int root = sep_find_or_add(g, rootpath, src, entry_is_dir);
if (root < 0) return 1;
/* #79 (-T): lib/test is the synth main's `test.run` callee but @test
* files never `import test;`. Inject it as a direct dep of the root so
@@ -1082,6 +1182,11 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
}
}
if (sep_load_pkg(g, root, srcdir) < 0) return 1;
int root_package = package_only;
if (root_package && strcmp(g->pkg[root].name, "main") == 0) {
fprintf(stderr, "ww: -p requires a non-main package\n");
return 1;
}
for (int i = 0; i < g->n; i++) g->pkg[i].color = 0;
int *order = calloc((size_t)g->n, sizeof *order);
int *stack = calloc((size_t)g->n, sizeof *stack);
@@ -1091,6 +1196,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
free(stack); free(order); return 1;
}
free(stack);
if (!root_package) g->pkg[root].path[0] = '\0';
for (int oi = 0; oi < norder; oi++) {
int pi = order[oi];
@@ -1114,13 +1220,15 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
const char *cs = warm ? asmnew : asmf;
const char *co = warm ? objnew : obj;
const char *ca = warm ? anew : apath;
int needs_export = pi != root || root_package;
int needs_archive = pi != root || root_package;
if (sep_compose_unit(g, pi, scratch, order, norder, srcdir,
cu) < 0) { free(order); return 1; }
if (warm && !stale_all && file_equal(unitnew, unitf)
&& file_is_reg(asmf)
&& (pi == root || file_is_reg(wwi))
&& (!needs_export || file_is_reg(wwi))
&& (emit_asm || (file_size_nonzero(obj)
&& (pi == root || file_size_nonzero(apath))))) {
&& (!needs_archive || file_size_nonzero(apath))))) {
if (unlink(unitnew) != 0) {
fprintf(stderr, "ww: cannot remove %s\n",
unitnew);
@@ -1135,7 +1243,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* LOCAL type (the root is never imported), which the
* export-check rejects. Skip -I for the root; its `.wwi`
* is never consumed. */
if (pi == root)
if (!needs_export)
/* #79: the root carries -T under `ww test`
* so w6c synthesizes the test main. Deps never
* get -T. */
@@ -1162,7 +1270,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* the build target, always fully linked), so `main` is defined
* before any archive is processed. The link consumes `.o`/`.a`,
* never `.wwi`. */
if (!emit_asm && pi != root) {
if (!emit_asm && needs_archive) {
if (archive_o(co, ca) != 0) {
fprintf(stderr, "ww: archive failed for %s\n",
g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)");
@@ -1172,10 +1280,10 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
/* Commit order: artifacts before the unit that vouches for
* them, unit strictly last. */
if (warm) {
if ((pi != root && rename(wwinew, wwi) != 0)
if ((needs_export && rename(wwinew, wwi) != 0)
|| rename(asmnew, asmf) != 0
|| (!emit_asm && rename(objnew, obj) != 0)
|| (!emit_asm && pi != root
|| (!emit_asm && needs_archive
&& rename(anew, apath) != 0)
|| rename(unitnew, unitf) != 0) {
fprintf(stderr, "ww: cannot commit %s\n",
@@ -1212,6 +1320,20 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
}
}
if (emit_asm) { free(order); return 0; }
if (root_package) {
char archive[1024], iface[1024], outiface[1100];
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;
}
/* reverse-topo link: root `.o` first (order[norder-1], force-loaded),
* then transitive dep `.a` in reverse-topo order, then libwwrt.a —
@@ -1256,15 +1378,16 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out,
* 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 *out,
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 char *extra_libs,
const char *extra_libdirs, int is_test, int emit_asm, int keepscratch,
const char *workdir)
const char *extra_libdirs, int package_only, int is_test, int emit_asm,
int keepscratch, const char *workdir)
{
char scratch[1100] = {0};
struct sepgraph *g = NULL;
int r = build_one_sep_impl(src, entry_is_dir, out, objstem,
extra_includes, extra_libs, extra_libdirs, is_test, emit_asm,
int r = build_one_sep_impl(src, entry_is_dir, root_identity, out, objstem,
extra_includes, extra_libs, extra_libdirs, package_only, is_test, emit_asm,
workdir, scratch, sizeof scratch, &g);
sep_graph_free(g);
if (!keepscratch && scratch[0]) {
@@ -1371,10 +1494,11 @@ parse_build_flags(const char *cmd, int argc, char **argv,
char *libs, size_t libsz,
char *outpath, size_t outsz,
char *workdir, size_t workdirsz,
const char **src_out, int *emit_asm_out)
const char **src_out, int *emit_asm_out, int *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) {
@@ -1383,6 +1507,12 @@ parse_build_flags(const char *cmd, int argc, char **argv,
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);
@@ -1470,18 +1600,29 @@ do_build(int argc, char **argv)
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,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, workdir, sizeof workdir,
&src, &emit_asm) < 0)
&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]) {
@@ -1499,8 +1640,9 @@ do_build(int argc, char **argv)
} else {
basename_no_ext(resolved, out, sizeof out);
}
return build_one_sep(resolved, is_dir, out, objstem, incs, libs,
libdirs, 0, emit_asm, 1, workdir);
const char *root_identity = package_only && !literal ? src : NULL;
return build_one_sep(resolved, is_dir, root_identity, out, objstem, incs, libs,
libdirs, package_only, 0, emit_asm, 1, workdir);
}
static int
@@ -1513,7 +1655,7 @@ do_run(int argc, char **argv)
char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */
int next = parse_build_flags("run", argc, argv, incs, sizeof incs,
libdirs, sizeof libdirs, libs, sizeof libs,
outflag, sizeof outflag, NULL, 0, &src, NULL);
outflag, sizeof outflag, NULL, 0, &src, NULL, NULL);
if (next < 0) return 2;
if (src == NULL) src = ".";
char resolved[1024];
@@ -1531,8 +1673,8 @@ do_run(int argc, char **argv)
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. */
if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, 0,
0, NULL) != 0) {
if (build_one_sep(resolved, is_dir, NULL, tmp, tmp, incs, libs, libdirs,
0, 0, 0, 0, NULL) != 0) {
if (unlink(tmp) != 0 && errno != ENOENT)
fputs("ww: cannot remove temporary output\n", stderr);
if (rmdir(tmpdir) != 0)
@@ -1754,8 +1896,8 @@ do_test(int argc, char **argv)
}
/* 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, outp,
outstem[0] ? outstem : tmp, incs, "", "", 1,
int br = build_one_sep(resolved, is_dir, NULL, outp,
outstem[0] ? outstem : tmp, incs, "", "", 0, 1,
emit_asm, outstem[0] ? 1 : 0, workdir);
if (br != 0) {
if (owntmp && unlink(outp) != 0 && errno != ENOENT)
@@ -1811,8 +1953,8 @@ do_test(int argc, char **argv)
outp = tmp;
}
/* See module-mode note: no-o scratch is redirected to /tmp. */
int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp,
incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0, workdir);
int br = build_one_sep(target, 0, NULL, outp, outstem[0] ? outstem : tmp,
incs, "", "", 0, 1, 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);