cmd: build local package graphs
This commit is contained in:
606
cmd/ww/main.c
606
cmd/ww/main.c
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user