feat(build): orchestrate sequential topological builds with fail-fast

Add buildAll() to src/tofu/build.d — sequential builder with:
- Empty plan → 'nothing to build' no-op
- Recipe existence pre-check → fail-fast on missing
- Skip-if-exists: don't rebuild when output present (unless --force)
- Separator line: U+2500 box chars '──── building <name> (i/n) ────'
- Fail-fast: halt immediately on first BuildException

6 unittests: all succeed, first fails, empty plan, skip existing,
force rebuild, missing recipe.
This commit is contained in:
2026-08-08 18:09:06 -04:00
parent c43c1a78d9
commit 8f9a3c85a4
3 changed files with 448 additions and 0 deletions
+43
View File
@@ -641,3 +641,46 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
- `dub build` passes with `warningsAsErrors`.
- Pre-existing breakage in `build.d` (parallel task 15 artefact) excluded via `.skip` rename for testing — NOT caused by task 13.
- Evidence logged to `.omo/evidence/task-13-tofu-core.log`.
---
## Task 15 — `tofu.build.buildAll` (sequential topological build orchestrator)
### Architecture
- `buildAll(BuildPlan plan, Config cfg, bool force = false)` orchestrates sequential package builds with fail-fast semantics.
- Added imports: `tofu.types : BuildPlan, BuildResult, BuildFailure, Source;`, `tofu.log : logStep, logOk, logError, logInfo;`.
- No new exception classes — reuses existing `BuildException` from `runMakepkg`.
### Algorithm
1. Empty plan → `logInfo("nothing to build")`, return empty `BuildResult`.
2. Iterate `plan.order()` entries (caller guarantees deps-first topological order):
a. **Pre-check**: `exists(entry.recipePath)` via `@trusted` wrapper. Missing → `BuildFailure` with "recipe not found at <path>", return immediately.
b. **Separator**: `logStep("──── building %s (%d/%d) ────", name, idx, total)` — U+2500 box drawing chars.
c. **Skip-if-exists**: If NOT force AND `cfg.builtDir()/packages/<name>/package.lua` exists → logInfo skip, add to succeeded, continue.
d. **Execute**: `runMakepkg(entry.recipePath, cfg.builtDir(), cfg.defaultJobs, force, cfg)`.
e. **Success**: logOk, add to succeeded.
f. **BuildException**: logError, add to failed, return immediately (halt).
3. Return populated `BuildResult`.
### `@safe` / `@trusted` architecture
- `buildAll` is `@safe` public.
- Only `std.file.exists` requires `@trusted` wrappers — same pattern as other modules.
- `log*` functions are `@safe` — no `@trusted` needed for logging.
### Test strategy (6 new unittests, numbered 8–13)
- Reused existing test helpers: `testTempDir`, `sWrite`, `sRmdirRecurse`, `makeFakeMakepkg`.
- Test (8): plan [depA, depB, target] all valid → all 3 succeed in order, outputs created.
- Test (9): fake always exits 1 → only depA fails, succeeded empty, depB/target skipped.
- Test (10): empty plan → no-op, both lists empty.
- Test (11): pre-existing package.lua + force=false → skipped, fake NOT invoked (args file absent).
- Test (12): pre-existing package.lua + force=true → rebuilt, fake invoked (args file present).
- Test (13): nonexistent recipe path → fail-fast with "recipe not found", remaining skipped.
### Skip-if-exists semantics
- Path checked: `cfg.builtDir() ~ "/packages/" ~ name ~ "/package.lua"` — matches what `runMakepkg` produces (verified at line 190 in `runMakepkg`).
- `force=true` bypasses skip — always invokes `runMakepkg`.
### Build verified
- `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`.