lib/dirs+lib/os+test: add lib/dirs (XDG paths) + os.mkdirs
Port Hare's lib/dirs (ref/hare/dirs/xdg.ha) — XDG base-directory
lookup with mkdir-on-demand:
dirs.config(prog) — XDG_CONFIG_HOME/<prog>, fallback $HOME/.config/<prog>
dirs.cache(prog) — XDG_CACHE_HOME/<prog>, fallback $HOME/.cache/<prog>
dirs.data(prog) — XDG_DATA_HOME/<prog>, fallback $HOME/.local/share/<prog>
dirs.state(prog) — XDG_STATE_HOME/<prog>, fallback $HOME/.local/state/<prog>
Static-buffer return (256B pathbuf), overwritten on the next dirs.*
call — same contract as lib/temp. Fallback triggers on unset, empty,
or non-absolute XDG var (matches Hare's path::abs check). HOME-unset
rt_aborts (matches Hare's `as str` panic on missing HOME).
os.mkdirs(path, mode) — recursive mkdir, EEXIST-silenced. Walks the
path replacing each '/' with NUL, mkdir'ing each prefix, restoring
the slash. Path bytes must be writable (documented).
Surface skips with reason notes in dirs.ww header:
- runtime() — needs stat+getuid; defer until lib/os has them
- XDG_CONFIG_DIRS / XDG_DATA_DIRS — not in Hare's xdg.ha
- fmt::fatalf-on-mkdir — wired to rt_abort until {n}-placeholder
fmt lands
Cohort coverage in lib/dirs/dirstest.ww + test/wcc/975_dirs_run.c
(mkdtemp-rooted env state): XDG-absolute / XDG-non-absolute fallback /
XDG-unset×2.
This commit is contained in:
6
Makefile
6
Makefile
@@ -234,7 +234,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
|||||||
$(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \
|
$(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \
|
||||||
$(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \
|
$(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \
|
||||||
$(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \
|
$(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \
|
||||||
$(BIN)/test_shlex_run $(BIN)/test_getenv_run \
|
$(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \
|
||||||
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
|
$(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \
|
||||||
$(BIN)/test_base32_run $(BIN)/test_base64_run \
|
$(BIN)/test_base32_run $(BIN)/test_base64_run \
|
||||||
$(BIN)/test_adler32_run $(BIN)/test_crc16_run \
|
$(BIN)/test_adler32_run $(BIN)/test_crc16_run \
|
||||||
@@ -452,6 +452,10 @@ $(BIN)/test_getenv_run: test/wcc/974_getenv_run.c $(BIN)/ww $(BIN)/w6c \
|
|||||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
$(BIN)/test_dirs_run: test/wcc/975_dirs_run.c $(BIN)/ww $(BIN)/w6c \
|
||||||
|
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
$(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \
|
$(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \
|
||||||
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|||||||
187
lib/dirs/dirs.ww
Normal file
187
lib/dirs/dirs.ww
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
// dirs — XDG base directory paths. Port of Hare's lib/dirs
|
||||||
|
// (ref/hare/dirs/xdg.ha) using the lib/temp-style static `[256]u8`
|
||||||
|
// pathbuf in place of Hare's `path::buffer` (lib/path doesn't ship
|
||||||
|
// a buffer type yet).
|
||||||
|
//
|
||||||
|
// Surface today (1:1 with Hare's xdg.ha minus runtime()):
|
||||||
|
//
|
||||||
|
// dirs.config(prog: str) str — XDG_CONFIG_HOME/<prog>, fallback $HOME/.config/<prog>
|
||||||
|
// dirs.cache(prog: str) str — XDG_CACHE_HOME/<prog>, fallback $HOME/.cache/<prog>
|
||||||
|
// dirs.data(prog: str) str — XDG_DATA_HOME/<prog>, fallback $HOME/.local/share/<prog>
|
||||||
|
// dirs.state(prog: str) str — XDG_STATE_HOME/<prog>, fallback $HOME/.local/state/<prog>
|
||||||
|
//
|
||||||
|
// Returns are static-buffer views — borrowed for the lifetime of
|
||||||
|
// the next dirs call (any of the four above). Callers needing the
|
||||||
|
// bytes to outlive the next call duplicate via [[strings.dup]].
|
||||||
|
// Same precedent as Hare's dirs:: (which doc-strings the same
|
||||||
|
// "overwritten on subsequent calls" contract).
|
||||||
|
//
|
||||||
|
// Lookup algorithm (mirrors Hare's xdg.ha:lookup):
|
||||||
|
//
|
||||||
|
// 1. If $XDG_<NAME>_HOME is set AND its first byte is '/'
|
||||||
|
// (Hare's path::abs check), return "<XDG>/<prog>" after
|
||||||
|
// mkdir-recursive (mode 0o755).
|
||||||
|
// 2. Otherwise return "$HOME/<default>/<prog>" after mkdir-
|
||||||
|
// recursive. The non-absolute and unset/empty XDG cases both
|
||||||
|
// fall through to this branch — matches Hare's `yield`
|
||||||
|
// after the `path::abs` test.
|
||||||
|
//
|
||||||
|
// rt_aborts in two cases (matches Hare's `as str` cast on missing
|
||||||
|
// HOME, plus its `fs::strerror` fatal on mkdir failure):
|
||||||
|
//
|
||||||
|
// - HOME is not set
|
||||||
|
// - mkdirs(path, 0o755) fails for a non-EEXIST reason
|
||||||
|
//
|
||||||
|
// Skipped from v1 (with reasons preserved for the next graduator):
|
||||||
|
//
|
||||||
|
// - runtime() — needs lib/os.stat + a getuid primitive for the
|
||||||
|
// uid/perm/isdir verification Hare's runtime() does. A relaxed
|
||||||
|
// env-only version would defeat the perm contract that's the
|
||||||
|
// entire point of XDG_RUNTIME_DIR. Defer (drew/rob aligned).
|
||||||
|
//
|
||||||
|
// - XDG_CONFIG_DIRS / XDG_DATA_DIRS — system search paths. Hare's
|
||||||
|
// xdg.ha doesn't ship them; lib/CLAUDE.md prohibits richer-
|
||||||
|
// than-Hare surfaces. Defer until upstream Hare adds them.
|
||||||
|
//
|
||||||
|
// - lib/fmt's fatalf-style error reporting on mkdir failure. Hare
|
||||||
|
// uses `fmt::fatalf("Error creating {}: {}", path, ...)`; ww's
|
||||||
|
// lib/fmt is print-string-only (no {n}-placeholder parser), so
|
||||||
|
// we hand the bare context "dirs: mkdirs failed" to rt_abort.
|
||||||
|
// Graduates when the {n}-placeholder parser lands.
|
||||||
|
|
||||||
|
use os;
|
||||||
|
|
||||||
|
@symbol("rt_abort") fn rtabort(msg: str) void;
|
||||||
|
|
||||||
|
// pathbuf — module-level scratch path. Sized for any
|
||||||
|
// "<HOME>/.local/share/<prog>" composition under reasonable
|
||||||
|
// HOME and prog lengths; dirs returns a view into pathbuf[0..pathlen].
|
||||||
|
// NUL byte at pathbuf[pathlen] for direct handoff to [[os.mkdirs]]
|
||||||
|
// (same precedent as lib/temp).
|
||||||
|
let pathbuf: [256]u8;
|
||||||
|
let pathlen: i32 = 0;
|
||||||
|
|
||||||
|
// SEP — '/' byte. Same constant as lib/path's SEP.
|
||||||
|
def SEP: u8 = 47u8;
|
||||||
|
|
||||||
|
// MODE_0755 — directory creation mode passed to [[os.mkdirs]].
|
||||||
|
// Hare uses the literal `0o755` at every call site; ww doesn't ship
|
||||||
|
// octal literals, so we name the constant once.
|
||||||
|
def MODE_0755: i32 = 493i32;
|
||||||
|
|
||||||
|
// puts — append `s` to pathbuf at offset `off`, capping against the
|
||||||
|
// buffer's capacity to leave room for the trailing NUL. Returns the
|
||||||
|
// new offset. Same shape as lib/temp.puts.
|
||||||
|
fn puts(off: i32, s: str) i32 = {
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
if (off + i >= 255) { break; };
|
||||||
|
pathbuf[off + i] = s[i];
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return off + i;
|
||||||
|
};
|
||||||
|
|
||||||
|
// build — assemble "<base>/<sub>/<prog>" into pathbuf, NUL-terminate,
|
||||||
|
// and store the length in [[pathlen]]. If `sub` is empty, the
|
||||||
|
// "/<sub>" segment is skipped and the result is "<base>/<prog>".
|
||||||
|
// Embedded '/' in `sub` (e.g. ".local/share") is fine — [[os.mkdirs]]
|
||||||
|
// handles intermediate dirs.
|
||||||
|
fn build(base: str, sub: str, prog: str) void = {
|
||||||
|
let off: i32 = 0;
|
||||||
|
off = puts(off, base);
|
||||||
|
if (sub.len > 0) {
|
||||||
|
if (off < 255) { pathbuf[off] = SEP; off += 1; };
|
||||||
|
off = puts(off, sub);
|
||||||
|
};
|
||||||
|
if (off < 255) { pathbuf[off] = SEP; off += 1; };
|
||||||
|
off = puts(off, prog);
|
||||||
|
if (off > 255) { off = 255; };
|
||||||
|
pathbuf[off] = 0u8;
|
||||||
|
pathlen = off;
|
||||||
|
};
|
||||||
|
|
||||||
|
// view — return a `str` view into pathbuf[0..pathlen]. Borrowed
|
||||||
|
// for the lifetime of the next dirs call.
|
||||||
|
fn view() str = {
|
||||||
|
let r: str;
|
||||||
|
r.ptr = &pathbuf[0];
|
||||||
|
r.len = pathlen;
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ensure — wrap [[os.mkdirs]] with the rt_abort-on-non-EEXIST
|
||||||
|
// behaviour Hare's lookup uses (`fmt::fatalf` on the HOME branch).
|
||||||
|
// Centralised so both branches in [[lookup]] share the error path.
|
||||||
|
fn ensure() void = {
|
||||||
|
match (os.mkdirs(&pathbuf[0], MODE_0755)) {
|
||||||
|
case void => {};
|
||||||
|
case let _e: os.oserror => rtabort("dirs: mkdirs failed");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// lookup — Hare's lookup() inlined: probe $envvar, fall through to
|
||||||
|
// $HOME/<dflt> on unset / empty / non-absolute XDG values. Auto-
|
||||||
|
// mkdirs the result. rt_aborts if HOME is unset (no fallback at
|
||||||
|
// the bottom of the chain — matches Hare's `as str` cast which
|
||||||
|
// would also fault on missing HOME).
|
||||||
|
fn lookup(prog: str, envvar: str, dflt: str) str = {
|
||||||
|
match (os.getenv(envvar)) {
|
||||||
|
case let xdg: str => {
|
||||||
|
if (xdg.len > 0) {
|
||||||
|
if (xdg[0] == SEP) { // Hare's path::abs(path)
|
||||||
|
build(xdg, "", prog);
|
||||||
|
ensure();
|
||||||
|
return view();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
case void => {};
|
||||||
|
};
|
||||||
|
let home: str;
|
||||||
|
match (os.getenv("HOME")) {
|
||||||
|
case let h: str => { home = h; };
|
||||||
|
case void => rtabort("dirs: HOME is not set");
|
||||||
|
};
|
||||||
|
build(home, dflt, prog);
|
||||||
|
ensure();
|
||||||
|
return view();
|
||||||
|
};
|
||||||
|
|
||||||
|
// config — directory suitable for storing config files for `prog`.
|
||||||
|
// $XDG_CONFIG_HOME/<prog> if set+absolute, else $HOME/.config/<prog>.
|
||||||
|
// Auto-created with mode 0o755. Returns a borrowed str view into
|
||||||
|
// the module-level pathbuf; subsequent dirs calls overwrite it.
|
||||||
|
//
|
||||||
|
// Mirrors Hare's dirs::config (xdg.ha:47).
|
||||||
|
export fn config(prog: str) str = {
|
||||||
|
return lookup(prog, "XDG_CONFIG_HOME", ".config");
|
||||||
|
};
|
||||||
|
|
||||||
|
// cache — directory suitable for cache files for `prog`.
|
||||||
|
// $XDG_CACHE_HOME/<prog> if set+absolute, else $HOME/.cache/<prog>.
|
||||||
|
//
|
||||||
|
// Mirrors Hare's dirs::cache (xdg.ha:53).
|
||||||
|
export fn cache(prog: str) str = {
|
||||||
|
return lookup(prog, "XDG_CACHE_HOME", ".cache");
|
||||||
|
};
|
||||||
|
|
||||||
|
// data — directory suitable for persistent data files for `prog`.
|
||||||
|
// $XDG_DATA_HOME/<prog> if set+absolute, else $HOME/.local/share/<prog>.
|
||||||
|
// Hare composes the default via path::set(.local, share); we emit
|
||||||
|
// the composed string directly since lib/path doesn't ship a
|
||||||
|
// path::buffer type yet.
|
||||||
|
//
|
||||||
|
// Mirrors Hare's dirs::data (xdg.ha:59).
|
||||||
|
export fn data(prog: str) str = {
|
||||||
|
return lookup(prog, "XDG_DATA_HOME", ".local/share");
|
||||||
|
};
|
||||||
|
|
||||||
|
// state — directory suitable for storing program state files for
|
||||||
|
// `prog`. $XDG_STATE_HOME/<prog> if set+absolute, else
|
||||||
|
// $HOME/.local/state/<prog>.
|
||||||
|
//
|
||||||
|
// Mirrors Hare's dirs::state (xdg.ha:69).
|
||||||
|
export fn state(prog: str) str = {
|
||||||
|
return lookup(prog, "XDG_STATE_HOME", ".local/state");
|
||||||
|
};
|
||||||
140
lib/dirs/dirstest.ww
Normal file
140
lib/dirs/dirstest.ww
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// dirstest — exercises lib/dirs against an env state pre-arranged by
|
||||||
|
// the C driver (test/wcc/975_dirs_run.c).
|
||||||
|
//
|
||||||
|
// Pre-arranged env (set via setenv/unsetenv before exec'ing
|
||||||
|
// `ww run lib/dirs/dirstest.ww`; see 975_dirs_run.c):
|
||||||
|
//
|
||||||
|
// HOME = <tmpl> (mkdtemp'd /tmp/wwdirs-XXXXXX)
|
||||||
|
// XDG_CONFIG_HOME = <tmpl>/cfg (absolute → XDG branch)
|
||||||
|
// XDG_CACHE_HOME = "relative/path" (non-abs → fallback to HOME)
|
||||||
|
// XDG_DATA_HOME unset (fallback to HOME)
|
||||||
|
// XDG_STATE_HOME unset (fallback to HOME)
|
||||||
|
// WW_TEST_TMPDIR = <tmpl> (so this fixture can compute
|
||||||
|
// the expected paths)
|
||||||
|
//
|
||||||
|
// Cohorts (one @test fn per row, in numeric order from main):
|
||||||
|
//
|
||||||
|
// 1. config("myapp") — XDG-set+absolute → <tmpl>/cfg/myapp
|
||||||
|
// 2. cache("myapp") — XDG-set+relative → <tmpl>/.cache/myapp (abs-check fallback)
|
||||||
|
// 3. data("myapp") — XDG-unset → <tmpl>/.local/share/myapp (HOME fallback)
|
||||||
|
// 4. state("myapp") — XDG-unset → <tmpl>/.local/state/myapp (HOME fallback)
|
||||||
|
//
|
||||||
|
// Static-buffer gotcha: dirs.config / dirs.cache / dirs.data /
|
||||||
|
// dirs.state all return views into the same module-level pathbuf.
|
||||||
|
// Each @test calls EXACTLY ONE dirs.* function, builds its expected
|
||||||
|
// string into a separate `scratch: [256]u8` BEFORE that call, and
|
||||||
|
// asserts immediately. Don't call two dirs.* fns and compare across
|
||||||
|
// — the second clobbers the first's view.
|
||||||
|
//
|
||||||
|
// `signalled` global bumps before each test so a failing exit code
|
||||||
|
// pinpoints the offending row (WEXITSTATUS = signalled + 10). Same
|
||||||
|
// shape as temptest / shlextest / fnmatchtest / ostest.
|
||||||
|
//
|
||||||
|
// Don't `use io;` here — lib/os and lib/io collide on read/write/
|
||||||
|
// close (task #17). We need only os.getenv + os.exit.
|
||||||
|
|
||||||
|
use dirs;
|
||||||
|
use os;
|
||||||
|
|
||||||
|
let signalled: i32 = 0;
|
||||||
|
|
||||||
|
fn fail() void = { os.exit(signalled + 10); };
|
||||||
|
|
||||||
|
fn streq(a: str, b: str) bool = {
|
||||||
|
if (a.len != b.len) { return false; };
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < a.len) {
|
||||||
|
if (a[i] != b[i]) { return false; };
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// scratch — local buffer for expected-path construction. Independent
|
||||||
|
// of dirs.pathbuf; both views stay valid through the streq compare
|
||||||
|
// because they live in different buffers.
|
||||||
|
let scratch: [256]u8;
|
||||||
|
|
||||||
|
fn putscratch(off: i32, s: str) i32 = {
|
||||||
|
let i: i32 = 0;
|
||||||
|
for (i < s.len) {
|
||||||
|
if (off + i >= 255) { break; };
|
||||||
|
scratch[off + i] = s[i];
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
return off + i;
|
||||||
|
};
|
||||||
|
|
||||||
|
// expected — build "<tmpl>/<sub>/<prog>" into scratch and return a
|
||||||
|
// view. Mirrors dirs.build but writes into our scratch buffer so
|
||||||
|
// the expected and got views can be compared without aliasing.
|
||||||
|
fn expected(tmpl: str, sub: str, prog: str) str = {
|
||||||
|
let off: i32 = 0;
|
||||||
|
off = putscratch(off, tmpl);
|
||||||
|
if (off < 255) { scratch[off] = 47u8; off += 1; }; // '/'
|
||||||
|
off = putscratch(off, sub);
|
||||||
|
if (off < 255) { scratch[off] = 47u8; off += 1; };
|
||||||
|
off = putscratch(off, prog);
|
||||||
|
let r: str;
|
||||||
|
r.ptr = &scratch[0];
|
||||||
|
r.len = off;
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
|
fn gettmpdir() str = {
|
||||||
|
match (os.getenv("WW_TEST_TMPDIR")) {
|
||||||
|
case let s: str => return s;
|
||||||
|
case void => {
|
||||||
|
fail();
|
||||||
|
let empty: str;
|
||||||
|
return empty;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 1: XDG_CONFIG_HOME=<tmpl>/cfg → <tmpl>/cfg/myapp --------------
|
||||||
|
|
||||||
|
@test fn test_config_xdg_set() void = {
|
||||||
|
let tmpl = gettmpdir();
|
||||||
|
let exp = expected(tmpl, "cfg", "myapp");
|
||||||
|
let got = dirs.config("myapp");
|
||||||
|
if (!streq(got, exp)) { fail(); };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 2: XDG_CACHE_HOME="relative/path" → fallback to $HOME/.cache --
|
||||||
|
//
|
||||||
|
// Exercises Hare's `if (!path::abs(path)) yield;` branch — a
|
||||||
|
// non-absolute XDG_*_HOME silently falls through to the HOME path.
|
||||||
|
|
||||||
|
@test fn test_cache_xdg_relative_fallback() void = {
|
||||||
|
let tmpl = gettmpdir();
|
||||||
|
let exp = expected(tmpl, ".cache", "myapp");
|
||||||
|
let got = dirs.cache("myapp");
|
||||||
|
if (!streq(got, exp)) { fail(); };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 3: XDG_DATA_HOME unset → fallback to $HOME/.local/share -------
|
||||||
|
|
||||||
|
@test fn test_data_unset_fallback() void = {
|
||||||
|
let tmpl = gettmpdir();
|
||||||
|
let exp = expected(tmpl, ".local/share", "myapp");
|
||||||
|
let got = dirs.data("myapp");
|
||||||
|
if (!streq(got, exp)) { fail(); };
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 4: XDG_STATE_HOME unset → fallback to $HOME/.local/state ------
|
||||||
|
|
||||||
|
@test fn test_state_unset_fallback() void = {
|
||||||
|
let tmpl = gettmpdir();
|
||||||
|
let exp = expected(tmpl, ".local/state", "myapp");
|
||||||
|
let got = dirs.state("myapp");
|
||||||
|
if (!streq(got, exp)) { fail(); };
|
||||||
|
};
|
||||||
|
|
||||||
|
export fn main() i32 = {
|
||||||
|
signalled = 1; test_config_xdg_set();
|
||||||
|
signalled = 2; test_cache_xdg_relative_fallback();
|
||||||
|
signalled = 3; test_data_unset_fallback();
|
||||||
|
signalled = 4; test_state_unset_fallback();
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
43
lib/os/os.ww
43
lib/os/os.ww
@@ -193,6 +193,49 @@ export fn rmdir(path: *u8) i32 = {
|
|||||||
return syscall1(nr.RMDIR, path: i64): i32;
|
return syscall1(nr.RMDIR, path: i64): i32;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// mkdirs — recursive mkdir. Creates `path` and any non-existent
|
||||||
|
// parent directories with the given mode. EEXIST is silently
|
||||||
|
// accepted (matches Hare's `errors::exists` skip in os::mkdirs);
|
||||||
|
// any other syscall failure surfaces as `oserror`.
|
||||||
|
//
|
||||||
|
// `path` must be NUL-terminated AND its bytes must be writable —
|
||||||
|
// mkdirs temporarily replaces '/' separators with NUL while
|
||||||
|
// invoking [[mkdir]] on each prefix, then restores them. Pointing
|
||||||
|
// `path` at a string literal will segfault. Callers hold the bytes
|
||||||
|
// in a writable buffer (rt_alloc'd, a static `[N]u8`, etc.) — same
|
||||||
|
// precedent as [[temp.named]]'s pathbuf.
|
||||||
|
//
|
||||||
|
// Mirrors Hare's os::mkdirs (recursive variant of os::mkdir).
|
||||||
|
export fn mkdirs(path: *u8, mode: i32) (void | oserror) = {
|
||||||
|
// Find the path length (excluding trailing NUL).
|
||||||
|
let n: i32 = 0;
|
||||||
|
for (path[n] != 0u8) { n += 1; };
|
||||||
|
if (n == 0) { return; };
|
||||||
|
|
||||||
|
// Walk forward; at each '/' boundary, NUL-terminate the prefix,
|
||||||
|
// mkdir it, restore the slash, continue. Skip index 0 so a
|
||||||
|
// leading '/' on absolute paths doesn't trigger an empty mkdir.
|
||||||
|
let i: i32 = 1;
|
||||||
|
for (i < n) {
|
||||||
|
if (path[i] == 47u8) { // '/'
|
||||||
|
path[i] = 0u8;
|
||||||
|
let r: i32 = mkdir(path, mode);
|
||||||
|
path[i] = 47u8;
|
||||||
|
if (r < 0) {
|
||||||
|
if (r != -17) { return r: i64: oserror; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
i += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// mkdir the full path.
|
||||||
|
let r: i32 = mkdir(path, mode);
|
||||||
|
if (r < 0) {
|
||||||
|
if (r != -17) { return r: i64: oserror; };
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
// getpid(2). Used by the driver to mint unique scratch paths.
|
// getpid(2). Used by the driver to mint unique scratch paths.
|
||||||
export fn getpid() i32 = {
|
export fn getpid() i32 = {
|
||||||
return syscall0(nr.GETPID): i32;
|
return syscall0(nr.GETPID): i32;
|
||||||
|
|||||||
84
test/wcc/975_dirs_run.c
Normal file
84
test/wcc/975_dirs_run.c
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* 975_dirs_run — execute the lib/dirs @test fixture under the C-side
|
||||||
|
* `ww run` driver and assert exit 0.
|
||||||
|
*
|
||||||
|
* Workflow:
|
||||||
|
* 1. mkdtemp /tmp/wwdirs-XXXXXX — fresh per run, no cross-run
|
||||||
|
* stale state, no parallel-test conflicts.
|
||||||
|
* 2. setenv the four cohort env states (see lib/dirs/dirstest.ww
|
||||||
|
* file header for the contract).
|
||||||
|
* 3. exec `ww run lib/dirs/dirstest.ww`. The fixture reads
|
||||||
|
* WW_TEST_TMPDIR + the XDG_/HOME envs and asserts.
|
||||||
|
* 4. system("rm -rf <tmpl>") to reclaim the tmpdir + the
|
||||||
|
* auto-created child dirs (dirs.* mkdir-recursive on the way
|
||||||
|
* down, so the directory tree is non-empty by test exit).
|
||||||
|
*
|
||||||
|
* Same wrapper shape as 970-974; cleanup-on-failure is rm-rf'd so a
|
||||||
|
* test failure never leaves residue in /tmp.
|
||||||
|
*/
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
static int
|
||||||
|
runwait(const char *cmd)
|
||||||
|
{
|
||||||
|
int rc = system(cmd);
|
||||||
|
if (rc == -1) return -1;
|
||||||
|
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main(void)
|
||||||
|
{
|
||||||
|
const char *bin = getenv("BIN");
|
||||||
|
if (!bin) bin = "out/bin";
|
||||||
|
char absbin[1024];
|
||||||
|
if (bin[0] != '/') {
|
||||||
|
char cwd[1024];
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||||
|
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||||
|
bin = absbin;
|
||||||
|
}
|
||||||
|
char cwd[1024];
|
||||||
|
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||||
|
|
||||||
|
/* mkdtemp the per-run scratch root. */
|
||||||
|
char tmpl[64];
|
||||||
|
strcpy(tmpl, "/tmp/wwdirs-XXXXXX");
|
||||||
|
if (mkdtemp(tmpl) == NULL) {
|
||||||
|
perror("mkdtemp");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pre-arrange env. The child `ww run` inherits these. */
|
||||||
|
char cfg[128];
|
||||||
|
snprintf(cfg, sizeof cfg, "%s/cfg", tmpl);
|
||||||
|
setenv("HOME", tmpl, 1);
|
||||||
|
setenv("XDG_CONFIG_HOME", cfg, 1);
|
||||||
|
setenv("XDG_CACHE_HOME", "relative/path", 1); /* non-abs → fallback */
|
||||||
|
unsetenv("XDG_DATA_HOME");
|
||||||
|
unsetenv("XDG_STATE_HOME");
|
||||||
|
setenv("WW_TEST_TMPDIR", tmpl, 1);
|
||||||
|
|
||||||
|
const char *src = "lib/dirs/dirstest.ww";
|
||||||
|
char path[1024], cmd[2048];
|
||||||
|
snprintf(path, sizeof path, "%s/%s", cwd, src);
|
||||||
|
snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path);
|
||||||
|
int rc = runwait(cmd);
|
||||||
|
|
||||||
|
/* Cleanup regardless of pass/fail — no residue under /tmp. */
|
||||||
|
char rmcmd[256];
|
||||||
|
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpl);
|
||||||
|
(void)system(rmcmd);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
fprintf(stderr, "dirs_run FAIL: %s exited %d\n", src, rc);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("dirs_run: %s ok\n", src);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user