feat(resolve): add version-constraint-aware dep resolution with binary fallback
This commit is contained in:
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user