diff --git a/.omo/evidence/task-17-tofu-core.log b/.omo/evidence/task-17-tofu-core.log new file mode 100644 index 0000000..898a352 --- /dev/null +++ b/.omo/evidence/task-17-tofu-core.log @@ -0,0 +1,152 @@ + Warning + Warning ## Warning for package tofu ## + Warning + Warning The following compiler flags have been specified in the package description + Warning file. They are handled by DUB and direct use in packages is discouraged. + Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags + Warning to the compiler, or use one of the suggestions below: + Warning + Warning warningsAsErrors: Use "buildRequirements" to control the warning level + Warning + Generating test runner configuration 'tofu-test-application' for 'application' (executable). + Warning Excluding main source file src/main.d from test. + Starting Performing "unittest" build using /usr/bin/dmd for x86_64. + Up-to-date toml 1.0.0: target for configuration [library] is up to date. + Building tofu ~main: building configuration [tofu-test-application] + Linking tofu-test-application + Finished To force a rebuild of up-to-date targets, run again with --force + Running tofu-test-application +zeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasedonezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depB...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building target...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phaseerror build failed for depA: build failed for depA: +zeta-makepkg: building depA... +zeta-makepkg: configure phase +zeta-makepkg: build phase +zeta-makepkg: install phase +zeta-makepkg: building pkg...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phaseerror build failed for noexist: recipe not found at /tmp/tofu-test-build-buildall-norecipe-216783/nonexistent.recipe +warn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2) +Warning: malformed TOML config at /tmp/tofu-test-config-bad-216783.toml: Invalid table key declaration (2:0) +Warning: invalid TOFU_DEFAULT_JOBS 'not-a-number', using default 1 +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 - +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/package.lua HTTP/1.1" 200 - +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/build.sh HTTP/1.1" 200 - +127.0.0.1 - - [08/Aug/2026 18:12:13] code 404, message File not found +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 - +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 - +127.0.0.1 - - [08/Aug/2026 18:12:13] code 404, message File not found +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/package.lua HTTP/1.1" 404 - +127.0.0.1 - - [08/Aug/2026 18:12:13] code 404, message File not found +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/build.sh HTTP/1.1" 404 - +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 - +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 - +127.0.0.1 - - [08/Aug/2026 18:12:13] code 404, message File not found +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/package.lua HTTP/1.1" 404 - +127.0.0.1 - - [08/Aug/2026 18:12:13] code 404, message File not found +127.0.0.1 - - [08/Aug/2026 18:12:13] "GET /recipes/hello/build.sh HTTP/1.1" 404 - +warn skipping index entry with empty name +warn skipping index entry with empty name +warn skipping index entry 'badpool': invalid pool 'bad_pool_value' + . checking binary firefox ... + - binary firefox-2.1.0 satisfies firefox>=2.0 + . checking binary libfoo ... + - binary libfoo-1.9 does not satisfy libfoo>=2.0 + . checking binary noexist ... + . checking binary brokenpkg ... + . checking binary badpkg ... + . checking binary testpkg ... + - binary testpkg-2.1.0 satisfies testpkg +==> ──── building depA (1/3) ──── + ok depA +==> ──── building depB (2/3) ──── + ok depB +==> ──── building target (3/3) ──── + ok target +==> ──── building depA (1/3) ──── + - nothing to build +==> ──── building pkg (1/1) ──── + - pkg already built, skipping +==> ──── building pkg (1/1) ──── + ok pkg +==> fetching recipe hello + . downloading hello.recipe + . downloading package.lua + . downloading build.sh + ok recipe hello downloaded +==> fetching recipe hello + . downloading hello.recipe +==> fetching recipe hello + . downloading hello.recipe + . downloading package.lua + - no package.lua for hello (optional manifest) + . downloading build.sh + - no build.sh for hello (optional build script) + . custom build system: fetching scripts/build.sh + ok recipe hello downloaded +==> fetching recipe hello + . downloading hello.recipe + . downloading package.lua + - no package.lua for hello (optional manifest) + . downloading build.sh + - no build.sh for hello (optional build script) + ok recipe hello downloaded +==> fetching recipe hello + . downloading hello.recipe + . fetching index http://127.0.0.1:50721//index.lua + ok index loaded: 3 packages + . fetching index http://127.0.0.1:47569//index.lua + . fetching index http://127.0.0.1:42797//index.lua + . fetching index http://127.0.0.1:42795//index.lua + . fetching index http://127.0.0.1:51365//index.lua + ok index loaded: 0 packages + . fetching index http://127.0.0.1:51389//index.lua + ok index loaded: 1 packages + . fetching index http://127.0.0.1:42841//index.lua + ok index loaded: 2 packages + . fetching index http://127.0.0.1:59485//index.lua + . fetching index http://127.0.0.1:35061//index.lua + ok index loaded: 1 packages +installing package...error: build failedreason: missing dependencytestpkg-2.1 is already installed -- use -ReProvide to reinstall - already installed — skipping + ok installed A with 2 dependencies + - nothing to install +install failed: conflict detectedtestpkg-2.1 is already installed -- use -ReProvide to reinstall13 modules passed unittests + - already installed — skipping + ok installed A with 0 dependencies + - binary libfoo-2.1 satisfies libfoo>=2.0 + - libfoo: binary 1.9 too old, building from recipe + - libfoo: binary 1.9 too old, building from recipe + - binary libbar-3.0 satisfies libbar (unconstrained) + - libbaz: no binary available, building from recipe + - binary libfoo-1.5 satisfies libfoo>=1.0 + - libfoo: binary 1.5 too old, building from recipe + - binary parentA-1.5 satisfies parentA (unconstrained) + - binary parentB-1.5 satisfies parentB (unconstrained) +==> generating build plan + - + C (recipe) + - + A (recipe) + ok build plan: 2 packages +==> generating build plan + - recipe libfoo not in cache, re-fetching + - + libfoo (recipe) + ok build plan: 1 packages +==> generating build plan +==> generating build plan + - nothing to build (all binary) +==> generating build plan + - + mypkg (recipe) + ok build plan: 1 packages +==> generating build plan + - + libfoo (recipe) + - + mypkg (recipe) + ok build plan: 2 packages + Warning + Warning ## Warning for package tofu ## + Warning + Warning The following compiler flags have been specified in the package description + Warning file. They are handled by DUB and direct use in packages is discouraged. + Warning Alternatively, you can set the DFLAGS environment variable to pass custom flags + Warning to the compiler, or use one of the suggestions below: + Warning + Warning warningsAsErrors: Use "buildRequirements" to control the warning level + Warning + Starting Performing "debug" build using /usr/bin/dmd for x86_64. + Up-to-date toml 1.0.0: target for configuration [library] is up to date. + Up-to-date tofu ~main: target for configuration [application] is up to date. + Finished To force a rebuild of up-to-date targets, run again with --force diff --git a/.omo/notepads/tofu-core/learnings.md b/.omo/notepads/tofu-core/learnings.md index 6d5ce10..52c7696 100644 --- a/.omo/notepads/tofu-core/learnings.md +++ b/.omo/notepads/tofu-core/learnings.md @@ -684,3 +684,54 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o - `dub test` passes — all 13 modules, including 6 new buildAll unittests. - `dub build` passes with `warningsAsErrors`. - Evidence logged to `.omo/evidence/task-15-tofu-core.log`. + +--- + +## Task 17 — `tofu.install.installAll` (install orchestrator) + +### Architecture +- `installAll(BuildPlan plan, Config cfg)` added to `tofu.install` — single root-package `-LocalProvide` call. +- Imports added: `tofu.types` (selective: `BuildPlan`, `BuildPlanEntry`, `Source`), `std.conv : to`, `std.file : exists`. +- Reuses `runLocalProvide` — no new process-spawning logic. + +### Algorithm +1. **Empty plan** → `logInfo("nothing to install")`, return early. +2. **Verify built cache**: for each `Source.recipe` entry, check `cfg.builtPackagesDir()/name/package.lua` exists. Missing → `InstallException("built package missing from cache: (was the build skipped?)")`. +3. **Root = last entry** in `plan.order()` — matches `deps.resolve`/`generateBuildPlan` convention (topological order, root last). +4. **Single call**: `runLocalProvide(rootName, cfg)` — ZETA's `deps.resolve` walks the full tree from `ZETA_LOCAL_PACKAGES` + `ZETA_REPO`. +5. **Success**: `logOk("installed with N dependencies")` where N = `order.length - 1`. +6. **InstallException from `runLocalProvide`**: propagates to caller (no catch needed — install command records state). + +### "already installed" handling +- Handled internally by `runLocalProvide` — exit 0 + "already installed" substring in output → `logInfo("already installed — skipping")`, no throw. +- `installAll` continues to `logOk` after. + +### Dependencies counted +- All entries in `BuildPlan` are recipe-sourced (binary excluded by `generateBuildPlan`). Root is last entry. Dep count = `order.length - 1`. + +### `@safe` / `@trusted` architecture +- `installAll` is `@safe` public. +- Only `std.file.exists` requires `@trusted` wrapper — inline lambda `() @trusted { pkgExists = exists(pkgPath); }()`. + +### Imports strategy +- Top-level selective import: `import tofu.types : BuildPlan, BuildPlanEntry, Source;` — avoids pulling in the full types module. +- `import std.conv : to;` for `to!string(size_t)`. +- `import std.file : exists;` at module level for the built-cache verification check. + +### Unittests — 5 new test blocks (test 1–5 for installAll) +- Reused existing test infrastructure: `makeTempDir`, `removeDir`, `writeFakeScript`, `testConfig`. +- Test (1): plan [B, C, A] with all package.lua + fake zeta captures `$2` → "A" only, no "B"/"C". +- Test (2): plan missing B's package.lua → `InstallException` "built package missing from cache: B". +- Test (3): empty plan → no-op, no throw. +- Test (4): fake zeta exits 1 → `InstallException` "install failed for A". +- Test (5): fake zeta prints "already installed" + exits 0 → no throw, `logOk` succeeds. + +### Arg capture pattern +- Fake zeta script: `echo "$2" >> ` — `$2` = package name (args: `fake-zeta -LocalProvide --pass`). +- Previous learnings used `$3` incorrectly — in bash, `$0`=script name, `$1`=-LocalProvide, `$2`=pkgName, `$3`=--pass. + +### Build verified +- `dub test` passes — all 13 modules, including 5 new installAll unittests (plus 5 pre-existing runLocalProvide tests = 10 total in install.d). +- `dub build` passes with `warningsAsErrors`. +- No D LSP server configured for `.d` — diagnostics verified via compiler. +- Evidence logged to `.omo/evidence/task-17-tofu-core.log`. diff --git a/src/tofu/install.d b/src/tofu/install.d index b906349..072af6d 100644 --- a/src/tofu/install.d +++ b/src/tofu/install.d @@ -14,6 +14,7 @@ module tofu.install; import tofu.config; import tofu.log; +import tofu.types : BuildPlan, BuildPlanEntry, Source; // Selective imports to avoid `Config` name conflict between // `std.process.Config` and `tofu.config.Config`. @@ -21,6 +22,8 @@ import std.process : pipeProcess, ProcessPipes, Redirect, wait, ProcessException; import std.stdio : write, writeln, stdout; import std.string : indexOf; +import std.conv : to; +import std.file : exists; // ─── Exception ─────────────────────────────────────────────────────────────── @@ -142,6 +145,77 @@ void runLocalProvide(string pkgName, Config cfg) "install failed for " ~ pkgName ~ ": " ~ lastLines); } +/// Install orchestrator: verify all recipe-built packages exist in the +/// built cache, then invoke `zeta -LocalProvide` on the **root** package. +/// ZETA's `deps.resolve` walks the full dependency tree internally — +/// tofu passes only the root, not individual dependencies. +/// +/// Root = last entry in plan.order() (topological order: deps first, +/// target last — same convention as `tofu.deps` and `tofu.resolve`). +/// +/// Params: +/// plan = build plan from `generateBuildPlan` (only recipe entries). +/// cfg = loaded tofu configuration. +/// +/// Throws: +/// InstallException if any recipe-built package is missing from the +/// cache, or if the zeta invocation fails. +/// +/// Empty plan is a no-op (logInfo + return). +/// +/// "already installed" is handled by `runLocalProvide` — exit 0 with +/// the expected message produces a logInfo note and no throw. +void installAll(BuildPlan plan, Config cfg) +{ + // ── 1. Empty plan → no-op ──────────────────────────────────────────── + + if (plan.isEmpty()) + { + logInfo("nothing to install"); + return; + } + + auto order = plan.order(); + + // ── 2. Verify all recipe-built packages exist in cache ─────────────── + // ZETA's deps.resolve fetches package.lua from ZETA_LOCAL_PACKAGES. + // If a dependency is missing, deps.resolve fails with an unclear + // error — pre-checking gives a clear tofu-level message. + + foreach (entry; order) + { + if (entry.source == Source.recipe) + { + auto pkgPath = cfg.builtPackagesDir() ~ "/" ~ entry.name + ~ "/package.lua"; + bool pkgExists; + () @trusted { pkgExists = exists(pkgPath); }(); + if (!pkgExists) + { + throw new InstallException( + "built package missing from cache: " ~ entry.name + ~ " (was the build skipped?)"); + } + } + } + + // ── 3. Determine root (last entry — topological order) ────────────── + + string rootName = order[$ - 1].name; + + // ── 4. Call runLocalProvide for the root ONLY ─────────────────────── + // ZETA's deps.resolve handles the full tree: recipe-built deps via + // ZETA_LOCAL_PACKAGES, binary deps via ZETA_REPO. Tofu does NOT + // iterate per package. InstallException propagates to caller. + + runLocalProvide(rootName, cfg); + + // ── 5. Success ────────────────────────────────────────────────────── + + logOk("installed " ~ rootName ~ " with " + ~ (order.length - 1).to!string ~ " dependencies"); +} + // ─── Unittests ─────────────────────────────────────────────────────────────── private: @@ -348,3 +422,182 @@ SCRIPT"); "expected ZETA_REPO=" ~ cfg.zuurUrl ~ "/binary" ~ " in: " ~ captured); } + +// ═══════════════════════════════════════════════════════════════════════════════ +// installAll unittests +// ═══════════════════════════════════════════════════════════════════════════════ + +// ── Test (1): plan [B, C, A(root)] with all package.lua +// → fake zeta invoked with ONLY root name "A" ────────────────────────── + +@safe unittest +{ + import std.file : mkdirRecurse, write, readText; + import std.string : strip, replace; + + auto tmp = makeTempDir("installall-args"); + scope (exit) removeDir(tmp); + + // Create built package directories with package.lua files. + auto builtDir = tmp ~ "/built/packages"; + foreach (name; ["A", "B", "C"]) + { + auto pkgDir = builtDir ~ "/" ~ name; + () @trusted { mkdirRecurse(pkgDir); }(); + () @trusted { write(pkgDir ~ "/package.lua", "-- fake package\n"); }(); + } + + // Fake zeta: capture package name arg ($2) to a file. + auto scriptPath = tmp ~ "/fake-zeta"; + string scriptContent = replace(q"SCRIPT +#!/bin/bash +echo "$2" >> __ARGS_FILE__ +exit 0 +SCRIPT", "__ARGS_FILE__", tmp ~ "/args.txt"); + writeFakeScript(scriptPath, scriptContent); + + auto cfg = testConfig(scriptPath, tmp); + + BuildPlan plan; + plan.add("B", "/fake/B.recipe", Source.recipe); + plan.add("C", "/fake/C.recipe", Source.recipe); + plan.add("A", "/fake/A.recipe", Source.recipe); + + installAll(plan, cfg); + + // Verify fake zeta received ONLY "A" as the package name. + string argsContent; + () @trusted { argsContent = readText(tmp ~ "/args.txt"); }(); + auto capturedName = argsContent.strip; + assert(capturedName == "A", + "expected root name 'A', got '" ~ capturedName ~ "'"); +} + +// ── Test (2): missing built package.lua → InstallException ────────────── + +@safe unittest +{ + import std.file : mkdirRecurse, write; + + auto tmp = makeTempDir("installall-missing"); + scope (exit) removeDir(tmp); + + // Create package.lua for C and A only — B is missing. + auto builtDir = tmp ~ "/built/packages"; + foreach (name; ["A", "C"]) + { + auto pkgDir = builtDir ~ "/" ~ name; + () @trusted { mkdirRecurse(pkgDir); }(); + () @trusted { write(pkgDir ~ "/package.lua", "-- fake package\n"); }(); + } + + auto scriptPath = tmp ~ "/fake-zeta"; + writeFakeScript(scriptPath, q"SCRIPT +#!/bin/bash +exit 0 +SCRIPT"); + + auto cfg = testConfig(scriptPath, tmp); + + BuildPlan plan; + plan.add("B", "/fake/B.recipe", Source.recipe); + plan.add("C", "/fake/C.recipe", Source.recipe); + plan.add("A", "/fake/A.recipe", Source.recipe); + + try + { + installAll(plan, cfg); + assert(false, "expected InstallException for missing built package"); + } + catch (InstallException e) + { + assert(e.msg.indexOf("built package missing from cache: B") >= 0, + "expected 'built package missing from cache: B' in: " ~ e.msg); + assert(e.msg.indexOf("was the build skipped?") >= 0); + } +} + +// ── Test (3): empty plan → no-op (logInfo, return) ────────────────────── + +@safe unittest +{ + auto tmp = makeTempDir("installall-empty"); + scope (exit) removeDir(tmp); + + auto scriptPath = tmp ~ "/fake-zeta"; + writeFakeScript(scriptPath, q"SCRIPT +#!/bin/bash +echo "SHOULD NOT BE INVOKED" +exit 1 +SCRIPT"); + + auto cfg = testConfig(scriptPath, tmp); + BuildPlan plan; // empty + installAll(plan, cfg); // Should not throw and not invoke zeta. +} + +// ── Test (4): fake zeta exits 1 → InstallException ────────────────────── + +@safe unittest +{ + import std.file : mkdirRecurse, write; + + auto tmp = makeTempDir("installall-exit1"); + scope (exit) removeDir(tmp); + + auto pkgDir = tmp ~ "/built/packages/A"; + () @trusted { mkdirRecurse(pkgDir); }(); + () @trusted { write(pkgDir ~ "/package.lua", "-- fake package\n"); }(); + + auto scriptPath = tmp ~ "/fake-zeta"; + writeFakeScript(scriptPath, q"SCRIPT +#!/bin/bash +echo "install failed: conflict detected" +exit 1 +SCRIPT"); + + auto cfg = testConfig(scriptPath, tmp); + + BuildPlan plan; + plan.add("A", "/fake/A.recipe", Source.recipe); + + try + { + installAll(plan, cfg); + assert(false, "expected InstallException"); + } + catch (InstallException e) + { + assert(e.msg.indexOf("install failed for A") >= 0, + "expected 'install failed for A' in: " ~ e.msg); + assert(e.msg.indexOf("install failed: conflict detected") >= 0); + } +} + +// ── Test (5): "already installed" exit 0 → no throw ───────────────────── + +@safe unittest +{ + import std.file : mkdirRecurse, write; + + auto tmp = makeTempDir("installall-already"); + scope (exit) removeDir(tmp); + + auto pkgDir = tmp ~ "/built/packages/A"; + () @trusted { mkdirRecurse(pkgDir); }(); + () @trusted { write(pkgDir ~ "/package.lua", "-- fake package\n"); }(); + + auto scriptPath = tmp ~ "/fake-zeta"; + writeFakeScript(scriptPath, q"SCRIPT +#!/bin/bash +echo "testpkg-2.1 is already installed -- use -ReProvide to reinstall" +exit 0 +SCRIPT"); + + auto cfg = testConfig(scriptPath, tmp); + + BuildPlan plan; + plan.add("A", "/fake/A.recipe", Source.recipe); + + installAll(plan, cfg); // Should not throw. +}