feat(resolve): add version-constraint-aware dep resolution with binary fallback

This commit is contained in:
2026-08-08 17:53:45 -04:00
parent 406a87ca41
commit ba411e9fc4
4 changed files with 677 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
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.
Up-to-date tofu ~main: target for configuration [tofu-test-application] is up to date.
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-201829.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:53:05] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:53:05] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:53:05] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:53:05] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:53:05] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:53:06] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
127.0.0.1 - - [08/Aug/2026 17:53:06] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:53:06] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
127.0.0.1 - - [08/Aug/2026 17:53:06] code 404, message File not found
127.0.0.1 - - [08/Aug/2026 17:53:06] "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:37645//index.lua
 ok index loaded: 3 packages
 . fetching index http://127.0.0.1:36355//index.lua
 . fetching index http://127.0.0.1:52041//index.lua
 . fetching index http://127.0.0.1:53177//index.lua
 . fetching index http://127.0.0.1:55829//index.lua
 ok index loaded: 0 packages
 . fetching index http://127.0.0.1:53801//index.lua
 ok index loaded: 1 packages
 . fetching index http://127.0.0.1:44861//index.lua
 ok index loaded: 2 packages
 . fetching index http://127.0.0.1:35139//index.lua
 . fetching index http://127.0.0.1:41715//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)
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
+3
View File
@@ -21,4 +21,7 @@ One exception class for the entire types module — no separate subclasses per e
### Task 4: Manual char scanning for dep spec parsing
Rather than pulling in `std.regex`, the parser uses hand-written `isNameChar()` and `isWhite()` helpers with a simple position cursor. This keeps the module dependency-light (only `std.ascii`). The logic is a direct 1:1 port of the Lua reference patterns: `[A-Za-z0-9_.+-]` for names/versions, whitespace `[ \t]` for separators.
### Task 12: `delegate` keyword in test lambdas for delegate-typed parameters
D non-capturing lambdas infer as `function` pointers, which cannot implicitly convert to `delegate`-typed parameters. The explicit `delegate` keyword (`scope f = delegate (...) { ... };`) forces the correct type. This pattern is needed for resolution seam delegates that production code will instantiate with captures (e.g. closing over `Config cfg`).
---
+53
View File
@@ -463,3 +463,56 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
- `dub build` passes with `warningsAsErrors`.
- `dub test` (with deps.d.skip) — 9 modules passed unittests.
- Evidence logged to `.omo/evidence/task-9-tofu-core.log`.
---
## Task 12 — `tofu.resolve` (version-constraint-aware dep resolution)
### Architecture
- Module `tofu.resolve` depends on: `tofu.types` (DepConstraint, DepOp, PackageIndex, Pool, BinaryCheckResult), `tofu.deps` (DepTree, DepNode), `tofu.log` (logInfo), `std.string` (indexOf).
- `constrainDepTree(tree, index, binaryCheck)` annotates a dep tree with source decisions: binary (pre-built zuur binary satisfied) vs recipe (build from source).
- The `binaryCheck` delegate is the testability seam — production wires `tofu.binary.checkBinaryVersion`; tests inject mocks.
### Algorithm — three-pass resolution
1. **Pass 1 (aggregate)**: Walk all nodes, collect all `DepConstraint[]` per dep name into `allConstraints` AA. Same dep constrained by multiple parents → all constraints aggregated.
2. **Pass 2 (resolve)**: Walk nodes in topological order. First time a dep name is encountered, resolve it:
- Test ALL aggregated constraints via `binaryCheck(depName, constraint)`.
- If ANY constraint returns `!satisfies` → dep goes recipe (after verifying recipe exists in index).
- If ALL constraints return `satisfies` → dep goes binary.
- Root (last node) is always recipe (it's what the user asked to build from ZUUR recipes).
- Neither binary nor recipe in index → `ResolveException("dependency '<name>' not found in ZUUR (neither binary nor recipe)")`.
3. **Pass 3 (output)**: Build `ConstrainedNode[]` array — one entry per unique node name in tree order, source from resolved map (fallback recipe).
### Deduplication semantics
- Same dep appearing under multiple parents → resolved ONCE. All constraints tested; if ANY fails → recipe. This implements the "strictest wins" rule from the plan.
- Unconstrained dep (`DepOp.none`) with binary → binary (satisfies always true). Without binary → recipe (if in index).
### Log format
- `"binary libfoo-2.1 satisfies libfoo>=2.0"` — binary satisfied
- `"libfoo: binary 1.9 too old, building from recipe"` — binary exists but too old
- `"libfoo: no binary available, building from recipe"` — binary 404/skip
### `delegate` vs `function` in testability seams
- **CRITICAL gotcha**: Non-capturing D lambdas in `@safe unittest` blocks are inferred as `function` pointers with inferred attributes (`pure nothrow @nogc @safe`). A parameter typed as `delegate` cannot accept a `function` even if attributes match.
- **Fix**: Use the explicit `delegate` keyword: `scope binaryCheck = delegate (string name, DepConstraint c) @safe { ... };`. This forces delegate type regardless of captures.
- **Why delegate, not function?** The production caller (install command) must capture `cfg` to call `checkBinaryVersion(name, constraint, cfg)`. A function pointer cannot carry captured state. The `delegate` keyword in tests matches the production usage pattern.
- Contrast with `deps.d` where `resolveDepTree` uses `function` — that works because recipe fetching doesn't need captured state (the production caller uses module-level functions).
### `std.string.indexOf` import
- `indexOf` is NOT in the default namespace. Must explicitly `import std.string : indexOf;` to use on string in assertions. Same pattern as `deps.d`.
### Test cases (8/8 pass)
1. dep libfoo>=2.0, binary 2.1 → binary
2. dep libfoo>=2.0, binary 1.9, recipe in index → recipe
3. dep libfoo>=2.0, binary 1.9, NOT in index → ResolveException ("neither binary nor recipe")
4. unconstrained dep with binary → binary
5. unconstrained dep without binary but in index → recipe
6. multiple constraints on same dep, one unsatisfied → recipe (dedup + strictest wins)
7. root marked recipe (single-node tree)
8. empty tree → empty result
### Build verified
- `dub build` passes with `warningsAsErrors`.
- `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`.