2886 lines
79 KiB
Plaintext
2886 lines
79 KiB
Plaintext
package wwpackage;
|
|
|
|
import os;
|
|
import os.exec;
|
|
import strconv;
|
|
import strings;
|
|
import temp;
|
|
import time;
|
|
|
|
type pkgsource = struct {
|
|
path: str,
|
|
dir: str,
|
|
pkg: str,
|
|
test: bool,
|
|
};
|
|
|
|
type pkgfolder = struct {
|
|
path: str,
|
|
start: i32,
|
|
end: i32,
|
|
prodpkg: str,
|
|
};
|
|
|
|
type pkggroup = struct {
|
|
dir: str,
|
|
pkg: str,
|
|
basename: str,
|
|
testname: str,
|
|
publish: str,
|
|
prodpkg: str,
|
|
samepkg: str,
|
|
externalpkg: str,
|
|
hassame: bool,
|
|
hasexternal: bool,
|
|
notests: bool,
|
|
production: bool,
|
|
plan: i32,
|
|
root: str,
|
|
bin: str,
|
|
buildok: str,
|
|
runoutput: str,
|
|
installoutput: str,
|
|
state: i32,
|
|
runstartfailed: bool,
|
|
runres: exec.result,
|
|
publicbin: bool,
|
|
installattempted: bool,
|
|
installres: exec.result,
|
|
};
|
|
|
|
type pkgplan = struct {
|
|
dir: str,
|
|
identity: str,
|
|
root: str,
|
|
workdir: str,
|
|
outputdir: str,
|
|
buildout: str,
|
|
builderr: str,
|
|
start: i32,
|
|
end: i32,
|
|
state: i32,
|
|
buildres: exec.result,
|
|
buildonly: bool,
|
|
publish: bool,
|
|
emitasm: bool,
|
|
outputpatherror: bool,
|
|
defaultoutputdir: str,
|
|
outputcollisionbase: str,
|
|
outputcollisiondir: str,
|
|
suppressbuildreports: bool,
|
|
deferpublish: bool,
|
|
};
|
|
|
|
def PKG_COUNT_MAX: i32 = 2147483647;
|
|
def PKG_INITIAL_CAP: i32 = 8;
|
|
|
|
// strings.concat/dup abort on allocation failure. Request selection and plan
|
|
// construction instead keep every new allocation fallible and diagnostic.
|
|
fn pkgstring(out: *str, values: str...) bool = {
|
|
let total: i64 = 0i64;
|
|
let i: i32 = 0;
|
|
for (i < values.len) {
|
|
total += values[i].len: i64;
|
|
if (total > PKG_COUNT_MAX: i64) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
i += 1;
|
|
};
|
|
if (total == 0i64) { *out = ""; return true; };
|
|
let allocation: ([]u8 | nomem) = pkgallocbytes(total: i32);
|
|
let bytes: []u8;
|
|
match (allocation) {
|
|
case let value: []u8 => bytes = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < values.len) {
|
|
let j: i32 = 0;
|
|
for (j < values[i].len) {
|
|
append(bytes, values[i][j]);
|
|
j += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
*out = strings.frombytes(bytes);
|
|
return true;
|
|
};
|
|
|
|
fn pkgallocstrs(cap: i32) ([]str | nomem) = {
|
|
let value: []str = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgalloci32(cap: i32) ([]i32 | nomem) = {
|
|
let value: []i32 = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgallocbytes(cap: i32) ([]u8 | nomem) = {
|
|
let value: []u8 = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgallocsources(cap: i32) ([]pkgsource | nomem) = {
|
|
let value: []pkgsource = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgallocfolders(cap: i32) ([]pkgfolder | nomem) = {
|
|
let value: []pkgfolder = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgallocgroups(cap: i32) ([]pkggroup | nomem) = {
|
|
let value: []pkggroup = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgallocplans(cap: i32) ([]pkgplan | nomem) = {
|
|
let value: []pkgplan = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
fn pkgallocprocesses(cap: i32) ([]exec.process | nomem) = {
|
|
let value: []exec.process = alloc([], cap: u64)?;
|
|
return value;
|
|
};
|
|
|
|
// Product and directory-plan process states for the bounded coordinator.
|
|
def PKGQUEUED: i32 = 0;
|
|
def PKGBUILDING: i32 = 1;
|
|
def PKGRUNNING: i32 = 2;
|
|
def PKGDONE: i32 = 3;
|
|
|
|
def pkgpoll: time.duration = 1000000i64: time.duration;
|
|
|
|
type pkgdiscover = struct {
|
|
paths: []str,
|
|
errors: i32,
|
|
fatal: bool,
|
|
};
|
|
|
|
fn pkggrowcap(current: i32, need: i32) i32 = {
|
|
if (need < 0) { return -1; };
|
|
if (need <= current) { return current; };
|
|
let cap: i32 = current;
|
|
if (cap == 0) { cap = PKG_INITIAL_CAP; };
|
|
for (cap < need) {
|
|
if (cap > PKG_COUNT_MAX / 2) {
|
|
cap = PKG_COUNT_MAX;
|
|
break;
|
|
};
|
|
cap *= 2;
|
|
};
|
|
if (cap < need) { return -1; };
|
|
return cap;
|
|
};
|
|
|
|
fn pkgappenddiscovered(st: *pkgdiscover, path: str) bool = {
|
|
if (st.paths.len == PKG_COUNT_MAX) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
st.errors += 1;
|
|
st.fatal = true;
|
|
return false;
|
|
};
|
|
let need: i32 = st.paths.len + 1;
|
|
if (need > st.paths.cap) {
|
|
let cap: i32 = pkggrowcap(st.paths.cap, need);
|
|
if (cap < 0) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
st.errors += 1;
|
|
st.fatal = true;
|
|
return false;
|
|
};
|
|
let allocation: ([]str | nomem) = pkgallocstrs(cap);
|
|
let next: []str;
|
|
match (allocation) {
|
|
case let value: []str => next = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: out of memory");
|
|
st.errors += 1;
|
|
st.fatal = true;
|
|
return false;
|
|
};
|
|
};
|
|
let n: i32 = st.paths.len;
|
|
next.len = cap;
|
|
let i: i32 = 0;
|
|
for (i < n) { next[i] = st.paths[i]; i += 1; };
|
|
next.len = n;
|
|
if (st.paths.ptr != nil) {
|
|
os.free(st.paths.ptr: *void,
|
|
(st.paths.cap: u64) * (size(str): u64));
|
|
};
|
|
st.paths = next;
|
|
};
|
|
append(st.paths, path);
|
|
return true;
|
|
};
|
|
|
|
// Preserve the caller's toolchain environment while pinning the locale and
|
|
// temporary directory used by the current build plan.
|
|
fn toolenv(tmpdir: str, out: *[]str) bool = {
|
|
let inherited: []str = os.getenvs();
|
|
if (inherited.len > PKG_COUNT_MAX - 2) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 2);
|
|
let env: []str;
|
|
match (allocation) {
|
|
case let value: []str => env = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
let i: i32 = 0;
|
|
for (i < inherited.len) {
|
|
if (!strings.hasprefix(inherited[i], "TMPDIR=")
|
|
&& !strings.hasprefix(inherited[i], "LC_ALL=")) {
|
|
append(env, inherited[i]);
|
|
};
|
|
i += 1;
|
|
};
|
|
append(env, "LC_ALL=C");
|
|
let tmpenv: str;
|
|
if (!pkgstring(&tmpenv, "TMPDIR=", tmpdir)) { return false; };
|
|
append(env, tmpenv);
|
|
*out = env;
|
|
return true;
|
|
};
|
|
|
|
fn pkgfreeownedstr(value: str) void = {
|
|
if (value.ptr != nil && value.len != 0) {
|
|
os.free(value.ptr: *void, value.len: u64);
|
|
};
|
|
};
|
|
|
|
fn pkgfreestrs(values: []str) void = {
|
|
if (values.ptr != nil && values.cap != 0) {
|
|
os.free(values.ptr: *void,
|
|
(values.cap: u64) * (size(str): u64));
|
|
};
|
|
};
|
|
|
|
fn pkgenvkeylen(entry: str) i32 = {
|
|
let i: i32 = 0;
|
|
for (i < entry.len) {
|
|
if (entry[i] == '=') { return i; };
|
|
i += 1;
|
|
};
|
|
return -1;
|
|
};
|
|
|
|
fn pkgenvkeyequal(a: str, an: i32, b: str, bn: i32) bool = {
|
|
if (an != bn) { return false; };
|
|
let i: i32 = 0;
|
|
for (i < an) {
|
|
if (a[i] != b[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn pkgenvkeyis(entry: str, n: i32, name: str) bool = {
|
|
return pkgenvkeyequal(entry, n, name, name.len);
|
|
};
|
|
|
|
fn pkgenvhash(entry: str, n: i32) u64 = {
|
|
let h: u64 = 14695981039346656037u64;
|
|
let i: i32 = 0;
|
|
for (i < n) {
|
|
h = h ^ (entry[i]: u64);
|
|
h = h * 1099511628211u64;
|
|
i += 1;
|
|
};
|
|
return h;
|
|
};
|
|
|
|
fn pkgenvtablecap(entries: i32) i32 = {
|
|
if (entries == 0) { return 0; };
|
|
if (entries > PKG_COUNT_MAX / 2) { return -1; };
|
|
let need: i32 = entries * 2;
|
|
let cap: i32 = PKG_INITIAL_CAP;
|
|
for (cap < need) {
|
|
if (cap > PKG_COUNT_MAX / 2) { return -1; };
|
|
cap *= 2;
|
|
};
|
|
return cap;
|
|
};
|
|
|
|
// Go snapshots user-test variables through Unix os.Environ before appending
|
|
// PATH and PWD. WW's raw environment walker and executor do no such duplicate
|
|
// normalization, so the launch owner must materialize that snapshot itself.
|
|
fn pkgenvfirst(slots: []i32, inherited: []str, at: i32, n: i32) bool = {
|
|
let slot: i32 = (pkgenvhash(inherited[at], n)
|
|
% (slots.len: u64)): i32;
|
|
let probes: i32 = 0;
|
|
for (probes < slots.len) {
|
|
let prior: i32 = slots[slot];
|
|
if (prior < 0) {
|
|
slots[slot] = at;
|
|
return true;
|
|
};
|
|
let priorn: i32 = pkgenvkeylen(inherited[prior]);
|
|
if (pkgenvkeyequal(inherited[at], n,
|
|
inherited[prior], priorn)) {
|
|
return false;
|
|
};
|
|
slot += 1;
|
|
if (slot == slots.len) { slot = 0; };
|
|
probes += 1;
|
|
};
|
|
return false;
|
|
};
|
|
|
|
// Go's appended PATH and PWD survive os/exec's later-value selection. WW's
|
|
// executor preserves its concrete vector, so inherited copies are excluded.
|
|
fn runenv(pwd: str, builder: str, out: *[]str, pathowned: *str,
|
|
pwdowned: *str) bool = {
|
|
let toolbin: str;
|
|
let oom: bool = false;
|
|
if (!pkgcanonicaldir(pkgdirname(builder), &toolbin, &oom)) {
|
|
if (!oom) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: cannot determine toolchain directory");
|
|
};
|
|
return false;
|
|
};
|
|
let inherited: []str = os.getenvs();
|
|
if (inherited.len > PKG_COUNT_MAX - 2) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
let allocation: ([]str | nomem) = pkgallocstrs(inherited.len + 2);
|
|
let env: []str;
|
|
match (allocation) {
|
|
case let value: []str => env = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
let keyed: i32 = 0;
|
|
let i: i32 = 0;
|
|
for (i < inherited.len) {
|
|
let n: i32 = pkgenvkeylen(inherited[i]);
|
|
if (n >= 0 && !pkgenvkeyis(inherited[i], n, "PATH")
|
|
&& !pkgenvkeyis(inherited[i], n, "PWD")) {
|
|
keyed += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
let tablecap: i32 = pkgenvtablecap(keyed);
|
|
if (tablecap < 0) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
pkgfreestrs(env);
|
|
return false;
|
|
};
|
|
let slots: []i32;
|
|
slots.ptr = nil: *i32;
|
|
slots.len = 0;
|
|
slots.cap = 0;
|
|
if (tablecap != 0) {
|
|
let tableallocation: ([]i32 | nomem) = pkgalloci32(tablecap);
|
|
match (tableallocation) {
|
|
case let value: []i32 => slots = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: out of memory");
|
|
pkgfreestrs(env);
|
|
return false;
|
|
};
|
|
};
|
|
slots.len = tablecap;
|
|
i = 0;
|
|
for (i < slots.len) { slots[i] = -1; i += 1; };
|
|
};
|
|
i = 0;
|
|
for (i < inherited.len) {
|
|
let n: i32 = pkgenvkeylen(inherited[i]);
|
|
if (n < 0) {
|
|
if (inherited[i].len != 0) { append(env, inherited[i]); };
|
|
} else if (!pkgenvkeyis(inherited[i], n, "PATH")
|
|
&& !pkgenvkeyis(inherited[i], n, "PWD")
|
|
&& pkgenvfirst(slots, inherited, i, n)) {
|
|
append(env, inherited[i]);
|
|
};
|
|
i += 1;
|
|
};
|
|
if (slots.ptr != nil) {
|
|
os.free(slots.ptr: *void,
|
|
(slots.cap: u64) * (size(i32): u64));
|
|
};
|
|
let pathenv: str;
|
|
let pathok: bool = false;
|
|
match (os.getenv("PATH")) {
|
|
case let inheritedpath: str => {
|
|
if (inheritedpath.len == 0) {
|
|
pathok = pkgstring(&pathenv, "PATH=", toolbin);
|
|
} else {
|
|
pathok = pkgstring(&pathenv, "PATH=", toolbin, ":",
|
|
inheritedpath);
|
|
};
|
|
};
|
|
case void => pathok = pkgstring(&pathenv, "PATH=", toolbin);
|
|
};
|
|
if (!pathok) {
|
|
pkgfreestrs(env);
|
|
return false;
|
|
};
|
|
let pwdenv: str;
|
|
if (!pkgstring(&pwdenv, "PWD=", pwd)) {
|
|
pkgfreeownedstr(pathenv);
|
|
pkgfreestrs(env);
|
|
return false;
|
|
};
|
|
append(env, pathenv);
|
|
append(env, pwdenv);
|
|
*out = env;
|
|
*pathowned = pathenv;
|
|
*pwdowned = pwdenv;
|
|
return true;
|
|
};
|
|
|
|
fn freerunenv(env: []str, pathowned: str, pwdowned: str) void = {
|
|
pkgfreeownedstr(pathowned);
|
|
pkgfreeownedstr(pwdowned);
|
|
pkgfreestrs(env);
|
|
};
|
|
|
|
fn pkgwrite(fd: i32, s: str) bool = {
|
|
let r: (i64 | os.oserror) = os.writeall(fd, s.ptr, s.len: u64);
|
|
match (r) {
|
|
case let n: i64 => return n == s.len: i64;
|
|
case let e: os.oserror => return false;
|
|
};
|
|
};
|
|
|
|
fn pkgput(fd: i32, s: str) void = {
|
|
pkgwrite(fd, s);
|
|
};
|
|
|
|
fn pkgputln(fd: i32, s: str) void = {
|
|
pkgput(fd, s);
|
|
pkgput(fd, "\n");
|
|
};
|
|
|
|
// Go's final test-status action is command-owned: after an explicit ordinary
|
|
// package request records a setup, build, or run failure, it prints one final
|
|
// FAIL after the ordered package results. Implicit current-directory testing
|
|
// and compile-only testing deliberately do not use this action.
|
|
fn pkgteststatusfail(explicitstatus: bool, compileonly: bool) int = {
|
|
if (explicitstatus && !compileonly) {
|
|
pkgputln(os.STDOUT_FILENO, "FAIL");
|
|
};
|
|
return 1;
|
|
};
|
|
|
|
fn pkgputhexbyte(fd: i32, value: u8) void = {
|
|
let digits: str = "0123456789abcdef";
|
|
let encoded: [2]u8;
|
|
let high: i32 = (value / 16u8): i32;
|
|
let low: i32 = (value % 16u8): i32;
|
|
encoded[0] = digits[high];
|
|
encoded[1] = digits[low];
|
|
os.write(fd, &encoded[0], 2u64);
|
|
};
|
|
|
|
fn pkgputhexrune(fd: i32, value: u32, digits: i32) void = {
|
|
let alphabet: str = "0123456789abcdef";
|
|
let encoded: [8]u8;
|
|
let i: i32 = digits - 1;
|
|
for (i >= 0) {
|
|
let shift: u32 = (i: u32) * 4u32;
|
|
let d: i32 = ((value >> shift) & 15u32): i32;
|
|
encoded[digits - 1 - i] = alphabet[d];
|
|
i -= 1;
|
|
};
|
|
os.write(fd, &encoded[0], digits: u64);
|
|
};
|
|
|
|
fn pkgutf8width(value: str, index: i32) i32 = {
|
|
let c: u8 = value[index];
|
|
if (c < 128u8) { return 1; };
|
|
if (c >= 194u8 && c <= 223u8 && index + 1 < value.len
|
|
&& value[index + 1] >= 128u8 && value[index + 1] <= 191u8) {
|
|
return 2;
|
|
};
|
|
if (index + 2 < value.len
|
|
&& value[index + 2] >= 128u8 && value[index + 2] <= 191u8) {
|
|
let c1: u8 = value[index + 1];
|
|
if ((c == 224u8 && c1 >= 160u8 && c1 <= 191u8)
|
|
|| (c >= 225u8 && c <= 236u8 && c1 >= 128u8 && c1 <= 191u8)
|
|
|| (c == 237u8 && c1 >= 128u8 && c1 <= 159u8)
|
|
|| (c >= 238u8 && c <= 239u8 && c1 >= 128u8 && c1 <= 191u8)) {
|
|
return 3;
|
|
};
|
|
};
|
|
if (index + 3 < value.len
|
|
&& value[index + 2] >= 128u8 && value[index + 2] <= 191u8
|
|
&& value[index + 3] >= 128u8 && value[index + 3] <= 191u8) {
|
|
let c1: u8 = value[index + 1];
|
|
if ((c == 240u8 && c1 >= 144u8 && c1 <= 191u8)
|
|
|| (c >= 241u8 && c <= 243u8 && c1 >= 128u8 && c1 <= 191u8)
|
|
|| (c == 244u8 && c1 >= 128u8 && c1 <= 143u8)) {
|
|
return 4;
|
|
};
|
|
};
|
|
return 1;
|
|
};
|
|
|
|
// Decode one filesystem-name rune with the same malformed-byte behavior as
|
|
// utf8.DecodeRuneInString: each malformed byte is one U+FFFD rune.
|
|
fn pkgutf8rune(value: str, index: i32) u32 = {
|
|
let width: i32 = pkgutf8width(value, index);
|
|
let c0: u32 = value[index]: u32;
|
|
if (width == 1) {
|
|
if (c0 >= 128u32) { return 0xfffdu32; };
|
|
return c0;
|
|
};
|
|
let c1: u32 = value[index + 1]: u32;
|
|
if (width == 2) {
|
|
return ((c0 & 31u32) << 6u32) | (c1 & 63u32);
|
|
};
|
|
let c2: u32 = value[index + 2]: u32;
|
|
if (width == 3) {
|
|
return ((c0 & 15u32) << 12u32)
|
|
| ((c1 & 63u32) << 6u32) | (c2 & 63u32);
|
|
};
|
|
let c3: u32 = value[index + 3]: u32;
|
|
return ((c0 & 7u32) << 18u32) | ((c1 & 63u32) << 12u32)
|
|
| ((c2 & 63u32) << 6u32) | (c3 & 63u32);
|
|
};
|
|
|
|
fn pkgvalidutf8(value: str) bool = {
|
|
let i: i32 = 0;
|
|
for (i < value.len) {
|
|
let width: i32 = pkgutf8width(value, i);
|
|
if (value[i] >= 128u8 && width == 1) { return false; };
|
|
i += width;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
// Go's unmatched-pattern diagnostic uses strconv.Quote. Filesystem arguments
|
|
// are emitted byte-exactly for Go-printable UTF-8, with Go escapes for
|
|
// controls, non-printing runes, and malformed bytes.
|
|
fn pkgputquoted(fd: i32, value: str) void = {
|
|
pkgput(fd, "\"");
|
|
let i: i32 = 0;
|
|
for (i < value.len) {
|
|
let c: u8 = value[i];
|
|
let width: i32 = pkgutf8width(value, i);
|
|
if (c >= 128u8 && width == 1) {
|
|
pkgput(fd, "\\x"); pkgputhexbyte(fd, c); i += 1; continue;
|
|
};
|
|
if (width > 1) {
|
|
let r: u32 = pkgutf8rune(value, i);
|
|
if (pkgisprint(r)) {
|
|
os.write(fd, value.ptr + (i: u64), width: u64);
|
|
} else if (r < 0x10000u32) {
|
|
pkgput(fd, "\\u"); pkgputhexrune(fd, r, 4);
|
|
} else {
|
|
pkgput(fd, "\\U"); pkgputhexrune(fd, r, 8);
|
|
};
|
|
i += width;
|
|
continue;
|
|
};
|
|
if (c == '"') { pkgput(fd, "\\\""); }
|
|
else { if (c == '\\') { pkgput(fd, "\\\\"); }
|
|
else { if (c == 7u8) { pkgput(fd, "\\a"); }
|
|
else { if (c == 8u8) { pkgput(fd, "\\b"); }
|
|
else { if (c == 12u8) { pkgput(fd, "\\f"); }
|
|
else { if (c == '\n') { pkgput(fd, "\\n"); }
|
|
else { if (c == '\r') { pkgput(fd, "\\r"); }
|
|
else { if (c == '\t') { pkgput(fd, "\\t"); }
|
|
else { if (c == 11u8) { pkgput(fd, "\\v"); }
|
|
else { if (c < 32u8 || c == 127u8) {
|
|
pkgput(fd, "\\x"); pkgputhexbyte(fd, c);
|
|
} else { os.write(fd, &value[i], 1u64); };
|
|
};};};};};};};};};
|
|
i += 1;
|
|
};
|
|
pkgput(fd, "\"");
|
|
};
|
|
|
|
fn pkgfailpath(path: str, reason: str) void = {
|
|
pkgput(os.STDERR_FILENO, "wwtest package: ");
|
|
pkgput(os.STDERR_FILENO, path);
|
|
pkgput(os.STDERR_FILENO, ": ");
|
|
pkgputln(os.STDERR_FILENO, reason);
|
|
};
|
|
|
|
fn pkgfailsource(path: str, line: i32, col: i32, reason: str) void = {
|
|
pkgput(os.STDERR_FILENO, path);
|
|
pkgput(os.STDERR_FILENO, ":");
|
|
pkgput(os.STDERR_FILENO, strconv.i32tos(line, strconv.base.DEC));
|
|
pkgput(os.STDERR_FILENO, ":");
|
|
pkgput(os.STDERR_FILENO, strconv.i32tos(col, strconv.base.DEC));
|
|
pkgput(os.STDERR_FILENO, ": error: ");
|
|
pkgputln(os.STDERR_FILENO, reason);
|
|
};
|
|
|
|
fn pkgusage() void = {
|
|
pkgput(os.STDERR_FILENO,
|
|
"usage: wwtest package [-c] [-S] [-list] [-j N] [-I DIR] [-L DIR] [-l LIB] [-w DIR] [-run|-filter GLOB] [-timeout-ms=N] [DIR | DIR/... ...] [-- GLOB ...]\n");
|
|
pkgput(os.STDERR_FILENO,
|
|
" *_test.ww is the sole test-source form; @test elsewhere is rejected\n");
|
|
pkgput(os.STDERR_FILENO,
|
|
" -c retains without running; -o retains and still runs unless -c is present\n");
|
|
pkgput(os.STDERR_FILENO,
|
|
" -w DIR is one persistent semantic-action store shared by the selected packages\n");
|
|
};
|
|
|
|
fn pkgread(path: str, out: *str) bool = {
|
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
|
if (fd < 0) { return false; };
|
|
let sr: (i64 | os.oserror) = os.filesize(fd);
|
|
let n: i64 = -1i64;
|
|
match (sr) {
|
|
case let v: i64 => n = v;
|
|
case let e: os.oserror => { os.close(fd); return false; };
|
|
};
|
|
if (n < 0i64 || n >= PKG_COUNT_MAX: i64) {
|
|
os.close(fd);
|
|
if (n >= PKG_COUNT_MAX: i64) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
};
|
|
return false;
|
|
};
|
|
let allocation: ([]u8 | nomem) = pkgallocbytes((n + 1i64): i32);
|
|
let b: []u8;
|
|
match (allocation) {
|
|
case let value: []u8 => b = value;
|
|
case nomem => {
|
|
os.close(fd);
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
b.len = (n + 1i64): i32;
|
|
let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64);
|
|
os.close(fd);
|
|
let got: i64 = -1i64;
|
|
match (rr) {
|
|
case let v: i64 => got = v;
|
|
case let e: os.oserror => return false;
|
|
};
|
|
if (got != n) { return false; };
|
|
let ni: i32 = n: i32;
|
|
b[ni] = 0u8;
|
|
out.ptr = b.ptr;
|
|
out.len = n: i32;
|
|
return true;
|
|
};
|
|
|
|
fn pkgidentfirst(c: u8) bool = {
|
|
if (c >= 'a' && c <= 'z') { return true; };
|
|
if (c >= 'A' && c <= 'Z') { return true; };
|
|
return c == '_';
|
|
};
|
|
|
|
fn pkgident(c: u8) bool = {
|
|
if (pkgidentfirst(c)) { return true; };
|
|
return c >= '0' && c <= '9';
|
|
};
|
|
|
|
fn pkgnulerrors(path: str, src: str) bool = {
|
|
let i: i32 = 0;
|
|
let ln: i32 = 1;
|
|
let cl: i32 = 1;
|
|
let found: bool = false;
|
|
for (i < src.len) {
|
|
if (src[i] == 0u8) {
|
|
pkgfailsource(path, ln, cl, "invalid NUL character");
|
|
found = true;
|
|
};
|
|
if (src[i] == '\n') {
|
|
ln += 1;
|
|
cl = 1;
|
|
} else {
|
|
cl += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
return found;
|
|
};
|
|
|
|
fn pkgskipspace(src: str, start: i32) i32 = {
|
|
let i: i32 = start;
|
|
for (i < src.len) {
|
|
let c: u8 = src[i];
|
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (c == '/' && i + 1 < src.len && src[i + 1] == '/') {
|
|
i += 2;
|
|
for (i < src.len && src[i] != '\n') { i += 1; };
|
|
continue;
|
|
};
|
|
// The driver's sep_skip_space (cmd/ww/main.c) also skips
|
|
// /* */ before the package clause; without this arm a source
|
|
// opening with a block comment built under ww but failed
|
|
// coordinator discovery ("invalid or missing package
|
|
// clause"). An unterminated comment runs to EOF and the
|
|
// clause parse fails loud.
|
|
if (c == '/' && i + 1 < src.len && src[i + 1] == '*') {
|
|
i += 2;
|
|
for (i + 1 < src.len
|
|
&& !(src[i] == '*' && src[i + 1] == '/')) {
|
|
i += 1;
|
|
};
|
|
if (i + 1 >= src.len) { return src.len; };
|
|
i += 2;
|
|
continue;
|
|
};
|
|
break;
|
|
};
|
|
return i;
|
|
};
|
|
|
|
fn pkgclause(src: str, out: *str) bool = {
|
|
// Match the compiler readers: one UTF-8 BOM is invisible only at the
|
|
// first raw source position. The full stage driver owns later-BOM errors.
|
|
let start: i32 = 0;
|
|
if (src.len >= 3 && src[0] == 0xefu8 && src[1] == 0xbbu8
|
|
&& src[2] == 0xbfu8) { start = 3; };
|
|
let i: i32 = pkgskipspace(src, start);
|
|
let word: str = "package";
|
|
if (i + word.len >= src.len) { return false; };
|
|
let j: i32 = 0;
|
|
for (j < word.len) {
|
|
if (src[i + j] != word[j]) { return false; };
|
|
j += 1;
|
|
};
|
|
i += word.len;
|
|
if (i >= src.len) { return false; };
|
|
if (!(src[i] == ' ' || src[i] == '\t' || src[i] == '\n' || src[i] == '\r')) {
|
|
return false;
|
|
};
|
|
i = pkgskipspace(src, i);
|
|
if (i >= src.len || !pkgidentfirst(src[i])) { return false; };
|
|
let begin: i32 = i;
|
|
i += 1;
|
|
for (i < src.len && pkgident(src[i])) { i += 1; };
|
|
let end: i32 = i;
|
|
i = pkgskipspace(src, i);
|
|
if (i >= src.len || src[i] != ';') { return false; };
|
|
out.ptr = src.ptr + (begin: u64);
|
|
out.len = end - begin;
|
|
return true;
|
|
};
|
|
|
|
fn pkgdirname(path: str) str = {
|
|
let i: i32 = path.len - 1;
|
|
for (i >= 0) {
|
|
if (path[i] == '/') {
|
|
if (i == 0) { return "/"; };
|
|
let r: str;
|
|
r.ptr = path.ptr;
|
|
r.len = i;
|
|
return r;
|
|
};
|
|
i -= 1;
|
|
};
|
|
return ".";
|
|
};
|
|
|
|
fn pkgbase(path: str) str = {
|
|
let i: i32 = path.len - 1;
|
|
for (i >= 0) {
|
|
if (path[i] == '/') {
|
|
let r: str;
|
|
r.ptr = path.ptr + ((i + 1): u64);
|
|
r.len = path.len - i - 1;
|
|
return r;
|
|
};
|
|
i -= 1;
|
|
};
|
|
return path;
|
|
};
|
|
|
|
// Directory test binaries are presentation artifacts. Their Go-like visible
|
|
// basename comes from the selected canonical import spelling, never from the
|
|
// declared package name or any test variant. A physical root is the fallback
|
|
// when the request has no explicit logical identity.
|
|
fn pkgimportbase(path: str) str = {
|
|
let i: i32 = path.len - 1;
|
|
for (i >= 0) {
|
|
if (path[i] == '.') {
|
|
let r: str;
|
|
r.ptr = path.ptr + ((i + 1): u64);
|
|
r.len = path.len - i - 1;
|
|
return r;
|
|
};
|
|
i -= 1;
|
|
};
|
|
return path;
|
|
};
|
|
|
|
fn pkgisabs(path: str) bool = {
|
|
return path.len != 0 && path[0] == '/';
|
|
};
|
|
|
|
fn pkgmodeis(m: os.mode, want: os.mode) bool = {
|
|
return (((m: u32) & 61440u32) == (want: u32));
|
|
};
|
|
|
|
fn pkgisdir(path: str) bool = {
|
|
let fi: os.filestat;
|
|
match (os.lstat(&fi, path)) {
|
|
case void => return pkgmodeis(fi.mode, os.mode.DIR);
|
|
case let e: os.oserror => return false;
|
|
};
|
|
};
|
|
|
|
fn pkgstatdir(path: str) bool = {
|
|
let fi: os.filestat;
|
|
match (os.stat(&fi, path)) {
|
|
case void => return pkgmodeis(fi.mode, os.mode.DIR);
|
|
case let e: os.oserror => return false;
|
|
};
|
|
};
|
|
|
|
fn pkgoutputdir(path: str) bool = {
|
|
return pkgstatdir(path) || strings.hassuffix(path, "/");
|
|
};
|
|
|
|
fn pkgjoinpath(dir: str, name: str, out: *str) bool = {
|
|
if (strings.hassuffix(dir, "/")) { return pkgstring(out, dir, name); };
|
|
return pkgstring(out, dir, "/", name);
|
|
};
|
|
|
|
fn pkgisreg(path: str) bool = {
|
|
let fi: os.filestat;
|
|
match (os.lstat(&fi, path)) {
|
|
case void => return pkgmodeis(fi.mode, os.mode.REG);
|
|
case let e: os.oserror => return false;
|
|
};
|
|
};
|
|
|
|
fn pkgnamerangeis(name: str, start: i32, end: i32, word: str) bool = {
|
|
if (end < start || end - start != word.len) { return false; };
|
|
let i: i32 = 0;
|
|
for (i < end - start) {
|
|
if (name[start + i] != word[i]) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn pkgknownos(name: str, start: i32, end: i32) bool = {
|
|
return pkgnamerangeis(name, start, end, "aix")
|
|
|| pkgnamerangeis(name, start, end, "android")
|
|
|| pkgnamerangeis(name, start, end, "darwin")
|
|
|| pkgnamerangeis(name, start, end, "dragonfly")
|
|
|| pkgnamerangeis(name, start, end, "freebsd")
|
|
|| pkgnamerangeis(name, start, end, "hurd")
|
|
|| pkgnamerangeis(name, start, end, "illumos")
|
|
|| pkgnamerangeis(name, start, end, "ios")
|
|
|| pkgnamerangeis(name, start, end, "js")
|
|
|| pkgnamerangeis(name, start, end, "linux")
|
|
|| pkgnamerangeis(name, start, end, "nacl")
|
|
|| pkgnamerangeis(name, start, end, "netbsd")
|
|
|| pkgnamerangeis(name, start, end, "openbsd")
|
|
|| pkgnamerangeis(name, start, end, "plan9")
|
|
|| pkgnamerangeis(name, start, end, "solaris")
|
|
|| pkgnamerangeis(name, start, end, "wasip1")
|
|
|| pkgnamerangeis(name, start, end, "windows")
|
|
|| pkgnamerangeis(name, start, end, "zos");
|
|
};
|
|
|
|
fn pkgknownarch(name: str, start: i32, end: i32) bool = {
|
|
return pkgnamerangeis(name, start, end, "386")
|
|
|| pkgnamerangeis(name, start, end, "amd64")
|
|
|| pkgnamerangeis(name, start, end, "amd64p32")
|
|
|| pkgnamerangeis(name, start, end, "arm")
|
|
|| pkgnamerangeis(name, start, end, "armbe")
|
|
|| pkgnamerangeis(name, start, end, "arm64")
|
|
|| pkgnamerangeis(name, start, end, "arm64be")
|
|
|| pkgnamerangeis(name, start, end, "loong64")
|
|
|| pkgnamerangeis(name, start, end, "mips")
|
|
|| pkgnamerangeis(name, start, end, "mipsle")
|
|
|| pkgnamerangeis(name, start, end, "mips64")
|
|
|| pkgnamerangeis(name, start, end, "mips64le")
|
|
|| pkgnamerangeis(name, start, end, "mips64p32")
|
|
|| pkgnamerangeis(name, start, end, "mips64p32le")
|
|
|| pkgnamerangeis(name, start, end, "ppc")
|
|
|| pkgnamerangeis(name, start, end, "ppc64")
|
|
|| pkgnamerangeis(name, start, end, "ppc64le")
|
|
|| pkgnamerangeis(name, start, end, "riscv")
|
|
|| pkgnamerangeis(name, start, end, "riscv64")
|
|
|| pkgnamerangeis(name, start, end, "s390")
|
|
|| pkgnamerangeis(name, start, end, "s390x")
|
|
|| pkgnamerangeis(name, start, end, "sparc")
|
|
|| pkgnamerangeis(name, start, end, "sparc64")
|
|
|| pkgnamerangeis(name, start, end, "wasm");
|
|
};
|
|
|
|
fn pkgtargettag(name: str, start: i32, end: i32) bool = {
|
|
return pkgnamerangeis(name, start, end, "linux")
|
|
|| pkgnamerangeis(name, start, end, "amd64");
|
|
};
|
|
|
|
// Recursive discovery must decide platform eligibility before a source can
|
|
// enter any production or test package graph.
|
|
fn pkgsourceplatform(name: str) bool = {
|
|
let stem: i32 = 0;
|
|
let hasunderscore: bool = false;
|
|
for (stem < name.len && name[stem] != '.') {
|
|
if (name[stem] == '_') { hasunderscore = true; };
|
|
stem += 1;
|
|
};
|
|
if (!hasunderscore) { return true; };
|
|
|
|
let end: i32 = stem;
|
|
let last: i32 = end;
|
|
for (last > 0 && name[last - 1] != '_') { last -= 1; };
|
|
if (pkgnamerangeis(name, last, end, "test")) {
|
|
if (last == 0) { return true; };
|
|
end = last - 1;
|
|
last = end;
|
|
for (last > 0 && name[last - 1] != '_') { last -= 1; };
|
|
};
|
|
if (last == 0) { return true; };
|
|
|
|
let prevend: i32 = last - 1;
|
|
let prev: i32 = prevend;
|
|
for (prev > 0 && name[prev - 1] != '_') { prev -= 1; };
|
|
if (prev < prevend && pkgknownos(name, prev, prevend)
|
|
&& pkgknownarch(name, last, end)) {
|
|
return pkgtargettag(name, last, end)
|
|
&& pkgtargettag(name, prev, prevend);
|
|
};
|
|
if (pkgknownos(name, last, end) || pkgknownarch(name, last, end)) {
|
|
return pkgtargettag(name, last, end);
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn pkgkeepfile(name: str) bool = {
|
|
if (name.len <= 3 || name[0] == '.' || name[0] == '_') { return false; };
|
|
return strings.hassuffix(name, ".ww") && pkgsourceplatform(name);
|
|
};
|
|
|
|
type pkgmatcher = struct {
|
|
full: str,
|
|
base: str,
|
|
fullcap: u64,
|
|
basecap: u64,
|
|
trailing: bool,
|
|
valid: bool,
|
|
};
|
|
|
|
fn pkgsortstrings(ss: []str) void = {
|
|
let i: i32 = 1;
|
|
for (i < ss.len) {
|
|
let j: i32 = i;
|
|
for (j > 0 && strings.compare(ss[j - 1], ss[j]) > 0) {
|
|
let value: str = ss[j];
|
|
ss[j] = ss[j - 1];
|
|
ss[j - 1] = value;
|
|
j -= 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
fn pkgcleanpath(spelling: str, out: *str) bool = {
|
|
if (spelling.len > PKG_COUNT_MAX - 2) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
let componentallocation: ([]str | nomem) =
|
|
pkgallocstrs(spelling.len + 1);
|
|
let components: []str;
|
|
match (componentallocation) {
|
|
case let value: []str => components = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
let absolute: bool = spelling.len != 0 && spelling[0] == '/';
|
|
let start: i32 = 0;
|
|
for (start <= spelling.len) {
|
|
let end: i32 = start;
|
|
for (end < spelling.len && spelling[end] != '/') { end += 1; };
|
|
let component: str = spelling[start:end];
|
|
if (component.len == 0 || strings.compare(component, ".") == 0) {
|
|
void;
|
|
} else if (strings.compare(component, "..") == 0) {
|
|
if (components.len != 0
|
|
&& strings.compare(components[components.len - 1], "..") != 0) {
|
|
components.len -= 1;
|
|
} else if (!absolute) {
|
|
append(components, component);
|
|
};
|
|
} else {
|
|
append(components, component);
|
|
};
|
|
if (end == spelling.len) { break; };
|
|
start = end + 1;
|
|
};
|
|
let byteallocation: ([]u8 | nomem) = pkgallocbytes(spelling.len + 2);
|
|
let cleaned: []u8;
|
|
match (byteallocation) {
|
|
case let value: []u8 => cleaned = value;
|
|
case nomem => {
|
|
os.free(components.ptr: *void,
|
|
((spelling.len + 1): u64) * (size(str): u64));
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
if (absolute) { append(cleaned, '/': u8); };
|
|
let i: i32 = 0;
|
|
for (i < components.len) {
|
|
if (i != 0) { append(cleaned, '/': u8); };
|
|
let j: i32 = 0;
|
|
for (j < components[i].len) {
|
|
append(cleaned, components[i][j]);
|
|
j += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
if (cleaned.len == 0) { append(cleaned, '.': u8); };
|
|
*out = strings.frombytes(cleaned);
|
|
os.free(components.ptr: *void,
|
|
((spelling.len + 1): u64) * (size(str): u64));
|
|
return true;
|
|
};
|
|
|
|
fn pkgfirstellipsis(value: str) i32 = {
|
|
let i: i32 = 0;
|
|
for (i + 2 < value.len) {
|
|
if (value[i] == '.' && value[i + 1] == '.'
|
|
&& value[i + 2] == '.') { return i; };
|
|
i += 1;
|
|
};
|
|
return -1;
|
|
};
|
|
|
|
// Non-terminal vendor elements become a byte that a wildcard cannot consume.
|
|
// Explicit vendor elements receive the same byte and therefore still match.
|
|
fn pkgvendorform(value: str, out: *str, capacity: *u64) bool = {
|
|
*capacity = value.len: u64;
|
|
if (value.len == 0) { *out = ""; return true; };
|
|
let allocation: ([]u8 | nomem) = pkgallocbytes(value.len);
|
|
let encoded: []u8;
|
|
match (allocation) {
|
|
case let bytes: []u8 => encoded = bytes;
|
|
case nomem => return false;
|
|
};
|
|
let start: i32 = 0;
|
|
for (start < value.len) {
|
|
let end: i32 = start;
|
|
for (end < value.len && value[end] != '/') { end += 1; };
|
|
if (end < value.len && end - start == 6
|
|
&& strings.compare(value[start:end], "vendor") == 0) {
|
|
append(encoded, 0u8);
|
|
} else {
|
|
let i: i32 = start;
|
|
for (i < end) { append(encoded, value[i]); i += 1; };
|
|
};
|
|
if (end == value.len) { break; };
|
|
append(encoded, '/': u8);
|
|
start = end + 1;
|
|
};
|
|
*out = strings.frombytes(encoded);
|
|
return true;
|
|
};
|
|
|
|
fn pkgfreeform(value: str, capacity: u64) void = {
|
|
if (value.ptr != nil && capacity != 0u64) {
|
|
os.free(value.ptr: *void, capacity);
|
|
};
|
|
};
|
|
|
|
fn pkgglob(pattern: str, name: str) bool = {
|
|
let pi: i32 = 0;
|
|
let ni: i32 = 0;
|
|
let star: i32 = -1;
|
|
let resume: i32 = 0;
|
|
for (ni < name.len) {
|
|
if (pi + 2 < pattern.len && pattern[pi] == '.'
|
|
&& pattern[pi + 1] == '.' && pattern[pi + 2] == '.') {
|
|
star = pi;
|
|
pi += 3;
|
|
resume = ni;
|
|
continue;
|
|
};
|
|
if (pi < pattern.len
|
|
&& pkgutf8rune(pattern, pi) == pkgutf8rune(name, ni)) {
|
|
pi += pkgutf8width(pattern, pi);
|
|
ni += pkgutf8width(name, ni);
|
|
continue;
|
|
};
|
|
if (star >= 0 && resume < name.len && name[resume] != 0u8
|
|
&& name[resume] != '\n') {
|
|
resume += pkgutf8width(name, resume);
|
|
ni = resume;
|
|
pi = star + 3;
|
|
continue;
|
|
};
|
|
return false;
|
|
};
|
|
for (pi + 2 < pattern.len && pattern[pi] == '.'
|
|
&& pattern[pi + 1] == '.' && pattern[pi + 2] == '.') {
|
|
pi += 3;
|
|
};
|
|
return pi == pattern.len;
|
|
};
|
|
|
|
fn pkgmakematcher(pattern: str, out: *pkgmatcher) bool = {
|
|
out.full = "";
|
|
out.base = "";
|
|
out.fullcap = 0u64;
|
|
out.basecap = 0u64;
|
|
out.trailing = strings.hassuffix(pattern, "/...");
|
|
out.valid = pkgvalidutf8(pattern);
|
|
if (!out.valid) { return true; };
|
|
if (!pkgvendorform(pattern, &out.full, &out.fullcap)) { return false; };
|
|
if (out.trailing && !pkgvendorform(pattern[0:pattern.len - 4],
|
|
&out.base, &out.basecap)) {
|
|
pkgfreeform(out.full, out.fullcap);
|
|
return false;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn pkgfreematcher(m: *pkgmatcher) void = {
|
|
pkgfreeform(m.full, m.fullcap);
|
|
pkgfreeform(m.base, m.basecap);
|
|
};
|
|
|
|
fn pkgmatchdir(m: *pkgmatcher, name: str, out: *bool) bool = {
|
|
if (!m.valid) { *out = false; return true; };
|
|
// pkgcleanpath removes a leading ./ from a relative pattern. Descent from
|
|
// its implicit "." traversal root produces ./child spellings, so compare
|
|
// both values in the same cleaned coordinate system.
|
|
if (strings.hasprefix(name, "./") && name.len > 2) { name = name[2:name.len]; };
|
|
let candidate: str;
|
|
let capacity: u64;
|
|
if (!pkgvendorform(name, &candidate, &capacity)) { return false; };
|
|
*out = pkgglob(m.full, candidate)
|
|
|| (m.trailing && pkgglob(m.base, candidate));
|
|
pkgfreeform(candidate, capacity);
|
|
return true;
|
|
};
|
|
|
|
fn pkgexcluded(name: str) bool = {
|
|
return name.len != 0 && strings.compare(name, ".") != 0
|
|
&& strings.compare(name, "..") != 0
|
|
&& (name[0] == '.' || name[0] == '_'
|
|
|| strings.compare(name, "testdata") == 0);
|
|
};
|
|
|
|
// A wildcard walk follows an explicitly selected root symlink but never a
|
|
// child symlink. Selection is separate from descent: vendor subtrees are
|
|
// walked, while the matcher prevents an implicit wildcard from consuming a
|
|
// non-terminal vendor element.
|
|
fn pkgdiscoverdir(pathname: str, st: *pkgdiscover,
|
|
matcher: *pkgmatcher) void = {
|
|
if (st.fatal) { return; };
|
|
let rootstat: os.filestat;
|
|
match (os.lstat(&rootstat, pathname)) {
|
|
case void => void;
|
|
case let e: os.oserror => {
|
|
pkgfailpath(pathname, "cannot stat package directory");
|
|
st.errors += 1;
|
|
return;
|
|
};
|
|
};
|
|
if (pkgmodeis(rootstat.mode, os.mode.LINK)) {
|
|
match (os.stat(&rootstat, pathname)) {
|
|
case void => void;
|
|
case let e: os.oserror => {
|
|
pkgfailpath(pathname, "cannot stat package directory");
|
|
st.errors += 1;
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
if (!pkgmodeis(rootstat.mode, os.mode.DIR)) {
|
|
pkgfailpath(pathname, "expected one package directory");
|
|
st.errors += 1;
|
|
return;
|
|
};
|
|
let selected: bool = matcher == nil;
|
|
if (matcher != nil && !pkgmatchdir(matcher, pathname, &selected)) {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
st.errors += 1;
|
|
st.fatal = true;
|
|
return;
|
|
};
|
|
let fd: i32 = os.open(pathname, os.flag.RDONLY, 0i32);
|
|
if (fd < 0) {
|
|
pkgfailpath(pathname, "cannot open directory");
|
|
st.errors += 1;
|
|
return;
|
|
};
|
|
let bufallocation: ([]u8 | nomem) = pkgallocbytes(8192);
|
|
let buf: []u8;
|
|
match (bufallocation) {
|
|
case let value: []u8 => buf = value;
|
|
case nomem => {
|
|
os.close(fd);
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
st.errors += 1;
|
|
st.fatal = true;
|
|
return;
|
|
};
|
|
};
|
|
buf.len = 8192;
|
|
let names: pkgdiscover;
|
|
let emptynames: []str;
|
|
names.paths = emptynames;
|
|
names.errors = 0;
|
|
names.fatal = false;
|
|
let n: i64 = os.getdents64(fd, buf.ptr, 8192u64);
|
|
for (n > 0i64) {
|
|
let off: u64 = 0u64;
|
|
for (off < n: u64) {
|
|
let reclen: u64 = (buf[off + 16u64]: u64)
|
|
+ (buf[off + 17u64]: u64) * 256u64;
|
|
if (reclen < 20u64 || off + reclen > n: u64) {
|
|
pkgfailpath(pathname, "malformed directory entry");
|
|
st.errors += 1;
|
|
off = n: u64;
|
|
continue;
|
|
};
|
|
let np: *u8 = buf.ptr + off + 19u64;
|
|
let nl: i32 = 0;
|
|
for (nl: u64 + 19u64 < reclen && np[nl] != 0u8) { nl += 1; };
|
|
let name: str;
|
|
name.ptr = np;
|
|
name.len = nl;
|
|
if (!(strings.compare(name, ".") == 0
|
|
|| strings.compare(name, "..") == 0)) {
|
|
let ownedname: str;
|
|
if (!pkgstring(&ownedname, name)) {
|
|
st.errors += 1;
|
|
st.fatal = true;
|
|
os.close(fd);
|
|
return;
|
|
};
|
|
if (!pkgappenddiscovered(&names, ownedname)) {
|
|
st.errors += names.errors;
|
|
st.fatal = names.fatal;
|
|
os.close(fd);
|
|
return;
|
|
};
|
|
};
|
|
off += reclen;
|
|
};
|
|
n = os.getdents64(fd, buf.ptr, 8192u64);
|
|
};
|
|
if (n < 0i64) {
|
|
pkgfailpath(pathname, "directory read failed");
|
|
st.errors += 1;
|
|
};
|
|
os.close(fd);
|
|
pkgsortstrings(names.paths);
|
|
let ni: i32 = 0;
|
|
for (ni < names.paths.len) {
|
|
let name: str = names.paths[ni];
|
|
let child: str;
|
|
if (!pkgstring(&child, pathname, "/", name)) {
|
|
st.errors += 1; st.fatal = true; return;
|
|
};
|
|
let fi: os.filestat;
|
|
match (os.lstat(&fi, child)) {
|
|
case void => {
|
|
if (pkgmodeis(fi.mode, os.mode.DIR)) {
|
|
if (matcher != nil && !pkgexcluded(name)) {
|
|
pkgdiscoverdir(child, st, matcher);
|
|
if (st.fatal) { return; };
|
|
};
|
|
} else if (selected && pkgkeepfile(name)) {
|
|
let sourceisregular: bool = pkgmodeis(fi.mode, os.mode.REG);
|
|
if (pkgmodeis(fi.mode, os.mode.LINK)) {
|
|
let target: os.filestat;
|
|
match (os.stat(&target, child)) {
|
|
case void => {
|
|
// Match go/build: a file symlink is a source under
|
|
// its directory-entry name; a directory target is not.
|
|
sourceisregular = pkgmodeis(target.mode, os.mode.REG);
|
|
};
|
|
case let e: os.oserror => {
|
|
pkgfailpath(child, "cannot stat source");
|
|
st.errors += 1;
|
|
};
|
|
};
|
|
};
|
|
if (sourceisregular && !pkgappenddiscovered(st, child)) {
|
|
return;
|
|
};
|
|
};
|
|
};
|
|
case let e: os.oserror => {
|
|
if (selected && pkgkeepfile(name)) {
|
|
pkgfailpath(child, "cannot stat source");
|
|
} else {
|
|
pkgfailpath(child, "cannot stat directory entry");
|
|
};
|
|
st.errors += 1;
|
|
};
|
|
};
|
|
ni += 1;
|
|
};
|
|
};
|
|
|
|
// Group paths by containing directory before filename order. A plain full-path
|
|
// sort can interleave a child directory between two files in its parent and
|
|
// split one package into multiple folder records during recursive discovery.
|
|
fn pkgsort(ss: []str) void = {
|
|
let i: i32 = 1;
|
|
for (i < ss.len) {
|
|
let j: i32 = i;
|
|
for (j > 0) {
|
|
let a: str = pkgdirname(ss[j - 1]);
|
|
let b: str = pkgdirname(ss[j]);
|
|
let c: int = strings.compare(a, b);
|
|
if (c == 0) { c = strings.compare(ss[j - 1], ss[j]); };
|
|
if (c <= 0) { break; };
|
|
let t: str = ss[j];
|
|
ss[j] = ss[j - 1];
|
|
ss[j - 1] = t;
|
|
j -= 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
fn pkgsortgroups(gs: []pkggroup) void = {
|
|
let i: i32 = 1;
|
|
for (i < gs.len) {
|
|
let j: i32 = i;
|
|
for (j > 0) {
|
|
let c: int = strings.compare(gs[j - 1].dir, gs[j].dir);
|
|
if (c == 0) { c = strings.compare(gs[j - 1].pkg, gs[j].pkg); };
|
|
if (c <= 0) { break; };
|
|
let t: pkggroup = gs[j];
|
|
gs[j] = gs[j - 1];
|
|
gs[j - 1] = t;
|
|
j -= 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
fn pkgdedup(ss: []str) []str = {
|
|
if (ss.len == 0) { return ss; };
|
|
let n: i32 = 1;
|
|
let i: i32 = 1;
|
|
for (i < ss.len) {
|
|
if (strings.compare(ss[n - 1], ss[i]) != 0) {
|
|
ss[n] = ss[i];
|
|
n += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
ss.len = n;
|
|
return ss;
|
|
};
|
|
|
|
fn pkgparsedec(s: str, max: i64) i64 = {
|
|
if (s.len == 0) { return -1i64; };
|
|
let i: i32 = 0;
|
|
let n: i64 = 0i64;
|
|
for (i < s.len) {
|
|
if (s[i] < '0' || s[i] > '9') { return -1i64; };
|
|
let digit: i64 = (s[i] - '0'): i64;
|
|
if (n > (max - digit) / 10i64) { return -1i64; };
|
|
n = n * 10i64 + digit;
|
|
i += 1;
|
|
};
|
|
return n;
|
|
};
|
|
|
|
fn pkgdefaultbuilder(out: *str) bool = {
|
|
let av: []str = os.args();
|
|
if (av.len == 0 || av[0].len == 0) { *out = "ww"; return true; };
|
|
return pkgstring(out, pkgdirname(av[0]), "/ww");
|
|
};
|
|
|
|
fn pkgmakedir(path: str) bool = {
|
|
return os.mkdir(path, 448i32) == 0;
|
|
};
|
|
|
|
// Resolve a directory to the kernel's symlink-free absolute spelling. The
|
|
// coordinator is single-threaded; each temporary cwd change is restored before
|
|
// it plans further paths or launches another child.
|
|
fn pkgcanonicaldir(path: str, out: *str, oom: *bool) bool = {
|
|
*oom = false;
|
|
let beforeallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX);
|
|
let before: []u8;
|
|
match (beforeallocation) {
|
|
case let value: []u8 => before = value;
|
|
case nomem => {
|
|
*oom = true;
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
before.len = os.PATH_MAX;
|
|
let bn: i64 = os.getcwd(before.ptr, before.len: u64);
|
|
if (bn <= 1i64 || bn > before.len: i64) { return false; };
|
|
if (os.chdir(path) != 0) { return false; };
|
|
let afterallocation: ([]u8 | nomem) = pkgallocbytes(os.PATH_MAX);
|
|
let after: []u8;
|
|
match (afterallocation) {
|
|
case let value: []u8 => after = value;
|
|
case nomem => {
|
|
*oom = true;
|
|
let restored: i32 = os.chdir(strings.frombytes(
|
|
before[0:(bn - 1i64): i32]));
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
after.len = os.PATH_MAX;
|
|
let an: i64 = os.getcwd(after.ptr, after.len: u64);
|
|
let restored: i32 = os.chdir(strings.frombytes(before[0:(bn - 1i64): i32]));
|
|
if (restored != 0 || an <= 1i64 || an > after.len: i64) { return false; };
|
|
after.len = (an - 1i64): i32;
|
|
*out = strings.frombytes(after);
|
|
return true;
|
|
};
|
|
|
|
// The coordinator removes only its minted temp root; validating the opened
|
|
// inode keeps a replaced path from carrying cleanup outside that ownership.
|
|
fn pkgremoveall(path: str) bool = {
|
|
let st: os.filestat;
|
|
match (os.lstat(&st, path)) {
|
|
case void => void;
|
|
case let e: os.oserror => return false;
|
|
};
|
|
if (!pkgmodeis(st.mode, os.mode.DIR)) { return os.remove(path) == 0; };
|
|
let fd: i32 = os.open(path, os.flag.RDONLY, 0i32);
|
|
if (fd < 0) { return false; };
|
|
let opened: os.filestat;
|
|
match (os.fstat(&opened, fd)) {
|
|
case void => void;
|
|
case let e: os.oserror => { os.close(fd); return false; };
|
|
};
|
|
if (!pkgmodeis(opened.mode, os.mode.DIR)
|
|
|| opened.inode != st.inode) { os.close(fd); return false; };
|
|
let ok: bool = true;
|
|
let allocation: ([]u8 | nomem) = pkgallocbytes(8192);
|
|
let buf: []u8;
|
|
match (allocation) {
|
|
case let value: []u8 => buf = value;
|
|
case nomem => { os.close(fd); return false; };
|
|
};
|
|
buf.len = 8192;
|
|
let n: i64 = os.getdents64(fd, buf.ptr, 8192u64);
|
|
for (n > 0i64) {
|
|
let off: u64 = 0u64;
|
|
for (off < n: u64) {
|
|
let reclen: u64 = (buf[off + 16u64]: u64)
|
|
+ (buf[off + 17u64]: u64) * 256u64;
|
|
if (reclen < 20u64 || off + reclen > n: u64) {
|
|
ok = false;
|
|
off = n: u64;
|
|
continue;
|
|
};
|
|
let np: *u8 = buf.ptr + off + 19u64;
|
|
let nl: i32 = 0;
|
|
for (nl: u64 + 19u64 < reclen && np[nl] != 0u8) { nl += 1; };
|
|
let name: str;
|
|
name.ptr = np;
|
|
name.len = nl;
|
|
if (!(strings.compare(name, ".") == 0 || strings.compare(name, "..") == 0)) {
|
|
let child: str;
|
|
if (!pkgstring(&child, path, "/", name)) {
|
|
ok = false; off = n: u64; continue;
|
|
};
|
|
let dtype: u8 = buf[off + 18u64];
|
|
if (dtype == 4u8 || (dtype == 0u8 && pkgisdir(child))) {
|
|
if (!pkgremoveall(child)) { ok = false; };
|
|
} else if (os.remove(child) != 0) { ok = false; };
|
|
};
|
|
off += reclen;
|
|
};
|
|
n = os.getdents64(fd, buf.ptr, 8192u64);
|
|
};
|
|
if (n < 0i64) { ok = false; };
|
|
if (os.close(fd) != 0) { ok = false; };
|
|
if (os.rmdir(path) != 0) { ok = false; };
|
|
return ok;
|
|
};
|
|
|
|
fn pkgemitfile(path: str, fd: i32) bool = {
|
|
let s: str;
|
|
if (!pkgread(path, &s)) { return false; };
|
|
if (s.len != 0) {
|
|
pkgput(fd, s);
|
|
if (s[s.len - 1] != '\n') { pkgput(fd, "\n"); };
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn pkgsetplanpaths(p: *pkgplan, groups: []pkggroup, root: str, index: i32,
|
|
compileonly: bool, buildonly: bool, outname: str, outputdir: bool,
|
|
createdir: str, workroot: str) bool = {
|
|
let num: str = strconv.i32tos(index, strconv.base.DEC);
|
|
if (!pkgstring(&p.root, root, "/plan-", num)) { return false; };
|
|
if (!pkgmakedir(p.root)) {
|
|
pkgfailpath(p.root, "cannot create directory-plan temporary path");
|
|
return false;
|
|
};
|
|
// The caller's -w directory is the semantic-action store itself. A pattern
|
|
// spelling or traversal prefix must never select another cache container.
|
|
// The private driver creates it only after graph preflight succeeds.
|
|
p.workdir = workroot;
|
|
p.outputdir = createdir;
|
|
if (!pkgstring(&p.buildout, p.root, "/build.stdout")
|
|
|| !pkgstring(&p.builderr, p.root, "/build.stderr")) { return false; };
|
|
let i: i32 = p.start;
|
|
for (i < p.end) {
|
|
let g: *pkggroup = &groups[i];
|
|
g.plan = index;
|
|
if (!pkgstring(&g.root, p.root, "/product-",
|
|
strconv.i32tos(i - p.start, strconv.base.DEC))) { return false; };
|
|
if (!pkgmakedir(g.root)) {
|
|
pkgfailpath(g.root, "cannot create product temporary path");
|
|
return false;
|
|
};
|
|
g.publicbin = false;
|
|
g.installattempted = false;
|
|
if (buildonly) {
|
|
if (outputdir && strings.compare(g.pkg, "main") == 0
|
|
&& !p.outputpatherror && !p.emitasm) {
|
|
if (!pkgjoinpath(outname, g.basename, &g.bin)) { return false; };
|
|
g.publicbin = true;
|
|
} else if (outname.len != 0 && !outputdir) {
|
|
g.bin = outname;
|
|
g.publicbin = true;
|
|
}
|
|
else if (!pkgstring(&g.bin, g.root, "/package.build")) { return false; };
|
|
} else {
|
|
if (!pkgstring(&g.bin, g.root, "/package.test")) { return false; };
|
|
};
|
|
if (!pkgstring(&g.runoutput, g.root, "/test.output")
|
|
|| !pkgstring(&g.installoutput, g.root, "/install.output")
|
|
|| !pkgstring(&g.buildok, g.root, "/build.ok")) { return false; };
|
|
i += 1;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
fn pkglabel(g: *pkggroup) void = {
|
|
pkgput(os.STDOUT_FILENO, g.dir);
|
|
pkgput(os.STDOUT_FILENO, " [");
|
|
pkgput(os.STDOUT_FILENO, g.pkg);
|
|
pkgput(os.STDOUT_FILENO, "]");
|
|
};
|
|
|
|
fn pkgreportcommand(fd: i32, kind: str, g: *pkggroup,
|
|
r: *exec.result) void = {
|
|
pkgput(fd, "FAIL ");
|
|
pkgput(fd, g.dir);
|
|
pkgput(fd, " [");
|
|
pkgput(fd, g.pkg);
|
|
pkgput(fd, "] (");
|
|
pkgput(fd, kind);
|
|
if (r.errno != 0 || r.cleanuperrno != 0
|
|
|| r.termination == exec.termination.ERROR) {
|
|
pkgput(fd, " harness error ");
|
|
let code: i32 = r.errno;
|
|
if (code == 0) { code = r.cleanuperrno; };
|
|
pkgput(fd, strconv.i32tos(code, strconv.base.DEC));
|
|
} else if (r.termination == exec.termination.SIGNAL) {
|
|
pkgput(fd, " signal ");
|
|
pkgput(fd, strconv.i32tos(r.code, strconv.base.DEC));
|
|
} else {
|
|
pkgput(fd, " exit ");
|
|
pkgput(fd, strconv.i32tos(r.code, strconv.base.DEC));
|
|
};
|
|
pkgputln(fd, ")");
|
|
};
|
|
|
|
fn pkgstartbuild(p: *pkgplan, groups: []pkggroup, builder: str, includes: []str,
|
|
libdirs: []str, libs: []str, h: *exec.process) bool = {
|
|
let nproducts: i32 = p.end - p.start;
|
|
let capacity: i32 = 24;
|
|
if (nproducts < 0 || nproducts > (PKG_COUNT_MAX - capacity) / 10) {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
capacity += nproducts * 10;
|
|
if (includes.len > (PKG_COUNT_MAX - capacity) / 2) {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
capacity += includes.len * 2;
|
|
if (libdirs.len > (PKG_COUNT_MAX - capacity) / 2) {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
capacity += libdirs.len * 2;
|
|
if (libs.len > (PKG_COUNT_MAX - capacity) / 2) {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
capacity += libs.len * 2;
|
|
let allocation: ([]str | nomem) = pkgallocstrs(capacity);
|
|
let ba: []str;
|
|
match (allocation) {
|
|
case let value: []str => ba = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
append(ba, builder);
|
|
append(ba, "test");
|
|
append(ba, "-c");
|
|
if (p.buildonly) { append(ba, "--ww-package-build"); };
|
|
if (p.publish) { append(ba, "--ww-package-publish"); };
|
|
if (p.emitasm) { append(ba, "-S"); };
|
|
if (p.workdir.len != 0) { append(ba, "--ww-create-workdir"); };
|
|
if (p.outputdir.len != 0) {
|
|
append(ba, "--ww-create-output-dir");
|
|
append(ba, p.outputdir);
|
|
};
|
|
if (p.outputpatherror) {
|
|
append(ba, "--ww-command-output-path-error");
|
|
};
|
|
if (p.defaultoutputdir.len != 0) {
|
|
append(ba, "--ww-default-output-dir");
|
|
append(ba, p.defaultoutputdir);
|
|
};
|
|
if (p.outputcollisionbase.len != 0) {
|
|
append(ba, "--ww-command-output-collision");
|
|
append(ba, p.outputcollisionbase);
|
|
append(ba, p.outputcollisiondir);
|
|
};
|
|
if (p.identity.len != 0) {
|
|
append(ba, "--ww-root-identity");
|
|
append(ba, p.identity);
|
|
};
|
|
let i: i32 = p.start;
|
|
for (i < p.end) {
|
|
let g: *pkggroup = &groups[i];
|
|
append(ba, "--ww-package-test");
|
|
if (p.buildonly && g.publicbin) { append(ba, "build-public"); }
|
|
else if (p.buildonly) { append(ba, "build"); }
|
|
else { append(ba, "test"); };
|
|
append(ba, g.pkg);
|
|
if (g.prodpkg.len != 0) { append(ba, g.prodpkg); }
|
|
else { append(ba, "-"); };
|
|
if (g.hassame) { append(ba, g.samepkg); }
|
|
else { append(ba, "-"); };
|
|
if (g.hasexternal) { append(ba, g.externalpkg); }
|
|
else { append(ba, "-"); };
|
|
append(ba, g.dir);
|
|
append(ba, g.bin);
|
|
if (g.publish.len != 0 && !p.deferpublish) { append(ba, g.publish); }
|
|
else { append(ba, "-"); };
|
|
append(ba, g.buildok);
|
|
i += 1;
|
|
};
|
|
let ii: i32 = 0;
|
|
for (ii < includes.len) {
|
|
append(ba, "-I");
|
|
append(ba, includes[ii]);
|
|
ii += 1;
|
|
};
|
|
ii = 0;
|
|
for (ii < libdirs.len) {
|
|
append(ba, "-L");
|
|
append(ba, libdirs[ii]);
|
|
ii += 1;
|
|
};
|
|
ii = 0;
|
|
for (ii < libs.len) {
|
|
append(ba, "-l");
|
|
append(ba, libs[ii]);
|
|
ii += 1;
|
|
};
|
|
if (p.workdir.len != 0) {
|
|
append(ba, "-w");
|
|
append(ba, p.workdir);
|
|
};
|
|
append(ba, p.dir);
|
|
let env: []str;
|
|
if (!toolenv(p.root, &env)) { return false; };
|
|
let bcmd: exec.command;
|
|
bcmd.path = builder;
|
|
bcmd.argv = ba;
|
|
bcmd.env = env;
|
|
bcmd.dir = "";
|
|
bcmd.stdoutpath = p.buildout;
|
|
bcmd.stderrpath = p.builderr;
|
|
bcmd.deadline.sec = 0i64;
|
|
bcmd.deadline.nsec = 0i64;
|
|
bcmd.grace = 0i64: time.duration;
|
|
exec.start(h, &bcmd);
|
|
return true;
|
|
};
|
|
|
|
fn pkgstartrun(g: *pkggroup, filters: []str, timeoutarg: str,
|
|
list: bool, builder: str, h: *exec.process) bool = {
|
|
if (filters.len > PKG_COUNT_MAX - 4) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
return false;
|
|
};
|
|
let allocation: ([]str | nomem) = pkgallocstrs(filters.len + 4);
|
|
let ra: []str;
|
|
match (allocation) {
|
|
case let value: []str => ra = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return false;
|
|
};
|
|
};
|
|
append(ra, g.bin);
|
|
let packagearg: str;
|
|
if (!pkgstring(&packagearg, "-package=", g.pkg)) {
|
|
pkgfreestrs(ra);
|
|
return false;
|
|
};
|
|
append(ra, packagearg);
|
|
if (list) { append(ra, "-list"); };
|
|
if (timeoutarg.len != 0) { append(ra, timeoutarg); };
|
|
let i: i32 = 0;
|
|
for (i < filters.len) { append(ra, filters[i]); i += 1; };
|
|
let env: []str;
|
|
let pathowned: str;
|
|
let pwdowned: str;
|
|
if (!runenv(g.dir, builder, &env, &pathowned, &pwdowned)) {
|
|
pkgfreeownedstr(packagearg);
|
|
pkgfreestrs(ra);
|
|
return false;
|
|
};
|
|
let rcmd: exec.command;
|
|
rcmd.path = g.bin;
|
|
rcmd.argv = ra;
|
|
rcmd.env = env;
|
|
rcmd.dir = g.dir;
|
|
rcmd.stdoutpath = g.runoutput;
|
|
rcmd.stderrpath = g.runoutput;
|
|
rcmd.deadline.sec = 0i64;
|
|
rcmd.deadline.nsec = 0i64;
|
|
rcmd.grace = 0i64: time.duration;
|
|
exec.start(h, &rcmd);
|
|
freerunenv(env, pathowned, pwdowned);
|
|
pkgfreeownedstr(packagearg);
|
|
pkgfreestrs(ra);
|
|
return true;
|
|
};
|
|
|
|
// Pinned builderTest orders a retained running test as build -> run ->
|
|
// BuildInstallFunc. Re-enter the selected stage driver for the final action so
|
|
// build and test share one overwrite predicate instead of copying object magic
|
|
// into the coordinator.
|
|
fn pkginstalloutput(g: *pkggroup, builder: str) void = {
|
|
let ia: []str = [builder, "test", "--ww-install-test-output",
|
|
g.bin, g.publish];
|
|
let env: []str = os.getenvs();
|
|
let icmd: exec.command;
|
|
icmd.path = builder;
|
|
icmd.argv = ia;
|
|
icmd.env = env;
|
|
icmd.dir = "";
|
|
icmd.stdoutpath = g.installoutput;
|
|
icmd.stderrpath = g.installoutput;
|
|
icmd.deadline.sec = 0i64;
|
|
icmd.deadline.nsec = 0i64;
|
|
icmd.grace = 0i64: time.duration;
|
|
let h: exec.process;
|
|
g.installattempted = true;
|
|
exec.start(&h, &icmd);
|
|
for (!exec.poll(&h)) { time.sleep(pkgpoll, time.clock.monotonic); };
|
|
g.installres = h.result;
|
|
};
|
|
|
|
fn pkgproductbuilt(g: *pkggroup, buildonly: bool) bool = {
|
|
if (buildonly || g.notests) { return pkgisreg(g.buildok); };
|
|
return pkgisreg(g.buildok) && pkgisreg(g.bin);
|
|
};
|
|
|
|
fn pkgrunok(g: *pkggroup) bool = {
|
|
return g.runres.errno == 0 && g.runres.cleanuperrno == 0
|
|
&& g.runres.termination == exec.termination.EXIT
|
|
&& g.runres.code == 0;
|
|
};
|
|
|
|
fn pkginstallok(g: *pkggroup) bool = {
|
|
return g.installres.errno == 0 && g.installres.cleanuperrno == 0
|
|
&& g.installres.termination == exec.termination.EXIT
|
|
&& g.installres.code == 0;
|
|
};
|
|
|
|
fn pkgemitinstall(g: *pkggroup) bool = {
|
|
if (!g.installattempted) { return true; };
|
|
let output: str;
|
|
if (!pkgread(g.installoutput, &output)) {
|
|
pkgfailpath(g.root, "cannot read install capture");
|
|
return false;
|
|
};
|
|
if (output.len != 0) {
|
|
pkgput(os.STDERR_FILENO, output);
|
|
if (output[output.len - 1] != '\n') {
|
|
pkgput(os.STDERR_FILENO, "\n");
|
|
};
|
|
};
|
|
if (!pkginstallok(g)) {
|
|
if (output.len == 0) {
|
|
pkgreportcommand(os.STDERR_FILENO, "install", g,
|
|
&g.installres);
|
|
};
|
|
return false;
|
|
};
|
|
return true;
|
|
};
|
|
|
|
// Pinned cmd/go recognizes the testing package's exact no-tests warning only
|
|
// at capture byte zero or after a newline. The harness owns whether a test ran;
|
|
// this coordinator owns only the corresponding package-result suffix.
|
|
fn pkgnoteststorun(output: str) bool = {
|
|
let first: str = "testing: warning: no tests to run\n";
|
|
let later: str = "\ntesting: warning: no tests to run\n";
|
|
return strings.hasprefix(output, first) || strings.contains(output, later);
|
|
};
|
|
|
|
fn pkgemitgroup(g: *pkggroup, compileonly: bool,
|
|
statusfailed: *bool) bool = {
|
|
if (g.notests) {
|
|
pkgput(os.STDOUT_FILENO, "? ");
|
|
pkgput(os.STDOUT_FILENO, g.dir);
|
|
pkgputln(os.STDOUT_FILENO, " [no test files]");
|
|
return true;
|
|
};
|
|
if (compileonly) {
|
|
// Go's compile-only print action is a nop after the retained binary
|
|
// install completes. Build and publication failures still diagnose on
|
|
// stderr through the plan result above.
|
|
return true;
|
|
};
|
|
if (g.runstartfailed) { return false; };
|
|
let runoutput: str;
|
|
if (!pkgread(g.runoutput, &runoutput)) {
|
|
pkgfailpath(g.root, "cannot read test capture");
|
|
return false;
|
|
};
|
|
pkgput(os.STDOUT_FILENO, runoutput);
|
|
if (runoutput.len != 0 && runoutput[runoutput.len - 1] != '\n') {
|
|
pkgput(os.STDOUT_FILENO, "\n");
|
|
};
|
|
if (!pkgrunok(g)) {
|
|
*statusfailed = true;
|
|
pkgreportcommand(os.STDOUT_FILENO, "test", g, &g.runres);
|
|
return false;
|
|
};
|
|
pkgput(os.STDOUT_FILENO, "ok ");
|
|
pkglabel(g);
|
|
if (pkgnoteststorun(runoutput)) {
|
|
pkgput(os.STDOUT_FILENO, " [no tests to run]");
|
|
};
|
|
pkgput(os.STDOUT_FILENO, "\n");
|
|
return true;
|
|
};
|
|
|
|
// A directory build capture is emitted once, followed by its independently
|
|
// executed products in their existing byte-sorted group order.
|
|
fn pkgemitplan(p: *pkgplan, groups: []pkggroup,
|
|
compileonly: bool, buildonly: bool, statusfailed: *bool) i32 = {
|
|
let buildcaptures: bool = pkgemitfile(p.buildout, os.STDOUT_FILENO);
|
|
buildcaptures = pkgemitfile(p.builderr, os.STDERR_FILENO) && buildcaptures;
|
|
if (!buildcaptures) {
|
|
pkgfailpath(p.root, "cannot read build capture");
|
|
return 1;
|
|
};
|
|
let failed: i32 = 0;
|
|
let i: i32 = p.start;
|
|
for (i < p.end) {
|
|
if (!pkgproductbuilt(&groups[i], buildonly)) {
|
|
if (!p.suppressbuildreports) {
|
|
pkgreportcommand(os.STDERR_FILENO, "build", &groups[i],
|
|
&p.buildres);
|
|
};
|
|
if (!buildonly && !compileonly) { *statusfailed = true; };
|
|
failed += 1;
|
|
} else if (buildonly) {
|
|
void;
|
|
} else {
|
|
if (!pkgemitgroup(&groups[i], compileonly, statusfailed)) {
|
|
failed += 1;
|
|
};
|
|
if (!compileonly && !pkgemitinstall(&groups[i])) { failed += 1; };
|
|
};
|
|
i += 1;
|
|
};
|
|
return failed;
|
|
};
|
|
|
|
export fn packagecommand(args: []str) int = {
|
|
let compileonly: bool = false;
|
|
let buildonly: bool = false;
|
|
let list: bool = false;
|
|
let jobs: i32 = 1;
|
|
let afterdash: bool = false;
|
|
let buildrootseen: bool = false;
|
|
let explicitstatus: bool = false;
|
|
if (args.len == PKG_COUNT_MAX) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: package graph is too large");
|
|
return 1;
|
|
};
|
|
let argcapacity: i32 = args.len + 1;
|
|
let rootallocation: ([]str | nomem) = pkgallocstrs(argcapacity);
|
|
let roots: []str;
|
|
match (rootallocation) {
|
|
case let value: []str => roots = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
let filterallocation: ([]str | nomem) = pkgallocstrs(argcapacity);
|
|
let filters: []str;
|
|
match (filterallocation) {
|
|
case let value: []str => filters = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
let includeallocation: ([]str | nomem) = pkgallocstrs(argcapacity);
|
|
let includes: []str;
|
|
match (includeallocation) {
|
|
case let value: []str => includes = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
let libdirallocation: ([]str | nomem) = pkgallocstrs(argcapacity);
|
|
let libdirs: []str;
|
|
match (libdirallocation) {
|
|
case let value: []str => libdirs = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
let liballocation: ([]str | nomem) = pkgallocstrs(argcapacity);
|
|
let libs: []str;
|
|
match (liballocation) {
|
|
case let value: []str => libs = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
let emitasm: bool = false;
|
|
let timeoutarg: str = "";
|
|
let outname: str = "";
|
|
let explicitout: bool = false;
|
|
let workroot: str = "";
|
|
let requestidentity: str = "";
|
|
let builder: str;
|
|
if (!pkgdefaultbuilder(&builder)) { return 1; };
|
|
let i: i32 = 0;
|
|
for (i < args.len) {
|
|
let a: str = args[i];
|
|
if (buildonly && buildrootseen) {
|
|
append(roots, a);
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (afterdash) {
|
|
if (buildonly) { append(roots, a); }
|
|
else { append(filters, a); };
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "--") == 0) { afterdash = true; i += 1; continue; };
|
|
if (strings.compare(a, "-c") == 0) { compileonly = true; i += 1; continue; };
|
|
if (strings.compare(a, "-S") == 0) { emitasm = true; i += 1; continue; };
|
|
if (!buildonly && strings.compare(a, "-list") == 0) {
|
|
list = true; i += 1; continue;
|
|
};
|
|
if (strings.compare(a, "-I") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
append(includes, args[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.hasprefix(a, "-I") && a.len > 2) {
|
|
append(includes, a[2:a.len]);
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "-L") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
append(libdirs, args[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.hasprefix(a, "-L") && a.len > 2) {
|
|
append(libdirs, a[2:a.len]);
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "-l") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
append(libs, args[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.hasprefix(a, "-l") && a.len > 2) {
|
|
append(libs, a[2:a.len]);
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "-o") == 0
|
|
|| strings.compare(a, "--o") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
outname = args[i + 1];
|
|
explicitout = outname.len != 0;
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.hasprefix(a, "-o=")
|
|
|| strings.hasprefix(a, "--o=")) {
|
|
let valueoff: i32 = 3;
|
|
if (a[1] == '-') { valueoff = 4; };
|
|
outname = a[valueoff:a.len];
|
|
explicitout = outname.len != 0;
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "-w") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
workroot = args[i + 1];
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.hasprefix(a, "-w") && a.len > 2) {
|
|
workroot = a[2:a.len];
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.hasprefix(a, "-timeout-ms=")) {
|
|
if (timeoutarg.len != 0 || a.len == 12
|
|
|| pkgparsedec(a[12:a.len], 3600000i64) <= 0i64) {
|
|
pkgusage();
|
|
return 2;
|
|
};
|
|
timeoutarg = a;
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "-j") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
let v: i64 = pkgparsedec(args[i + 1], 2147483647i64);
|
|
if (v <= 0i64) { pkgusage(); return 2; };
|
|
jobs = v: i32;
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "-filter") == 0 || strings.compare(a, "-run") == 0) {
|
|
if (i + 1 >= args.len) { pkgusage(); return 2; };
|
|
append(filters, args[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "--ww-driver") == 0) {
|
|
if (i + 1 >= args.len || args[i + 1].len == 0) {
|
|
pkgusage();
|
|
return 2;
|
|
};
|
|
builder = args[i + 1];
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "--ww-explicit-test-target") == 0) {
|
|
if (explicitstatus || buildonly) { pkgusage(); return 2; };
|
|
explicitstatus = true;
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "--ww-operation") == 0) {
|
|
if (i + 1 >= args.len || strings.compare(args[i + 1], "build") != 0
|
|
|| buildonly) {
|
|
pkgusage();
|
|
return 2;
|
|
};
|
|
buildonly = true;
|
|
compileonly = true;
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (strings.compare(a, "--ww-root-identity") == 0) {
|
|
if (i + 1 >= args.len || args[i + 1].len == 0
|
|
|| requestidentity.len != 0) {
|
|
pkgusage();
|
|
return 2;
|
|
};
|
|
requestidentity = args[i + 1];
|
|
i += 2;
|
|
continue;
|
|
};
|
|
if (a.len != 0 && a[0] == '-') { pkgusage(); return 2; };
|
|
append(roots, a);
|
|
if (buildonly) { buildrootseen = true; };
|
|
i += 1;
|
|
};
|
|
if (roots.len == 0) { append(roots, "."); };
|
|
pkgsortstrings(roots);
|
|
let buildnull: bool = buildonly && explicitout
|
|
&& strings.compare(outname, "/dev/null") == 0;
|
|
if (compileonly && !buildonly
|
|
&& (list || filters.len != 0 || timeoutarg.len != 0)) {
|
|
pkgusage();
|
|
return 2;
|
|
};
|
|
if (!buildonly && (emitasm || libdirs.len != 0 || libs.len != 0)) {
|
|
pkgusage();
|
|
return 2;
|
|
};
|
|
let ds: pkgdiscover;
|
|
let emptypaths: []str;
|
|
ds.paths = emptypaths;
|
|
ds.errors = 0;
|
|
ds.fatal = false;
|
|
let directroots: pkgdiscover;
|
|
let emptydirect: []str;
|
|
directroots.paths = emptydirect;
|
|
directroots.errors = 0;
|
|
directroots.fatal = false;
|
|
let anyrecurse: bool = false;
|
|
i = 0;
|
|
for (i < roots.len) {
|
|
let requested: str = roots[i];
|
|
let cleaned: str;
|
|
if (!pkgcleanpath(requested, &cleaned)) {
|
|
return 1;
|
|
};
|
|
let ellipsis: i32 = pkgfirstellipsis(cleaned);
|
|
let recurse: bool = ellipsis >= 0;
|
|
let discoverroot: str = cleaned;
|
|
let matcher: pkgmatcher;
|
|
if (recurse) {
|
|
let slash: i32 = ellipsis - 1;
|
|
for (slash >= 0 && cleaned[slash] != '/') { slash -= 1; };
|
|
if (slash < 0) { discoverroot = "."; }
|
|
else if (slash == 0) { discoverroot = "/"; }
|
|
else { discoverroot = cleaned[0:slash]; };
|
|
if (!pkgmakematcher(cleaned, &matcher)) {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
if (recurse) { anyrecurse = true; };
|
|
let canonical: str;
|
|
let canonicaloom: bool = false;
|
|
if (!pkgcanonicaldir(discoverroot, &canonical, &canonicaloom)) {
|
|
if (canonicaloom) { return 1; };
|
|
pkgfailpath(discoverroot, "cannot canonicalize package directory");
|
|
ds.errors += 1;
|
|
if (recurse) { pkgfreematcher(&matcher); };
|
|
i += 1;
|
|
continue;
|
|
};
|
|
if (!recurse && !pkgappenddiscovered(&directroots, canonical)) {
|
|
return 1;
|
|
};
|
|
let before: i32 = ds.paths.len;
|
|
let errorsbefore: i32 = ds.errors;
|
|
if (!recurse) {
|
|
pkgdiscoverdir(discoverroot, &ds, nil);
|
|
} else {
|
|
// MatchDirs applies .*, _*, and testdata pruning to the walk root
|
|
// itself as well as descendants. Literal direct selection stays legal.
|
|
if (!pkgexcluded(pkgbase(discoverroot))) {
|
|
pkgdiscoverdir(discoverroot, &ds, &matcher);
|
|
};
|
|
pkgfreematcher(&matcher);
|
|
};
|
|
if (recurse && ds.paths.len == before && ds.errors == errorsbefore) {
|
|
pkgput(os.STDERR_FILENO, "ww: warning: ");
|
|
pkgputquoted(os.STDERR_FILENO, requested);
|
|
pkgputln(os.STDERR_FILENO, " matched no packages");
|
|
} else if (!recurse && ds.paths.len == before
|
|
&& ds.errors == errorsbefore) {
|
|
pkgfailpath(discoverroot, "directory contains no WW package sources");
|
|
ds.errors += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
if (ds.errors != 0) {
|
|
if (ds.fatal) { return 1; };
|
|
return pkgteststatusfail(explicitstatus, compileonly);
|
|
};
|
|
pkgsortstrings(directroots.paths);
|
|
directroots.paths = pkgdedup(directroots.paths);
|
|
i = 0;
|
|
for (i < ds.paths.len) {
|
|
let canonicaldir: str;
|
|
let canonicaloom: bool = false;
|
|
if (!pkgcanonicaldir(pkgdirname(ds.paths[i]), &canonicaldir,
|
|
&canonicaloom)) {
|
|
if (canonicaloom) { return 1; };
|
|
pkgfailpath(ds.paths[i], "cannot canonicalize package directory");
|
|
return pkgteststatusfail(explicitstatus, compileonly);
|
|
};
|
|
let canonicalsource: str;
|
|
if (!pkgstring(&canonicalsource, canonicaldir, "/",
|
|
pkgbase(ds.paths[i]))) { return 1; };
|
|
ds.paths[i] = canonicalsource;
|
|
i += 1;
|
|
};
|
|
pkgsort(ds.paths);
|
|
ds.paths = pkgdedup(ds.paths);
|
|
if (ds.paths.len == 0) {
|
|
if (buildonly) {
|
|
if (explicitout && !buildnull && pkgoutputdir(outname)) {
|
|
pkgputln(os.STDERR_FILENO, "ww: no main packages to build");
|
|
return 1;
|
|
};
|
|
if (explicitout && !buildnull) {
|
|
pkgputln(os.STDERR_FILENO, "ww: no packages to build");
|
|
return 1;
|
|
};
|
|
return 0;
|
|
};
|
|
pkgputln(os.STDERR_FILENO, "ww test: no packages to test");
|
|
return 1;
|
|
};
|
|
|
|
let sourceallocation: ([]pkgsource | nomem) =
|
|
pkgallocsources(ds.paths.len);
|
|
let srcs: []pkgsource;
|
|
match (sourceallocation) {
|
|
case let value: []pkgsource => srcs = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < ds.paths.len) {
|
|
let s: pkgsource;
|
|
s.path = ds.paths[i];
|
|
if (!pkgstring(&s.dir, pkgdirname(ds.paths[i]))) { return 1; };
|
|
s.test = strings.hassuffix(pkgbase(ds.paths[i]), "_test.ww");
|
|
if (buildonly && s.test) {
|
|
s.pkg = "";
|
|
append(srcs, s);
|
|
i += 1;
|
|
continue;
|
|
};
|
|
let body: str;
|
|
let pn: str;
|
|
if (!pkgread(ds.paths[i], &body)) {
|
|
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
|
return pkgteststatusfail(explicitstatus, compileonly);
|
|
};
|
|
if (pkgnulerrors(ds.paths[i], body)) {
|
|
return pkgteststatusfail(explicitstatus, compileonly);
|
|
};
|
|
if (!pkgclause(body, &pn)) {
|
|
pkgfailpath(ds.paths[i], "invalid or missing package clause");
|
|
return pkgteststatusfail(explicitstatus, compileonly);
|
|
};
|
|
if (!pkgstring(&s.pkg, pn)) { return 1; };
|
|
append(srcs, s);
|
|
i += 1;
|
|
};
|
|
|
|
let folderallocation: ([]pkgfolder | nomem) =
|
|
pkgallocfolders(srcs.len);
|
|
let folders: []pkgfolder;
|
|
match (folderallocation) {
|
|
case let value: []pkgfolder => folders = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < srcs.len) {
|
|
let f: pkgfolder;
|
|
f.path = srcs[i].dir;
|
|
f.start = i;
|
|
f.end = i;
|
|
f.prodpkg = "";
|
|
for (f.end < srcs.len && strings.compare(srcs[f.end].dir, f.path) == 0) {
|
|
if (!srcs[f.end].test && f.prodpkg.len == 0) {
|
|
f.prodpkg = srcs[f.end].pkg;
|
|
};
|
|
f.end += 1;
|
|
};
|
|
append(folders, f);
|
|
i = f.end;
|
|
};
|
|
|
|
let groupallocation: ([]pkggroup | nomem) =
|
|
pkgallocgroups(srcs.len);
|
|
let groups: []pkggroup;
|
|
match (groupallocation) {
|
|
case let value: []pkggroup => groups = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < folders.len) {
|
|
let f: pkgfolder = folders[i];
|
|
if (buildonly) {
|
|
if (f.prodpkg.len != 0) {
|
|
let g: pkggroup;
|
|
g.dir = f.path;
|
|
g.pkg = f.prodpkg;
|
|
g.testname = "";
|
|
g.publish = "";
|
|
g.prodpkg = f.prodpkg;
|
|
g.samepkg = "";
|
|
g.externalpkg = "";
|
|
g.hassame = false;
|
|
g.hasexternal = false;
|
|
g.notests = false;
|
|
g.production = true;
|
|
append(groups, g);
|
|
} else {
|
|
let explicit: bool = false;
|
|
let di: i32 = 0;
|
|
for (di < directroots.paths.len) {
|
|
if (strings.compare(directroots.paths[di], f.path) == 0) {
|
|
explicit = true;
|
|
break;
|
|
};
|
|
di += 1;
|
|
};
|
|
if (explicit) {
|
|
pkgfailpath(f.path,
|
|
"directory contains no WW package sources");
|
|
return 1;
|
|
};
|
|
};
|
|
i += 1;
|
|
continue;
|
|
};
|
|
let family: str = f.prodpkg;
|
|
let samepkg: str = "";
|
|
let externalpkg: str = "";
|
|
let hassame: bool = false;
|
|
let hasexternal: bool = false;
|
|
let sawtestfile: bool = false;
|
|
if (f.prodpkg.len != 0) {
|
|
let expectedexternal: str;
|
|
if (!pkgstring(&expectedexternal, f.prodpkg, "_test")) {
|
|
return 1;
|
|
};
|
|
let j: i32 = f.start;
|
|
for (j < f.end) {
|
|
if (srcs[j].test) {
|
|
sawtestfile = true;
|
|
if (strings.compare(srcs[j].pkg, f.prodpkg) == 0) {
|
|
hassame = true;
|
|
samepkg = srcs[j].pkg;
|
|
} else if (strings.compare(srcs[j].pkg,
|
|
expectedexternal) == 0) {
|
|
hasexternal = true;
|
|
externalpkg = srcs[j].pkg;
|
|
} else {
|
|
pkgfailpath(srcs[j].path,
|
|
"test package must match production package or <package>_test");
|
|
return pkgteststatusfail(explicitstatus,
|
|
compileonly);
|
|
};
|
|
};
|
|
j += 1;
|
|
};
|
|
} else {
|
|
let firstpkg: str = "";
|
|
let secondpkg: str = "";
|
|
let j: i32 = f.start;
|
|
for (j < f.end) {
|
|
if (srcs[j].test) {
|
|
sawtestfile = true;
|
|
if (firstpkg.len == 0) {
|
|
firstpkg = srcs[j].pkg;
|
|
} else if (strings.compare(srcs[j].pkg, firstpkg) != 0
|
|
&& secondpkg.len == 0) {
|
|
secondpkg = srcs[j].pkg;
|
|
} else if (strings.compare(srcs[j].pkg, firstpkg) != 0
|
|
&& strings.compare(srcs[j].pkg, secondpkg) != 0) {
|
|
pkgfailpath(srcs[j].path,
|
|
"test package must match production package or <package>_test");
|
|
return pkgteststatusfail(explicitstatus,
|
|
compileonly);
|
|
};
|
|
};
|
|
j += 1;
|
|
};
|
|
if (secondpkg.len == 0) {
|
|
if (strings.hassuffix(firstpkg, "_test")) {
|
|
family = firstpkg[0:(firstpkg.len - 5)];
|
|
if (family.len == 0) {
|
|
pkgfailpath(f.path,
|
|
"external test package has empty base name");
|
|
return pkgteststatusfail(explicitstatus,
|
|
compileonly);
|
|
};
|
|
hasexternal = true;
|
|
externalpkg = firstpkg;
|
|
} else {
|
|
family = firstpkg;
|
|
hassame = true;
|
|
samepkg = firstpkg;
|
|
};
|
|
} else {
|
|
let firstexternal: str;
|
|
let secondexternal: str;
|
|
if (!pkgstring(&firstexternal, firstpkg, "_test")
|
|
|| !pkgstring(&secondexternal, secondpkg, "_test")) {
|
|
return 1;
|
|
};
|
|
if (strings.compare(secondpkg, firstexternal) == 0) {
|
|
family = firstpkg;
|
|
hassame = true;
|
|
samepkg = firstpkg;
|
|
hasexternal = true;
|
|
externalpkg = secondpkg;
|
|
} else if (strings.compare(firstpkg, secondexternal) == 0) {
|
|
family = secondpkg;
|
|
hassame = true;
|
|
samepkg = secondpkg;
|
|
hasexternal = true;
|
|
externalpkg = firstpkg;
|
|
} else {
|
|
pkgfailpath(f.path,
|
|
"test package must match production package or <package>_test");
|
|
return pkgteststatusfail(explicitstatus, compileonly);
|
|
};
|
|
};
|
|
};
|
|
let g: pkggroup;
|
|
g.dir = f.path;
|
|
g.pkg = family;
|
|
if (g.pkg.len == 0) { g.pkg = srcs[f.start].pkg; };
|
|
g.basename = "";
|
|
g.testname = "";
|
|
g.publish = "";
|
|
g.prodpkg = f.prodpkg;
|
|
g.samepkg = samepkg;
|
|
g.externalpkg = externalpkg;
|
|
g.hassame = hassame;
|
|
g.hasexternal = hasexternal;
|
|
g.notests = !sawtestfile;
|
|
g.production = f.prodpkg.len != 0;
|
|
append(groups, g);
|
|
i += 1;
|
|
};
|
|
if (groups.len == 0) {
|
|
if (buildonly) {
|
|
if (explicitout && !buildnull && pkgoutputdir(outname)) {
|
|
pkgputln(os.STDERR_FILENO, "ww: no main packages to build");
|
|
return 1;
|
|
};
|
|
if (explicitout && !buildnull) {
|
|
pkgputln(os.STDERR_FILENO, "ww: no packages to build");
|
|
return 1;
|
|
};
|
|
return 0;
|
|
};
|
|
pkgputln(os.STDERR_FILENO, "ww test: no packages to test");
|
|
return 1;
|
|
};
|
|
pkgsortgroups(groups);
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
groups[i].basename = pkgbase(groups[i].dir);
|
|
if (roots.len == 1 && !anyrecurse && requestidentity.len != 0) {
|
|
groups[i].basename = pkgimportbase(requestidentity);
|
|
};
|
|
i += 1;
|
|
};
|
|
let defaultout: bool = false;
|
|
if (buildonly && !buildnull && outname.len == 0 && folders.len == 1
|
|
&& groups.len == 1 && strings.compare(groups[0].pkg, "main") == 0) {
|
|
outname = groups[0].basename;
|
|
defaultout = true;
|
|
};
|
|
let defaultoutputdir: str = "";
|
|
if (defaultout && pkgstatdir(outname)) {
|
|
defaultoutputdir = outname;
|
|
};
|
|
let outputdir: bool = buildonly && explicitout && !buildnull
|
|
&& pkgoutputdir(outname);
|
|
let outputpatherror: bool = false;
|
|
let outputcollisionbase: str = "";
|
|
let outputcollisiondir: str = "";
|
|
if (outputdir) {
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
if (strings.compare(groups[i].pkg, "main") == 0) {
|
|
let slash: i64 = 1i64;
|
|
if (strings.hassuffix(outname, "/")) { slash = 0i64; };
|
|
let outputlen: i64 = outname.len: i64 + slash
|
|
+ groups[i].basename.len: i64;
|
|
if (outputlen + 1i64 > os.PATH_MAX: i64) {
|
|
outputpatherror = true;
|
|
};
|
|
let j: i32 = 0;
|
|
for (j < i) {
|
|
if (strings.compare(groups[j].pkg, "main") == 0
|
|
&& strings.compare(groups[j].basename,
|
|
groups[i].basename) == 0
|
|
&& outputcollisionbase.len == 0) {
|
|
outputcollisionbase = groups[i].basename;
|
|
outputcollisiondir = outname;
|
|
};
|
|
j += 1;
|
|
};
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
// Go's test binary is always linked into request-private storage. -c and
|
|
// -o independently request a caller-visible executable copy; only -c
|
|
// suppresses execution. Visible names are import-leaf metadata and never
|
|
// action, package, variant, symbol, or persistence identity.
|
|
let testretain: bool = !buildonly && (compileonly || explicitout);
|
|
let testnull: bool = !buildonly && explicitout
|
|
&& strings.compare(outname, "/dev/null") == 0;
|
|
let testoutdir: bool = !buildonly && explicitout && !testnull
|
|
&& pkgoutputdir(outname);
|
|
let invocationdir: str = "";
|
|
if (testretain) {
|
|
let cwdoom: bool = false;
|
|
if (!pkgcanonicaldir(".", &invocationdir, &cwdoom)) {
|
|
if (!cwdoom) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: cannot determine invocation directory");
|
|
};
|
|
return 1;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
if (!pkgstring(&groups[i].testname, groups[i].basename, ".test")) {
|
|
return 1;
|
|
};
|
|
groups[i].publish = "";
|
|
if (testretain && !groups[i].notests && !testnull) {
|
|
if (!explicitout) {
|
|
if (!pkgjoinpath(invocationdir, groups[i].testname,
|
|
&groups[i].publish)) { return 1; };
|
|
} else if (testoutdir) {
|
|
let targetdir: str = outname;
|
|
if (!pkgisabs(outname)
|
|
&& !pkgjoinpath(invocationdir, outname, &targetdir)) {
|
|
return 1;
|
|
};
|
|
if (!pkgjoinpath(targetdir, groups[i].testname,
|
|
&groups[i].publish)) { return 1; };
|
|
} else if (pkgisabs(outname)) {
|
|
groups[i].publish = outname;
|
|
} else if (!pkgjoinpath(invocationdir, outname,
|
|
&groups[i].publish)) { return 1; };
|
|
};
|
|
i += 1;
|
|
};
|
|
if (!buildonly && explicitout && groups.len > 1
|
|
&& !testnull && !testoutdir) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"ww test: with multiple packages, -o must refer to a directory or /dev/null");
|
|
return 1;
|
|
};
|
|
if (!buildonly && groups.len > 1 && testretain && !testnull) {
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
let j: i32 = 0;
|
|
for (j < i) {
|
|
if (strings.compare(groups[j].testname,
|
|
groups[i].testname) == 0) {
|
|
pkgput(os.STDERR_FILENO,
|
|
"ww test: cannot write test binary ");
|
|
pkgput(os.STDERR_FILENO, groups[i].testname);
|
|
pkgputln(os.STDERR_FILENO,
|
|
" for multiple packages:");
|
|
let k: i32 = 0;
|
|
for (k < groups.len) {
|
|
if (strings.compare(groups[k].testname,
|
|
groups[i].testname) == 0) {
|
|
pkgputln(os.STDERR_FILENO, groups[k].dir);
|
|
};
|
|
k += 1;
|
|
};
|
|
return 1;
|
|
};
|
|
j += 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
// A non-directory caller-owned build name cannot fan out. A directory
|
|
// build output publishes each selected command under its directory leaf.
|
|
if (buildonly && !buildnull && outname.len != 0
|
|
&& !outputdir && groups.len > 1) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: cannot use -o with multiple packages");
|
|
return 2;
|
|
};
|
|
// Recursive/multi-root -S needs a caller-owned artifact tree. Without -w
|
|
// every assembly file would otherwise live only in the coordinator's
|
|
// temporary plan and disappear on successful return.
|
|
if (buildonly && emitasm && workroot.len == 0 && !buildnull) {
|
|
pkgputln(os.STDERR_FILENO,
|
|
"wwtest package: recursive -S needs -w");
|
|
return 2;
|
|
};
|
|
let planallocation: ([]pkgplan | nomem) = pkgallocplans(1);
|
|
let plans: []pkgplan;
|
|
match (planallocation) {
|
|
case let value: []pkgplan => plans = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
return 1;
|
|
};
|
|
};
|
|
let plan: pkgplan;
|
|
plan.dir = groups[0].dir;
|
|
plan.identity = "";
|
|
if (roots.len == 1 && !anyrecurse) { plan.identity = requestidentity; };
|
|
plan.start = 0;
|
|
plan.end = groups.len;
|
|
plan.state = PKGQUEUED;
|
|
plan.buildonly = buildonly;
|
|
plan.publish = buildonly && explicitout && !buildnull && outname.len != 0
|
|
&& groups.len == 1
|
|
&& strings.compare(groups[0].pkg, "main") != 0;
|
|
plan.emitasm = emitasm;
|
|
plan.outputpatherror = outputpatherror;
|
|
plan.defaultoutputdir = defaultoutputdir;
|
|
plan.outputcollisionbase = outputcollisionbase;
|
|
plan.outputcollisiondir = outputcollisiondir;
|
|
plan.suppressbuildreports = buildonly
|
|
&& (outputdir || defaultoutputdir.len != 0);
|
|
plan.deferpublish = !buildonly && !compileonly && testretain && !testnull;
|
|
append(plans, plan);
|
|
let createdir: str = "";
|
|
if (buildonly && outputdir) {
|
|
createdir = outname;
|
|
} else if (!buildonly && compileonly && testretain && !testnull) {
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
if (groups[i].publish.len != 0) {
|
|
createdir = pkgdirname(groups[i].publish);
|
|
break;
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
let tmproot: str = temp.dir();
|
|
let planout: str = outname;
|
|
if (buildnull) { planout = ""; };
|
|
let failed: i32 = 0;
|
|
let statusfailed: bool = false;
|
|
i = 0;
|
|
for (i < plans.len) {
|
|
if (!pkgsetplanpaths(&plans[i], groups, tmproot, i,
|
|
compileonly, buildonly, planout, outputdir, createdir,
|
|
workroot)) {
|
|
if (!pkgremoveall(tmproot)) {
|
|
pkgput(os.STDERR_FILENO,
|
|
"wwtest package: cleanup failed; retained ");
|
|
pkgputln(os.STDERR_FILENO, tmproot);
|
|
};
|
|
return 1;
|
|
};
|
|
i += 1;
|
|
};
|
|
|
|
// Build the complete request in one command-owned package universe. Once
|
|
// the union build completes, successful products run under the same global
|
|
// -j bound. Emission stays in byte-sorted group order below.
|
|
let handleallocation: ([]exec.process | nomem) =
|
|
pkgallocprocesses(plans.len);
|
|
let handles: []exec.process;
|
|
match (handleallocation) {
|
|
case let value: []exec.process => handles = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
if (!pkgremoveall(tmproot)) {
|
|
pkgput(os.STDERR_FILENO,
|
|
"wwtest package: cleanup failed; retained ");
|
|
pkgputln(os.STDERR_FILENO, tmproot);
|
|
};
|
|
return 1;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < plans.len) {
|
|
let h: exec.process;
|
|
append(handles, h);
|
|
i += 1;
|
|
};
|
|
let runhandleallocation: ([]exec.process | nomem) =
|
|
pkgallocprocesses(groups.len);
|
|
let runhandles: []exec.process;
|
|
match (runhandleallocation) {
|
|
case let value: []exec.process => runhandles = value;
|
|
case nomem => {
|
|
pkgputln(os.STDERR_FILENO, "wwtest package: out of memory");
|
|
if (!pkgremoveall(tmproot)) {
|
|
pkgput(os.STDERR_FILENO,
|
|
"wwtest package: cleanup failed; retained ");
|
|
pkgputln(os.STDERR_FILENO, tmproot);
|
|
};
|
|
return 1;
|
|
};
|
|
};
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
let h: exec.process;
|
|
append(runhandles, h);
|
|
groups[i].state = PKGQUEUED;
|
|
groups[i].runstartfailed = false;
|
|
i += 1;
|
|
};
|
|
let planlaunched: i32 = 0;
|
|
let plancompleted: i32 = 0;
|
|
let productcompleted: i32 = 0;
|
|
if (compileonly) { productcompleted = groups.len; };
|
|
let active: i32 = 0;
|
|
for (plancompleted < plans.len || productcompleted < groups.len) {
|
|
// Fill free slots with already-built products first, then the next
|
|
// byte-sorted directory build. Missing product markers are completed
|
|
// build failures and consume no process slot.
|
|
let filling: bool = true;
|
|
for (filling && active < jobs) {
|
|
filling = false;
|
|
if (!compileonly) {
|
|
let gi: i32 = 0;
|
|
for (gi < groups.len) {
|
|
let g: *pkggroup = &groups[gi];
|
|
if (g.state == PKGQUEUED
|
|
&& plans[g.plan].state == PKGDONE) {
|
|
if (!pkgproductbuilt(g, buildonly)) {
|
|
g.state = PKGDONE;
|
|
productcompleted += 1;
|
|
} else if (g.notests) {
|
|
g.state = PKGDONE;
|
|
productcompleted += 1;
|
|
} else {
|
|
if (!pkgstartrun(g, filters, timeoutarg, list,
|
|
builder, &runhandles[gi])) {
|
|
g.runstartfailed = true;
|
|
g.state = PKGDONE;
|
|
productcompleted += 1;
|
|
} else {
|
|
g.state = PKGRUNNING;
|
|
active += 1;
|
|
};
|
|
};
|
|
filling = true;
|
|
break;
|
|
};
|
|
gi += 1;
|
|
};
|
|
};
|
|
if (!filling && planlaunched < plans.len) {
|
|
if (!pkgstartbuild(&plans[planlaunched], groups, builder,
|
|
includes, libdirs, libs, &handles[planlaunched])) {
|
|
if (!pkgremoveall(tmproot)) {
|
|
pkgput(os.STDERR_FILENO,
|
|
"wwtest package: cleanup failed; retained ");
|
|
pkgputln(os.STDERR_FILENO, tmproot);
|
|
};
|
|
return 1;
|
|
};
|
|
plans[planlaunched].state = PKGBUILDING;
|
|
active += 1;
|
|
planlaunched += 1;
|
|
filling = true;
|
|
};
|
|
};
|
|
let k: i32 = 0;
|
|
for (k < planlaunched) {
|
|
let p: *pkgplan = &plans[k];
|
|
if (p.state == PKGBUILDING && exec.poll(&handles[k])) {
|
|
p.buildres = handles[k].result;
|
|
p.state = PKGDONE;
|
|
active -= 1;
|
|
plancompleted += 1;
|
|
};
|
|
k += 1;
|
|
};
|
|
if (!compileonly) {
|
|
k = 0;
|
|
for (k < groups.len) {
|
|
let g: *pkggroup = &groups[k];
|
|
if (g.state == PKGRUNNING && exec.poll(&runhandles[k])) {
|
|
g.runres = runhandles[k].result;
|
|
g.state = PKGDONE;
|
|
active -= 1;
|
|
productcompleted += 1;
|
|
};
|
|
k += 1;
|
|
};
|
|
};
|
|
if (active > 0) { time.sleep(pkgpoll, time.clock.monotonic); };
|
|
};
|
|
|
|
// A failed, signalled, timed-out, or unstartable run propagates to its Go
|
|
// install action, so it preserves any prior retained binary. Successful
|
|
// products install independently after their private execution.
|
|
if (!compileonly) {
|
|
i = 0;
|
|
for (i < groups.len) {
|
|
let g: *pkggroup = &groups[i];
|
|
if (g.publish.len != 0 && !g.notests
|
|
&& pkgproductbuilt(g, buildonly) && !g.runstartfailed
|
|
&& pkgrunok(g)) {
|
|
pkginstalloutput(g, builder);
|
|
};
|
|
i += 1;
|
|
};
|
|
};
|
|
|
|
i = 0;
|
|
for (i < plans.len) {
|
|
failed += pkgemitplan(&plans[i], groups, compileonly, buildonly,
|
|
&statusfailed);
|
|
i += 1;
|
|
};
|
|
if (!pkgremoveall(tmproot)) {
|
|
pkgput(os.STDERR_FILENO, "wwtest package: cleanup failed; retained ");
|
|
pkgputln(os.STDERR_FILENO, tmproot);
|
|
failed += 1;
|
|
};
|
|
if (statusfailed && explicitstatus && !compileonly) {
|
|
pkgputln(os.STDOUT_FILENO, "FAIL");
|
|
};
|
|
if (failed != 0) { return 1; };
|
|
return 0;
|
|
};
|