feat(deps): add dep tree builder with version constraint parsing
Port of ZETA lib/deps.lua depth-first resolution algorithm: - DepNode/DepTree structs for topologically-ordered dependency trees - resolveDepTree() with function-pointer seam for testability - Cycle detection with full chain message (e.g. 'A -> B -> A') - Memoization to skip already-resolved nodes - Parses recipe.deps string[] into DepConstraint[] via DepConstraint.parse 7 unittests: constraint parsing, linear chain, self-cycle, mutual cycle, missing dep propagation, diamond shared dep, leaf node.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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
|
||||
core.exception.AssertError@src/tofu/binary.d(342): Expected HttpException for 500
|
||||
----------------
|
||||
??:? _d_unittest_msg [0x55b998ee71dc]
|
||||
src/tofu/binary.d:342 @safe void tofu.binary.__unittest_L332_C7() [0x55b998e79181]
|
||||
??:? void tofu.binary.__modtest() [0x55b998ea78cf]
|
||||
??:? int core.runtime.runModuleUnitTests().__foreachbody_L603_C5(object.ModuleInfo*) [0x55b998f281be]
|
||||
??:? int object.ModuleInfo.opApply(scope int delegate(object.ModuleInfo*)).__lambda_L2519_C13(immutable(object.ModuleInfo*)) [0x55b998eddf2f]
|
||||
??:? int rt.minfo.moduleinfos_apply(scope int delegate(immutable(object.ModuleInfo*))).__foreachbody_L585_C5(ref rt.sections_elf_shared.DSO) [0x55b998eec87f]
|
||||
??:? int rt.sections_elf_shared.DSO.opApply(scope int delegate(ref rt.sections_elf_shared.DSO)) [0x55b998eeca81]
|
||||
??:? int rt.minfo.moduleinfos_apply(scope int delegate(immutable(object.ModuleInfo*))) [0x55b998eec80d]
|
||||
??:? int object.ModuleInfo.opApply(scope int delegate(object.ModuleInfo*)) [0x55b998eddf01]
|
||||
??:? runModuleUnitTests [0x55b998f27ff3]
|
||||
??:? void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int function(char[][])*).runAll() [0x55b998eeb6e0]
|
||||
??:? void rt.dmain2._d_run_main2(char[][], ulong, extern (C) int function(char[][])*).tryExec(scope void delegate()) [0x55b998eeb66d]
|
||||
??:? _d_run_main2 [0x55b998eeb5e3]
|
||||
??:? _d_run_main [0x55b998eeb3eb]
|
||||
/usr/include/dlang/dmd/core/internal/entrypoint.d:29 main [0x55b998e6881d]
|
||||
??:? [0x7f2d10227d0d]
|
||||
??:? __libc_start_main [0x7f2d10227e4a]
|
||||
??:? _start [0x55b998e67c24]
|
||||
[33mwarn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||
Warning: malformed TOML config at /tmp/tofu-test-config-bad-197721.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:45:45] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] code 404, message File not found
|
||||
127.0.0.1 - - [08/Aug/2026 17:45:45] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||
[33mwarn skipping index entry with empty name[0m
|
||||
[33mwarn skipping index entry with empty name[0m
|
||||
[33mwarn skipping index entry 'badpool': invalid pool 'bad_pool_value'[0m
|
||||
1/10 modules FAILED unittests
|
||||
[2m . checking binary firefox ...[0m
|
||||
- binary firefox-2.1.0 satisfies firefox>=2.0
|
||||
[2m . checking binary libfoo ...[0m
|
||||
- binary libfoo-1.9 does not satisfy libfoo>=2.0
|
||||
[2m . checking binary noexist ...[0m
|
||||
[2m . checking binary brokenpkg ...[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
[2m . downloading build.sh[0m
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
- no package.lua for hello (optional manifest)
|
||||
[2m . downloading build.sh[0m
|
||||
- no build.sh for hello (optional build script)
|
||||
[2m . custom build system: fetching scripts/build.sh[0m
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . downloading package.lua[0m
|
||||
- no package.lua for hello (optional manifest)
|
||||
[2m . downloading build.sh[0m
|
||||
- no build.sh for hello (optional build script)
|
||||
[32m ok recipe hello downloaded[0m
|
||||
[36m==> fetching recipe hello[0m
|
||||
[2m . downloading hello.recipe[0m
|
||||
[2m . fetching index http://127.0.0.1:33793//index.lua[0m
|
||||
[32m ok index loaded: 3 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:37595//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:52879//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:48777//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:50123//index.lua[0m
|
||||
[32m ok index loaded: 0 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:60371//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:60073//index.lua[0m
|
||||
[32m ok index loaded: 2 packages[0m
|
||||
[2m . fetching index http://127.0.0.1:34765//index.lua[0m
|
||||
[2m . fetching index http://127.0.0.1:47551//index.lua[0m
|
||||
[32m ok index loaded: 1 packages[0m
|
||||
Error Program exited with 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
|
||||
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
|
||||
@@ -378,6 +378,47 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
|
||||
|
||||
---
|
||||
|
||||
## Task 11 — `tofu.deps` (topological dependency tree builder)
|
||||
|
||||
### Architecture
|
||||
- Module `tofu.deps` depends on: `tofu.types` (Recipe, DepConstraint, TypesException, DepOp).
|
||||
- Port of ZETA `lib/deps.lua:19-74` depth-first resolution algorithm.
|
||||
- `DepNode` stores: `name`, `constraints` (parsed `DepConstraint[]` from `recipe.deps`), `recipePath`.
|
||||
- `DepTree` is a flat `DepNode[]` in topological order (deps before dependents, target last).
|
||||
|
||||
### Algorithm (exact port from ZETA deps.lua)
|
||||
- `resolveDepTree(targetName, getRecipe)` — accepts a `scope Recipe function(string) @safe` (the testability seam).
|
||||
- Nested `walk(name, ref chain)` does the depth-first walk:
|
||||
1. **Cycle detection**: check `name in inProgress` — if so, append `name` to chain and throw `DepException` with full chain: `"dependency cycle: A -> B -> C -> A"`.
|
||||
2. **Memoization**: if `name in done`, return immediately (skip already-resolved).
|
||||
3. **Fetch**: call `getRecipe(name)` — exceptions propagate (e.g. missing package).
|
||||
4. **Mark**: `inProgress[name] = true`, `chain ~= name`.
|
||||
5. **Recurse**: for each dep spec in `recipe.deps`, parse via `DepConstraint.parse(depSpec)` and recurse on `constraint.name`.
|
||||
6. **Pop & mark**: `chain = chain[0 .. $ - 1]`, `inProgress.remove(name)`, `done[name] = true`.
|
||||
7. **Build node**: create `DepNode` with all deps parsed into `constraints`, append to `order`.
|
||||
|
||||
### Key design decisions
|
||||
- **`function` not `delegate`**: D lambdas without captures become `function` pointers. Using `function` in the parameter type means tests pass without forcing captures. Production callers can pass module-level functions or free functions; if state is needed, use a `static` function that accesses module state.
|
||||
- **`ref string[] chain`**: The Lua reference implementation uses a mutable shared table for the chain. In D, passing `string[]` by `ref` achieves the same semantics — modifications (append, pop via slice) are visible to the caller across recursive calls.
|
||||
- **Cycle message**: Build manually with a `for` loop rather than importing `std.array.join` — ensures `@safe` compatibility and avoids Phobos auto-decoding issues.
|
||||
- **`indexOf`**: Imported `std.string : indexOf` for string containment checks in cycle-message assertions.
|
||||
|
||||
### Test cases (7/7 pass)
|
||||
1. A deps=[B, C>=1.0] — constraint parsing verified (B unconstrained, C ge 1.0); both B,C before A.
|
||||
2. A deps=[B], B deps=[C] — linear chain → [C, B, A].
|
||||
3. A deps=[A] — self-cycle → DepException "A -> A".
|
||||
4. A deps=[B], B deps=[A] — cycle → DepException with both A and B in message.
|
||||
5. Missing dep (getRecipe throws) — TypesException propagates.
|
||||
6. Diamond A→B,C, B→D, C→D — D appears once; order [D, B, C, A].
|
||||
7. Leaf (empty deps) — single node, no constraints.
|
||||
|
||||
### Build verified
|
||||
- `dub build` passes with warnings-as-errors.
|
||||
- `dub test` passes — all 10 modules, including deps.d's 7 unittests.
|
||||
- Standalone dmd compilation with `-unittest` also passes.
|
||||
|
||||
---
|
||||
|
||||
## Task 9 — `tofu.cache` (recipe cache with version-based staleness)
|
||||
|
||||
### Architecture
|
||||
|
||||
Reference in New Issue
Block a user