feat(resolve): generate build plan from constrained dep tree

Add generateBuildPlan() to tofu.resolve — filters constrained dep tree
to recipe-only packages, verifies cached recipes exist, and produces
an ordered BuildPlan ready for the build orchestrator (task 15).

- Skips binary-satisfied deps (Zeta handles those)
- Preserves topological order from constrained nodes (deps-first)
- Missing recipe files: re-fetch via injectable delegate seam
  (production wires tofu.fetch.fetchRecipe; tests inject mocks)
- Empty plan (all binary) → returns empty BuildPlan + log info
- Root always recipe — always included in plan
- 6 unittests: filter binary, fetch seam, missing no-seam,
  all-binary empty, root with binary dep, cache hit no-fetch
- dub test + dub build pass with warningsAsErrors
This commit is contained in:
2026-08-08 17:58:25 -04:00
parent ba411e9fc4
commit 7928b4d5f2
3 changed files with 548 additions and 1 deletions
+152
View File
@@ -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]
src/tofu/build.d(126,33): Error: `;` expected
import std.process : Config as ProcConfig;
^
src/tofu/build.d(243,59): Error: conflicting attribute `@safe`
string exitCode = "0", string argsFile = "") @safe {
^
Error /usr/bin/dmd failed with exit code 1.
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
warn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2)
Warning: malformed TOML config at /tmp/tofu-test-config-bad-204774.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 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:57:40] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:57:40] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:57:40] "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'
11 modules passed unittests
 . 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
==> 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:33945//index.lua
 ok index loaded: 3 packages
 . fetching index http://127.0.0.1:33079//index.lua
 . fetching index http://127.0.0.1:39107//index.lua
 . fetching index http://127.0.0.1:58239//index.lua
 . fetching index http://127.0.0.1:52265//index.lua
 ok index loaded: 0 packages
 . fetching index http://127.0.0.1:55027//index.lua
 ok index loaded: 1 packages
 . fetching index http://127.0.0.1:54741//index.lua
 ok index loaded: 2 packages
 . fetching index http://127.0.0.1:46869//index.lua
 . fetching index http://127.0.0.1:58081//index.lua
 ok index loaded: 1 packages
- 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.
Building tofu ~main: building configuration [application]
Linking tofu
Finished To force a rebuild of up-to-date targets, run again with --force
+54
View File
@@ -516,3 +516,57 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
- `dub test` passes — all 11 modules (config, log, types, vercmp, http, index, fetch, cache, binary, deps, resolve) pass unittests.
- No D LSP server configured for `.d` files — diagnostics verified via compiler.
- Evidence logged to `.omo/evidence/task-12-tofu-core.log`.
---
## Task 13 — `tofu.resolve.generateBuildPlan` (build plan from constrained tree)
### Architecture
- New function `generateBuildPlan(constrained, tree, cfg, fetchRecipe = null)` in `tofu.resolve`.
- Input: `ConstrainedNode[]` from `constrainDepTree` (already in topological order, deps-first, root last). The `DepTree` parameter is preserved for future context but not used for logic — ordering comes from the constrained array.
- Output: `BuildPlan` with `Source.recipe` entries for every recipe-sourced node (binary nodes excluded).
- The `fetchRecipe` delegate (`scope string delegate(string) @safe`) is the testability seam. Default `null` means "no fetcher available" — missing recipes throw `FetchException`.
- Production wires: `delegate (string name) @safe { return tofu.fetch.fetchRecipe(name, cfg); }`.
### Algorithm
1. Iterate constrained nodes in order (preserves topological ordering).
2. Skip nodes with `source == DepSource.binary` — Zeta handles binary deps.
3. For recipe nodes: compute cache path via `cfg.recipesCacheDir(name) ~ "/" ~ name ~ ".recipe"`.
4. Check `std.file.exists(path)` via `@trusted` wrapper.
5. Missing → if `fetchRecipe` delegate provided, call it; else throw `FetchException("recipe not cached and no fetcher provided")`.
6. Add to `BuildPlan` via `plan.add(name, recipePath, Source.recipe)`.
7. Empty plan → log `"nothing to build (all binary)"`.
8. Non-empty → log `"build plan: N packages"` via `logOk`.
### `@safe` / `@trusted` architecture
- `generateBuildPlan` is `@safe`.
- Only `std.file.exists` requires `@trusted` wrapper — inline lambda `() @trusted { return exists(path); }()`.
- Follows existing patterns from `fetch.d`, `cache.d`, `index.d`.
### FetchException reuse
- Uses `tofu.fetch.FetchException` (imported via `import tofu.fetch;`). No new exception class needed — the message `"recipe not cached and no fetcher provided"` is distinct and searchable.
### New imports in resolve.d
- `import tofu.config;` — for `Config` type (parameter in function signature).
- `import tofu.fetch;` — for `FetchException`.
- `import std.file : exists;` — for recipe cache existence check.
- `import tofu.log;` already had `logInfo`; now also uses `logStep`, `logOk`.
### Test strategy (6 new unittests, numbered 9–14)
- Reused the temp-dir + `scope(exit)` cleanup pattern from other modules.
- `version(unittest)` block with `@trusted` helpers: `createCachedRecipe(dir, name)`, `testTempDir(suffix)`, `testRmdir(path)`, `testConfig(cacheDir)`.
- Test (9): [C(recipe), B(binary), A(recipe)] → plan [C, A] — B excluded, order preserved.
- Test (10): missing cache + fetch delegate → fetch called, returned path used in plan.
- Test (11): missing cache + no fetch delegate → FetchException thrown.
- Test (12): all binary → empty plan.
- Test (13): root included even when dep is binary.
- Test (14): recipe files exist in cache → fetch delegate NOT called (delegate throws assert on invocation).
### `scope` on constrained parameter
- `scope const ConstrainedNode[] constrained` — DMD 2.112 requires `scope` on array/class reference parameters for `@safe` inference when the function does not escape them. Same pattern as `constrainDepTree` (which uses `scope const PackageIndex[] index`).
### Build verified
- `dub test` passes — all 11 modules, including 6 new generateBuildPlan unittests.
- `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`.