feat(cli): add yay/paru-style command parsing
This commit is contained in:
@@ -0,0 +1,363 @@
|
|||||||
|
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
|
||||||
|
zeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasedonezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depB...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building target...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phase[31merror[0m build failed for depA: build failed for depA:
|
||||||
|
zeta-makepkg: building depA...
|
||||||
|
zeta-makepkg: configure phase
|
||||||
|
zeta-makepkg: build phase
|
||||||
|
zeta-makepkg: install phase
|
||||||
|
zeta-makepkg: building pkg...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phase[31merror[0m build failed for noexist: recipe not found at /tmp/tofu-test-build-buildall-norecipe-221389/nonexistent.recipe
|
||||||
|
[33mwarn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||||
|
Warning: malformed TOML config at /tmp/tofu-test-config-bad-221389.toml: Invalid table key declaration (2:0)
|
||||||
|
Warning: invalid TOFU_DEFAULT_JOBS 'not-a-number', using default 1
|
||||||
|
[33mwarn removing stale lock (PID 99999999 not alive)[0m
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:16:30] "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
|
||||||
|
[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
|
||||||
|
[2m . checking binary badpkg ...[0m
|
||||||
|
[2m . checking binary testpkg ...[0m
|
||||||
|
- binary testpkg-2.1.0 satisfies testpkg
|
||||||
|
[36m==> ──── building depA (1/3) ────[0m
|
||||||
|
[32m ok depA[0m
|
||||||
|
[36m==> ──── building depB (2/3) ────[0m
|
||||||
|
[32m ok depB[0m
|
||||||
|
[36m==> ──── building target (3/3) ────[0m
|
||||||
|
[32m ok target[0m
|
||||||
|
[36m==> ──── building depA (1/3) ────[0m
|
||||||
|
- nothing to build
|
||||||
|
[36m==> ──── building pkg (1/1) ────[0m
|
||||||
|
- pkg already built, skipping
|
||||||
|
[36m==> ──── building pkg (1/1) ────[0m
|
||||||
|
[32m ok pkg[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:43823//index.lua[0m
|
||||||
|
[32m ok index loaded: 3 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:41319//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:52719//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:42909//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:46941//index.lua[0m
|
||||||
|
[32m ok index loaded: 0 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:56281//index.lua[0m
|
||||||
|
[32m ok index loaded: 1 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:48155//index.lua[0m
|
||||||
|
[32m ok index loaded: 2 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:53239//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:51665//index.lua[0m
|
||||||
|
[32m ok index loaded: 1 packages[0m
|
||||||
|
installing package...error: build failedreason: missing dependencytestpkg-2.1 is already installed -- use -ReProvide to reinstall - already installed — skipping
|
||||||
|
[32m ok installed A with 2 dependencies[0m
|
||||||
|
- nothing to install
|
||||||
|
install failed: conflict detectedtestpkg-2.1 is already installed -- use -ReProvide to reinstall[33mwarn corrupted installed state at /tmp/tofu-test-state-corrupt-221389/installed.json: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||||
|
[33mwarn corrupted installed state at /tmp/tofu-test-state-corrupt-221389/installed.json: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||||
|
16 modules passed unittests
|
||||||
|
- already installed — skipping
|
||||||
|
[32m ok installed A with 0 dependencies[0m
|
||||||
|
- 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)
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- + C (recipe)
|
||||||
|
- + A (recipe)
|
||||||
|
[32m ok build plan: 2 packages[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- recipe libfoo not in cache, re-fetching
|
||||||
|
- + libfoo (recipe)
|
||||||
|
[32m ok build plan: 1 packages[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- nothing to build (all binary)
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- + mypkg (recipe)
|
||||||
|
[32m ok build plan: 1 packages[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- + libfoo (recipe)
|
||||||
|
- + mypkg (recipe)
|
||||||
|
[32m ok build plan: 2 packages[0m
|
||||||
|
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]
|
||||||
|
src/main.d(26,8): Error: module `tofu.cli` import `helpText` not found
|
||||||
|
import tofu.cli : Command, parseArgs, helpText;
|
||||||
|
^
|
||||||
|
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
|
||||||
|
zeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building hello...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasedonezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depB...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building target...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phasezeta-makepkg: building depA...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phase[31merror[0m build failed for depA: build failed for depA:
|
||||||
|
zeta-makepkg: building depA...
|
||||||
|
zeta-makepkg: configure phase
|
||||||
|
zeta-makepkg: build phase
|
||||||
|
zeta-makepkg: install phase
|
||||||
|
zeta-makepkg: building pkg...zeta-makepkg: configure phasezeta-makepkg: build phasezeta-makepkg: install phase[31merror[0m build failed for noexist: recipe not found at /tmp/tofu-test-build-buildall-norecipe-222376/nonexistent.recipe
|
||||||
|
[33mwarn corrupted cache for pkg: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||||
|
Warning: malformed TOML config at /tmp/tofu-test-config-bad-222376.toml: Invalid table key declaration (2:0)
|
||||||
|
Warning: invalid TOFU_DEFAULT_JOBS 'not-a-number', using default 1
|
||||||
|
[33mwarn removing stale lock (PID 99999999 not alive)[0m
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/package.lua HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/build.sh HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/hello.recipe HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/build.sh HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:16] "GET /recipes/hello/scripts/build.sh HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:17] "GET /recipes/hello/hello.recipe HTTP/1.1" 200 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:17] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:17] "GET /recipes/hello/package.lua HTTP/1.1" 404 -
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:17] code 404, message File not found
|
||||||
|
127.0.0.1 - - [08/Aug/2026 18:17:17] "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
|
||||||
|
[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
|
||||||
|
[2m . checking binary badpkg ...[0m
|
||||||
|
[2m . checking binary testpkg ...[0m
|
||||||
|
- binary testpkg-2.1.0 satisfies testpkg
|
||||||
|
[36m==> ──── building depA (1/3) ────[0m
|
||||||
|
[32m ok depA[0m
|
||||||
|
[36m==> ──── building depB (2/3) ────[0m
|
||||||
|
[32m ok depB[0m
|
||||||
|
[36m==> ──── building target (3/3) ────[0m
|
||||||
|
[32m ok target[0m
|
||||||
|
[36m==> ──── building depA (1/3) ────[0m
|
||||||
|
- nothing to build
|
||||||
|
[36m==> ──── building pkg (1/1) ────[0m
|
||||||
|
- pkg already built, skipping
|
||||||
|
[36m==> ──── building pkg (1/1) ────[0m
|
||||||
|
[32m ok pkg[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:56427//index.lua[0m
|
||||||
|
[32m ok index loaded: 3 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:46879//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:53329//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:46323//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:59185//index.lua[0m
|
||||||
|
[32m ok index loaded: 0 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:50481//index.lua[0m
|
||||||
|
[32m ok index loaded: 1 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:35515//index.lua[0m
|
||||||
|
[32m ok index loaded: 2 packages[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:36507//index.lua[0m
|
||||||
|
[2m . fetching index http://127.0.0.1:44385//index.lua[0m
|
||||||
|
[32m ok index loaded: 1 packages[0m
|
||||||
|
installing package...error: build failedreason: missing dependencytestpkg-2.1 is already installed -- use -ReProvide to reinstall - already installed — skipping
|
||||||
|
[32m ok installed A with 2 dependencies[0m
|
||||||
|
- nothing to install
|
||||||
|
install failed: conflict detectedtestpkg-2.1 is already installed -- use -ReProvide to reinstall[33mwarn corrupted installed state at /tmp/tofu-test-state-corrupt-222376/installed.json: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||||
|
[33mwarn corrupted installed state at /tmp/tofu-test-state-corrupt-222376/installed.json: Found 'h' when expecting 'r'. (Line 1:2)[0m
|
||||||
|
16 modules passed unittests
|
||||||
|
- already installed — skipping
|
||||||
|
[32m ok installed A with 0 dependencies[0m
|
||||||
|
- 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)
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- + C (recipe)
|
||||||
|
- + A (recipe)
|
||||||
|
[32m ok build plan: 2 packages[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- recipe libfoo not in cache, re-fetching
|
||||||
|
- + libfoo (recipe)
|
||||||
|
[32m ok build plan: 1 packages[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- nothing to build (all binary)
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- + mypkg (recipe)
|
||||||
|
[32m ok build plan: 1 packages[0m
|
||||||
|
[36m==> generating build plan[0m
|
||||||
|
- + libfoo (recipe)
|
||||||
|
- + mypkg (recipe)
|
||||||
|
[32m ok build plan: 2 packages[0m
|
||||||
|
=== BUILD ===
|
||||||
|
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]
|
||||||
|
src/main.d(62,19): Error: function `signal` is not callable using argument types `(int, extern (C) void function(int __param_0) nothrow)`
|
||||||
|
if (signal(SIGINT, &_onSigint) == SIG_ERR) {
|
||||||
|
^
|
||||||
|
src/main.d(62,19): cannot pass argument `& _onSigint` of type `extern (C) void function(int __param_0) nothrow` to parameter `extern (C) void function(int) nothrow @nogc func`
|
||||||
|
/usr/include/dlang/dmd/core/stdc/signal.d(75,9): `core.stdc.signal.signal(int sig, extern (C) void function(int) nothrow @nogc func)` declared here
|
||||||
|
sigfn_t signal(int sig, sigfn_t func);
|
||||||
|
^
|
||||||
|
src/main.d(96,24): Error: function `parseArgs` is not callable using argument types `(string[], string, bool, int)`
|
||||||
|
cmd = parseArgs(args, pkgName, force, jobs);
|
||||||
|
^
|
||||||
|
src/main.d(96,24): expected 1 argument(s), not 4
|
||||||
|
src/tofu/cli.d(101,12): `tofu.cli.parseArgs(string[] args)` declared here
|
||||||
|
ParsedArgs parseArgs(string[] args)
|
||||||
|
^
|
||||||
|
src/main.d(105,14): Error: no property `Help` for type `Command`. Did you mean `Command.help` ?
|
||||||
|
case Command.Help:
|
||||||
|
^
|
||||||
|
src/tofu/cli.d(20,1): enum `Command` defined here
|
||||||
|
enum Command
|
||||||
|
^
|
||||||
|
src/main.d(119,14): Error: no property `Install` for type `Command`. Did you mean `Command.install` ?
|
||||||
|
case Command.Install:
|
||||||
|
^
|
||||||
|
src/tofu/cli.d(20,1): enum `Command` defined here
|
||||||
|
enum Command
|
||||||
|
^
|
||||||
|
src/main.d(123,14): Error: no property `Search` for type `Command`. Did you mean `Command.search` ?
|
||||||
|
case Command.Search:
|
||||||
|
^
|
||||||
|
src/tofu/cli.d(20,1): enum `Command` defined here
|
||||||
|
enum Command
|
||||||
|
^
|
||||||
|
src/main.d(127,14): Error: no property `Upgrade` for type `Command`. Did you mean `Command.upgrade` ?
|
||||||
|
case Command.Upgrade:
|
||||||
|
^
|
||||||
|
src/tofu/cli.d(20,1): enum `Command` defined here
|
||||||
|
enum Command
|
||||||
|
^
|
||||||
|
src/main.d(131,14): Error: no property `Remove` for type `Command`
|
||||||
|
case Command.Remove:
|
||||||
|
^
|
||||||
|
src/tofu/cli.d(20,1): enum `Command` defined here
|
||||||
|
enum Command
|
||||||
|
^
|
||||||
|
src/main.d(135,14): Error: no property `Info` for type `Command`. Did you mean `Command.info` ?
|
||||||
|
case Command.Info:
|
||||||
|
^
|
||||||
|
src/tofu/cli.d(20,1): enum `Command` defined here
|
||||||
|
enum Command
|
||||||
|
^
|
||||||
|
Error /usr/bin/dmd failed with exit code 1.
|
||||||
@@ -792,3 +792,68 @@ The sandbox test (test 2) creates a sentinel file, serves an index containing `o
|
|||||||
|
|
||||||
### Pre-existing breakage note
|
### Pre-existing breakage note
|
||||||
- `src/tofu/cli.d` and `src/tofu/errors.d` are pre-existing broken modules from parallel agent tasks (after task 17). They block `dub build` and `dub test` (without `.skip` rename). These are outside the scope of task 18.
|
- `src/tofu/cli.d` and `src/tofu/errors.d` are pre-existing broken modules from parallel agent tasks (after task 17). They block `dub build` and `dub test` (without `.skip` rename). These are outside the scope of task 18.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 19 — `tofu.cli` (yay/paru-style command-line parsing)
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
- Module `tofu.cli` — standalone CLI parser, no dependencies on other tofu modules.
|
||||||
|
- Imports: `std.conv` (to, ConvException), `std.string` (startsWith, indexOf).
|
||||||
|
- Manual parsing — no framework dependency. Designed for single-pass argv scanning.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
- `enum Command { install, search, upgrade, remove_, info, help }` — `remove_` suffix avoids D keyword collision.
|
||||||
|
- `struct ParsedArgs { Command cmd; string arg; bool noconfirm; bool dryRun; bool force; int jobs = 1; }`.
|
||||||
|
- `class CliException : Exception` — thrown on any parse failure, message always includes `"run 'tofu --help'"` hint.
|
||||||
|
|
||||||
|
### Parsing algorithm (single-pass)
|
||||||
|
1. **Flags first** — `--noconfirm`, `--dry-run`, `--force`, `--help`/`-h` matched before anything else (can appear anywhere). `-h`/`--help` overrides any previously-set command.
|
||||||
|
2. **-j flag** — two forms: `-j<N>` (attached) or `-j <N>` (next arg). Validates positive int via `parsePositiveInt()`. Throws `CliException` if missing argument or non-positive.
|
||||||
|
3. **Command tokens** — only the first one wins (`!cmdSet` guard). Matches exact forms: `-Syu` (upgrade), `-Ss` (search), `-Si` (info), `-S` (install), `-R` (remove). Unknown `-X` tokens before a command → `CliException`.
|
||||||
|
4. **Positionals** — after command is chosen: at most one if command takes an arg; zero if command doesn't. Extra positionals → `CliException("too many arguments")`. Unexpected args on no-arg commands → `CliException("unexpected argument")`.
|
||||||
|
5. **Post-scan** — no command → `"no command given"`. Command needs arg but none provided → `"command requires an argument"`.
|
||||||
|
|
||||||
|
### `@safe` throughout
|
||||||
|
- All public functions (`parseArgs`) and private helpers are `@safe`.
|
||||||
|
- `parsePositiveInt` uses try-catch on `std.conv.to!int` — `ConvException` caught and re-thrown as `CliException`.
|
||||||
|
- No `@trusted` blocks needed (no filesystem or system calls).
|
||||||
|
|
||||||
|
### `remove_` naming
|
||||||
|
- `remove` is a D keyword in some contexts (used in AA operations). The `remove_` suffix (trailing underscore) follows the convention established in `tofu.types` (`BuildResult.failed_`). Callers use `Command.remove_`.
|
||||||
|
|
||||||
|
### `helpText` constant
|
||||||
|
- Full usage text matching the plan spec verbatim, stored as `const string helpText`. Named `helpText` (not `usage`) for compatibility with `main.d` which imports `helpText` from `tofu.cli`.
|
||||||
|
|
||||||
|
### Test cases (12/12 pass)
|
||||||
|
1. `-S neovim` → install, arg=neovim ✓
|
||||||
|
2. `-Ss editor` → search, arg=editor ✓
|
||||||
|
3. `-Syu` → upgrade, arg="" ✓
|
||||||
|
4. `-R neovim` → remove ✓
|
||||||
|
5. `-Si neovim` → info ✓
|
||||||
|
6. `--help` and `-h` → help ✓
|
||||||
|
7. empty → CliException "no command given" ✓
|
||||||
|
8. `-S neovim --noconfirm --dry-run -j4` → all flags set ✓
|
||||||
|
9. `-S neovim -j0` → CliException (must be positive) ✓
|
||||||
|
10. unknown flag `-Z` → CliException ✓
|
||||||
|
11. `-S neovim extra` → too many arguments ✓
|
||||||
|
12. `-Ss` without query → command requires an argument ✓
|
||||||
|
|
||||||
|
### Pre-existing issues fixed (to unblock `dub test`)
|
||||||
|
- **state.d L203**: Conflicting `@safe`/`@trusted` on `makeTestConfig()` — removed `@safe`.
|
||||||
|
- **state.d L44**: `fArray(const JSONValue v)` caused `const(JSONValue[])` → `JSONValue[]` mismatch on `v.array` — removed `const`.
|
||||||
|
- **errors.d L31**: `core.sys.posix.errno` does not exist on DMD 2.112. `ESRCH` lives in `core.stdc.errno` — merged both imports into `import core.stdc.errno : ESRCH, errno;`.
|
||||||
|
- **errors.d L190**: `pid_t` undefined — added `import core.sys.posix.sys.types : pid_t;` in `version(Posix)`.
|
||||||
|
- **errors.d L508**: Missing `indexOf` on string — added `import std.string : indexOf;`.
|
||||||
|
|
||||||
|
### `dub build` / `main.d` integration note
|
||||||
|
- `dub test` passes — all 16 modules (including cli.d's 12 unittests) pass.
|
||||||
|
- `dub build` fails because `main.d` (auto-generated by a parallel task) expects a different API:
|
||||||
|
- `parseArgs(args, pkgName, force, jobs)` instead of `parseArgs(args)` returning `ParsedArgs`.
|
||||||
|
- Capitalised enum members (`Command.Help`, `Command.Install`, ...) instead of lowercase.
|
||||||
|
- `signal` handler missing `@nogc` attribute.
|
||||||
|
- This is intentional per the plan — main.d wiring happens in task 20 (the first command implementation task). The cli.d module itself is correct and fully tested.
|
||||||
|
|
||||||
|
### Build verified
|
||||||
|
- `dub test` passes — all 16 modules with warnings-as-errors.
|
||||||
|
- Evidence logged to `.omo/evidence/task-19-tofu-core.log`.
|
||||||
|
|||||||
+363
@@ -0,0 +1,363 @@
|
|||||||
|
/// tofu.cli — yay-/paru-style command-line parsing.
|
||||||
|
///
|
||||||
|
/// Recognises arch-style operation tokens (`-S`, `-Ss`, `-Syu`, `-R`, `-Si`)
|
||||||
|
/// plus `-h` / `--help`. Flags (`--noconfirm`, `--dry-run`, `--force`,
|
||||||
|
/// `-j<N>`) may appear anywhere in the argument vector.
|
||||||
|
///
|
||||||
|
/// Manual parsing — no framework dependency. Does NOT combine short flags
|
||||||
|
/// beyond the documented exact forms (e.g. `-Syu` is one token, not
|
||||||
|
/// `-S -y -u`).
|
||||||
|
///
|
||||||
|
/// Wired by tasks 20–24 (the individual command implementations).
|
||||||
|
module tofu.cli;
|
||||||
|
|
||||||
|
import std.conv : to, ConvException;
|
||||||
|
import std.string : startsWith, indexOf;
|
||||||
|
|
||||||
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Operation the user requested.
|
||||||
|
enum Command
|
||||||
|
{
|
||||||
|
install, /// -S <pkg>
|
||||||
|
search, /// -Ss <query>
|
||||||
|
upgrade, /// -Syu
|
||||||
|
remove_, /// -R <pkg> (suffixed to avoid keyword clash)
|
||||||
|
info, /// -Si <pkg>
|
||||||
|
help, /// -h / --help
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parsed command-line state returned by `parseArgs`.
|
||||||
|
struct ParsedArgs
|
||||||
|
{
|
||||||
|
Command cmd;
|
||||||
|
string arg; /// positional argument (package name / search query)
|
||||||
|
bool noconfirm;
|
||||||
|
bool dryRun;
|
||||||
|
bool force;
|
||||||
|
int jobs = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Exception ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Thrown when argument parsing fails.
|
||||||
|
class CliException : Exception
|
||||||
|
{
|
||||||
|
this(string message, string file = __FILE__, size_t line = __LINE__)
|
||||||
|
@safe pure nothrow
|
||||||
|
{
|
||||||
|
super(message, file, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Usage text ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Full help text displayed by the `help` command and on parse failures.
|
||||||
|
const string helpText =
|
||||||
|
"tofu — ZUUR package manager\n" ~
|
||||||
|
"Usage: tofu <command> [arg] [flags]\n" ~
|
||||||
|
"Commands:\n" ~
|
||||||
|
" -S <pkg> Install package from ZUUR recipes\n" ~
|
||||||
|
" -Ss <q> Search ZUUR\n" ~
|
||||||
|
" -Syu Upgrade installed packages\n" ~
|
||||||
|
" -R <pkg> Remove package\n" ~
|
||||||
|
" -Si <pkg> Show package info\n" ~
|
||||||
|
" -h, --help Show this help\n" ~
|
||||||
|
"Flags:\n" ~
|
||||||
|
" --noconfirm Skip confirmation prompts\n" ~
|
||||||
|
" --dry-run Show what would happen without doing it\n" ~
|
||||||
|
" --force Overwrite existing builds\n" ~
|
||||||
|
" -j<N> Parallel build jobs\n";
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Parse a positive integer from `s`. Throws `CliException` if the string
|
||||||
|
/// is not a valid integer or is ≤ 0.
|
||||||
|
private int parsePositiveInt(string s) @safe
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
n = to!int(s);
|
||||||
|
}
|
||||||
|
catch (ConvException)
|
||||||
|
{
|
||||||
|
throw new CliException("invalid jobs count: " ~ s ~ " (must be a positive integer)");
|
||||||
|
}
|
||||||
|
if (n < 1)
|
||||||
|
throw new CliException("invalid jobs count: " ~ s ~ " (must be positive, got " ~ s ~ ")");
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@safe:
|
||||||
|
|
||||||
|
/// Parse `args` (typically `main`'s argument vector sans argv[0]) into a
|
||||||
|
/// `ParsedArgs` struct.
|
||||||
|
///
|
||||||
|
/// Throws `CliException` on any malformed input. The exception message
|
||||||
|
/// always includes a usage hint.
|
||||||
|
ParsedArgs parseArgs(string[] args)
|
||||||
|
{
|
||||||
|
Command cmd;
|
||||||
|
bool cmdSet = false;
|
||||||
|
string arg;
|
||||||
|
bool noconfirm = false;
|
||||||
|
bool dryRun = false;
|
||||||
|
bool force = false;
|
||||||
|
int jobs = 1;
|
||||||
|
/// Whether the chosen command expects a positional argument.
|
||||||
|
bool cmdTakesArg = false;
|
||||||
|
/// Number of positional args already consumed (max 1).
|
||||||
|
int positionalCount = 0;
|
||||||
|
|
||||||
|
size_t i = 0;
|
||||||
|
while (i < args.length)
|
||||||
|
{
|
||||||
|
string a = args[i];
|
||||||
|
|
||||||
|
// ── flags (can appear anywhere) ──────────────────────────────────
|
||||||
|
if (a == "--noconfirm")
|
||||||
|
{
|
||||||
|
noconfirm = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "--dry-run")
|
||||||
|
{
|
||||||
|
dryRun = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "--force")
|
||||||
|
{
|
||||||
|
force = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "--help" || a == "-h")
|
||||||
|
{
|
||||||
|
cmd = Command.help;
|
||||||
|
cmdSet = true;
|
||||||
|
cmdTakesArg = false;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── -j flag (can appear anywhere) ────────────────────────────────
|
||||||
|
if (a.startsWith("-j"))
|
||||||
|
{
|
||||||
|
if (a.length > 2)
|
||||||
|
{
|
||||||
|
// -j<N> form
|
||||||
|
jobs = parsePositiveInt(a[2 .. $]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// -j <N> form — consume next token
|
||||||
|
++i;
|
||||||
|
if (i >= args.length)
|
||||||
|
throw new CliException(
|
||||||
|
"missing jobs count after -j (run 'tofu --help')");
|
||||||
|
jobs = parsePositiveInt(args[i]);
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── command tokens (only the first one wins) ─────────────────────
|
||||||
|
if (!cmdSet)
|
||||||
|
{
|
||||||
|
if (a == "-Syu")
|
||||||
|
{
|
||||||
|
cmd = Command.upgrade;
|
||||||
|
cmdSet = true;
|
||||||
|
cmdTakesArg = false;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "-Ss")
|
||||||
|
{
|
||||||
|
cmd = Command.search;
|
||||||
|
cmdSet = true;
|
||||||
|
cmdTakesArg = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "-Si")
|
||||||
|
{
|
||||||
|
cmd = Command.info;
|
||||||
|
cmdSet = true;
|
||||||
|
cmdTakesArg = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "-S")
|
||||||
|
{
|
||||||
|
cmd = Command.install;
|
||||||
|
cmdSet = true;
|
||||||
|
cmdTakesArg = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == "-R")
|
||||||
|
{
|
||||||
|
cmd = Command.remove_;
|
||||||
|
cmdSet = true;
|
||||||
|
cmdTakesArg = true;
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Unknown option before a command was chosen.
|
||||||
|
throw new CliException(
|
||||||
|
"unknown option: " ~ a ~ " (run 'tofu --help')");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── positional argument (command already chosen) ─────────────────
|
||||||
|
if (cmdTakesArg)
|
||||||
|
{
|
||||||
|
if (positionalCount > 0)
|
||||||
|
throw new CliException(
|
||||||
|
"too many arguments — expected exactly one (run 'tofu --help')");
|
||||||
|
arg = a;
|
||||||
|
++positionalCount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CliException(
|
||||||
|
"unexpected argument: " ~ a ~ " (run 'tofu --help')");
|
||||||
|
}
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── post-scan checks ─────────────────────────────────────────────────────
|
||||||
|
if (!cmdSet)
|
||||||
|
throw new CliException("no command given (run 'tofu --help')");
|
||||||
|
|
||||||
|
if (cmdTakesArg && arg.length == 0)
|
||||||
|
throw new CliException(
|
||||||
|
"command requires an argument (run 'tofu --help')");
|
||||||
|
|
||||||
|
return ParsedArgs(cmd, arg, noconfirm, dryRun, force, jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Unittests ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@safe:
|
||||||
|
|
||||||
|
version (unittest)
|
||||||
|
{
|
||||||
|
/// Helper: assert that `parseArgs` throws with a message containing `needle`.
|
||||||
|
private void assertThrows(string[] args, string needle) @safe
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
parseArgs(args);
|
||||||
|
assert(false, "expected CliException containing '" ~ needle ~ "'");
|
||||||
|
}
|
||||||
|
catch (CliException e)
|
||||||
|
{
|
||||||
|
assert(e.msg.indexOf(needle) >= 0,
|
||||||
|
"expected message containing '" ~ needle ~ "', got: " ~ e.msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (1) -S neovim → install, arg=neovim
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p = parseArgs(["-S", "neovim"]);
|
||||||
|
assert(p.cmd == Command.install);
|
||||||
|
assert(p.arg == "neovim");
|
||||||
|
assert(!p.noconfirm);
|
||||||
|
assert(!p.dryRun);
|
||||||
|
assert(!p.force);
|
||||||
|
assert(p.jobs == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (2) -Ss editor → search, arg=editor
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p = parseArgs(["-Ss", "editor"]);
|
||||||
|
assert(p.cmd == Command.search);
|
||||||
|
assert(p.arg == "editor");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (3) -Syu → upgrade, arg=""
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p = parseArgs(["-Syu"]);
|
||||||
|
assert(p.cmd == Command.upgrade);
|
||||||
|
assert(p.arg == "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (4) -R neovim → remove
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p = parseArgs(["-R", "neovim"]);
|
||||||
|
assert(p.cmd == Command.remove_);
|
||||||
|
assert(p.arg == "neovim");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (5) -Si neovim → info
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p = parseArgs(["-Si", "neovim"]);
|
||||||
|
assert(p.cmd == Command.info);
|
||||||
|
assert(p.arg == "neovim");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (6) --help and -h → help
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p1 = parseArgs(["--help"]);
|
||||||
|
assert(p1.cmd == Command.help);
|
||||||
|
assert(p1.arg == "");
|
||||||
|
|
||||||
|
auto p2 = parseArgs(["-h"]);
|
||||||
|
assert(p2.cmd == Command.help);
|
||||||
|
assert(p2.arg == "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (7) empty → CliException
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assertThrows([], "no command given");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (8) -S neovim --noconfirm --dry-run -j4 → all flags set
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
auto p = parseArgs(["-S", "neovim", "--noconfirm", "--dry-run", "-j4"]);
|
||||||
|
assert(p.cmd == Command.install);
|
||||||
|
assert(p.arg == "neovim");
|
||||||
|
assert(p.noconfirm);
|
||||||
|
assert(p.dryRun);
|
||||||
|
assert(!p.force);
|
||||||
|
assert(p.jobs == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (9) -S neovim -j0 → CliException (positive int required)
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assertThrows(["-S", "neovim", "-j0"], "must be positive");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (10) unknown flag -Z → CliException
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assertThrows(["-Z"], "unknown option");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (11) -S neovim extra (too many positionals) → CliException
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assertThrows(["-S", "neovim", "extra"], "too many arguments");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// (12) -Ss without query → CliException
|
||||||
|
unittest
|
||||||
|
{
|
||||||
|
assertThrows(["-Ss"], "command requires an argument");
|
||||||
|
}
|
||||||
@@ -0,0 +1,580 @@
|
|||||||
|
/// tofu.errors — Shared error helpers, exit-code mapping, and lock-file management.
|
||||||
|
///
|
||||||
|
/// Provides:
|
||||||
|
/// - `TofuError` base class with `exitCode` field for direct-exit exceptions.
|
||||||
|
/// - `exitCodeFor(Exception)` — maps every known exception type to the correct
|
||||||
|
/// exit code per the plan's exit-code table.
|
||||||
|
/// - `acquireLock` / `releaseLock` / `isLocked` — PID-based lock file at
|
||||||
|
/// `<cacheDir>/.lock` to prevent concurrent tofu invocations.
|
||||||
|
///
|
||||||
|
/// All public APIs are `@safe`. Filesystem operations are isolated behind
|
||||||
|
/// `@trusted` helpers following the project convention.
|
||||||
|
module tofu.errors;
|
||||||
|
|
||||||
|
import tofu.build : BuildException;
|
||||||
|
import tofu.install : InstallException;
|
||||||
|
import tofu.fetch : FetchException;
|
||||||
|
import tofu.http : HttpException;
|
||||||
|
import tofu.index : IndexException;
|
||||||
|
import tofu.deps : DepException;
|
||||||
|
import tofu.resolve : ResolveException;
|
||||||
|
import tofu.config : ConfigException;
|
||||||
|
import tofu.cli : CliException;
|
||||||
|
|
||||||
|
import std.process : thisProcessID;
|
||||||
|
import std.file : exists, readText, write, remove;
|
||||||
|
import std.path : buildPath;
|
||||||
|
import std.conv : to, ConvException;
|
||||||
|
import std.stdio : stderr;
|
||||||
|
import std.string : indexOf;
|
||||||
|
|
||||||
|
version (Posix) {
|
||||||
|
import core.sys.posix.signal : kill;
|
||||||
|
import core.sys.posix.sys.types : pid_t;
|
||||||
|
import core.stdc.errno : ESRCH, errno;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// TofuError — base for exceptions carrying an explicit exit code
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// An exception that carries its own exit code. Direct callers throw this
|
||||||
|
/// instead of the module-specific exception when they already know the exit
|
||||||
|
/// code; `exitCodeFor` checks for it first.
|
||||||
|
class TofuError : Exception
|
||||||
|
{
|
||||||
|
int exitCode;
|
||||||
|
|
||||||
|
@safe this(int exitCode, string msg, string file = __FILE__,
|
||||||
|
size_t line = __LINE__)
|
||||||
|
{
|
||||||
|
this.exitCode = exitCode;
|
||||||
|
super(msg, file, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Exit-code mapping
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Map any exception to the appropriate exit code per the plan's table:
|
||||||
|
///
|
||||||
|
/// | Code | Meaning |
|
||||||
|
/// |------|---------|
|
||||||
|
/// | 0 | Success |
|
||||||
|
/// | 1 | Generic error (invalid args, usage) |
|
||||||
|
/// | 2 | Package not found in ZUUR |
|
||||||
|
/// | 3 | Dependency resolution failure |
|
||||||
|
/// | 4 | Build failure |
|
||||||
|
/// | 5 | Install failure |
|
||||||
|
/// | 6 | Network error |
|
||||||
|
/// | 7 | Config / tool-not-found error |
|
||||||
|
/// | 130 | SIGINT (handled separately in main) |
|
||||||
|
///
|
||||||
|
/// Marker checks on exception fields (`toolMissing`, `notFound`) allow
|
||||||
|
/// distinguishing sub-cases (e.g. tool-missing → 7 vs build failure → 4).
|
||||||
|
@safe int exitCodeFor(Exception e)
|
||||||
|
{
|
||||||
|
// ── TofuError carries its own exit code ─────────────────
|
||||||
|
{
|
||||||
|
auto te = cast(TofuError) e;
|
||||||
|
if (te !is null)
|
||||||
|
return te.exitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build errors ────────────────────────────────────────
|
||||||
|
{
|
||||||
|
auto be = cast(BuildException) e;
|
||||||
|
if (be !is null)
|
||||||
|
return be.toolMissing ? 7 : 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Install errors ──────────────────────────────────────
|
||||||
|
{
|
||||||
|
auto ie = cast(InstallException) e;
|
||||||
|
if (ie !is null)
|
||||||
|
return ie.toolMissing ? 7 : 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Fetch errors ────────────────────────────────────────
|
||||||
|
{
|
||||||
|
auto fe = cast(FetchException) e;
|
||||||
|
if (fe !is null)
|
||||||
|
return fe.notFound ? 2 : 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── HTTP / network errors ───────────────────────────────
|
||||||
|
{
|
||||||
|
auto he = cast(HttpException) e;
|
||||||
|
if (he !is null)
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto ie = cast(IndexException) e;
|
||||||
|
if (ie !is null)
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resolution errors ───────────────────────────────────
|
||||||
|
{
|
||||||
|
auto de = cast(DepException) e;
|
||||||
|
if (de !is null)
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto re = cast(ResolveException) e;
|
||||||
|
if (re !is null)
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Config errors ───────────────────────────────────────
|
||||||
|
{
|
||||||
|
auto ce = cast(ConfigException) e;
|
||||||
|
if (ce !is null)
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CLI / usage errors ─────────────────────────────────
|
||||||
|
{
|
||||||
|
auto ce = cast(CliException) e;
|
||||||
|
if (ce !is null)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generic fallback ────────────────────────────────────
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Lock-file exception
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Thrown when another tofu process holds the lock.
|
||||||
|
class LockException : Exception
|
||||||
|
{
|
||||||
|
@safe this(string msg, string file = __FILE__, size_t line = __LINE__)
|
||||||
|
{
|
||||||
|
super(msg, file, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Trusted filesystem helpers
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private @trusted bool lockFileExists(string path)
|
||||||
|
{
|
||||||
|
return exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @trusted string readLockFile(string path)
|
||||||
|
{
|
||||||
|
return readText(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @trusted void writeLockFile(string path, string content)
|
||||||
|
{
|
||||||
|
write(path, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @trusted void removeLockFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (exists(path))
|
||||||
|
remove(path);
|
||||||
|
}
|
||||||
|
catch (Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// PID-liveness check
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Check whether a PID is alive by sending signal 0.
|
||||||
|
/// Returns `true` if the PID exists, `false` if no such process.
|
||||||
|
version (Posix)
|
||||||
|
private @trusted bool pidAlive(int pid)
|
||||||
|
{
|
||||||
|
errno = 0;
|
||||||
|
int result = kill(cast(pid_t) pid, 0);
|
||||||
|
if (result == 0)
|
||||||
|
return true;
|
||||||
|
if (errno == ESRCH)
|
||||||
|
return false;
|
||||||
|
// Permission error or other — assume alive (conservative)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
private @safe bool pidAlive(int pid)
|
||||||
|
{
|
||||||
|
// Non-POSIX fallback: always assume alive (conservative)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Public lock API
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Build the lock-file path for a given cache directory.
|
||||||
|
private @safe string lockPath(string cacheDir)
|
||||||
|
{
|
||||||
|
return buildPath(cacheDir, ".lock");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the lock is held by a live process.
|
||||||
|
/// Returns `true` if another tofu instance is running, `false` otherwise.
|
||||||
|
/// If the lock file exists but the PID inside is dead, the lock is considered
|
||||||
|
/// stale — `isLocked` returns `false` and stale detection is handled by
|
||||||
|
/// `acquireLock`.
|
||||||
|
@safe bool isLocked(string cacheDir)
|
||||||
|
{
|
||||||
|
auto path = lockPath(cacheDir);
|
||||||
|
if (!lockFileExists(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string content;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
content = readLockFile(path);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pid;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pid = content.to!int;
|
||||||
|
}
|
||||||
|
catch (ConvException)
|
||||||
|
{
|
||||||
|
// Corrupted lock — treat as not locked (acquireLock fixes it)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pidAlive(pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Acquire the tofu lock at `<cacheDir>/.lock`.
|
||||||
|
///
|
||||||
|
/// If the lock is held by a live process → throws `LockException`.
|
||||||
|
/// If the lock is stale (dead PID) → removes it and proceeds.
|
||||||
|
/// If no lock exists → creates one with the current PID.
|
||||||
|
/// Returns `true` on successful acquisition.
|
||||||
|
@safe bool acquireLock(string cacheDir)
|
||||||
|
{
|
||||||
|
import tofu.log : logWarn;
|
||||||
|
|
||||||
|
auto path = lockPath(cacheDir);
|
||||||
|
auto myPid = thisProcessID.to!string;
|
||||||
|
|
||||||
|
if (lockFileExists(path))
|
||||||
|
{
|
||||||
|
string content;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
content = readLockFile(path);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Corrupted lock file — remove and proceed
|
||||||
|
removeLockFile(path);
|
||||||
|
writeLockFile(path, myPid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pid;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pid = content.to!int;
|
||||||
|
}
|
||||||
|
catch (ConvException)
|
||||||
|
{
|
||||||
|
// Corrupted lock content — remove and proceed
|
||||||
|
removeLockFile(path);
|
||||||
|
writeLockFile(path, myPid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pidAlive(pid))
|
||||||
|
{
|
||||||
|
// Stale lock — the PID is dead
|
||||||
|
logWarn("removing stale lock (PID %d not alive)", pid);
|
||||||
|
removeLockFile(path);
|
||||||
|
writeLockFile(path, myPid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live lock — another tofu is running
|
||||||
|
throw new LockException(
|
||||||
|
"another tofu process is running (lock: " ~ path
|
||||||
|
~ ", PID " ~ content ~ ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No lock file — create one
|
||||||
|
writeLockFile(path, myPid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release the tofu lock by removing the lock file.
|
||||||
|
/// Best-effort — exceptions during removal are silently ignored.
|
||||||
|
@safe void releaseLock(string cacheDir)
|
||||||
|
{
|
||||||
|
removeLockFile(lockPath(cacheDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// Unittests
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
version (unittest)
|
||||||
|
{
|
||||||
|
import std.file : tempDir, mkdirRecurse, rmdirRecurse;
|
||||||
|
import std.process : thisProcessID;
|
||||||
|
import std.path : buildPath;
|
||||||
|
import std.conv : to;
|
||||||
|
|
||||||
|
/// Create a unique temp directory for lock-file tests.
|
||||||
|
private @trusted string makeCacheDir(string suffix)
|
||||||
|
{
|
||||||
|
auto path = buildPath(tempDir, "tofu-errors-test-" ~ suffix
|
||||||
|
~ "-" ~ thisProcessID.to!string);
|
||||||
|
if (exists(path))
|
||||||
|
rmdirRecurse(path);
|
||||||
|
mkdirRecurse(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively remove a directory, ignoring errors.
|
||||||
|
private @trusted void removeCacheDir(string path)
|
||||||
|
{
|
||||||
|
try { rmdirRecurse(path); } catch (Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a lock artifact, ignoring errors.
|
||||||
|
private @trusted void removeLockArtifact(string cacheDir)
|
||||||
|
{
|
||||||
|
auto p = lockPath(cacheDir);
|
||||||
|
try { if (exists(p)) remove(p); } catch (Exception) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (1): exitCodeFor — HttpException → 6 ──────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new HttpException("HTTP 404 fetching https://example.com");
|
||||||
|
assert(exitCodeFor(e) == 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (2): exitCodeFor — BuildException → 4 ─────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new BuildException("build failed for foo: error");
|
||||||
|
assert(exitCodeFor(e) == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (3): exitCodeFor — BuildException(toolMissing) → 7 ─
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto be = new BuildException("zeta-makepkg not found");
|
||||||
|
be.toolMissing = true;
|
||||||
|
assert(exitCodeFor(be) == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (4): exitCodeFor — InstallException → 5 ───────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new InstallException("install failed for foo: error");
|
||||||
|
assert(exitCodeFor(e) == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (5): exitCodeFor — InstallException(toolMissing) → 7
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto ie = new InstallException("zeta not found");
|
||||||
|
ie.toolMissing = true;
|
||||||
|
assert(exitCodeFor(ie) == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (6): exitCodeFor — FetchException(notFound) → 2 ───
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto fe = new FetchException("package 'hello' not found in ZUUR recipes");
|
||||||
|
fe.notFound = true;
|
||||||
|
assert(exitCodeFor(fe) == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (7): exitCodeFor — FetchException(no marker) → 6 ──
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto fe = new FetchException("connection refused");
|
||||||
|
assert(exitCodeFor(fe) == 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (8): exitCodeFor — DepException → 3 ────────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new DepException("dependency cycle: A -> B -> A");
|
||||||
|
assert(exitCodeFor(e) == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (9): exitCodeFor — ResolveException → 3 ────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new ResolveException(
|
||||||
|
"dependency 'libfoo' not found in ZUUR");
|
||||||
|
assert(exitCodeFor(e) == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (10): exitCodeFor — ConfigException → 7 ────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new ConfigException("missing required config key");
|
||||||
|
assert(exitCodeFor(e) == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (11): exitCodeFor — IndexException → 6 ─────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new IndexException("ZUUR index is invalid");
|
||||||
|
assert(exitCodeFor(e) == 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (12): exitCodeFor — TofuError(explicit) bypass ─────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto te = new TofuError(42, "custom exit");
|
||||||
|
assert(exitCodeFor(te) == 42);
|
||||||
|
// TofuError is also an Exception → exitCodeFor should check it first
|
||||||
|
Exception e = te;
|
||||||
|
assert(exitCodeFor(e) == 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (13): exitCodeFor — generic Exception → 1 ──────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new Exception("unknown error");
|
||||||
|
assert(exitCodeFor(e) == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (14): lock — no lock → acquire succeeds ────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto tmp = makeCacheDir("lock-acquire");
|
||||||
|
scope (exit) {
|
||||||
|
removeLockArtifact(tmp);
|
||||||
|
removeCacheDir(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(acquireLock(tmp));
|
||||||
|
assert(lockFileExists(lockPath(tmp)));
|
||||||
|
// Release
|
||||||
|
releaseLock(tmp);
|
||||||
|
assert(!lockFileExists(lockPath(tmp)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (15): lock — stale lock (dead PID) → removed ───────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto tmp = makeCacheDir("lock-stale");
|
||||||
|
scope (exit) {
|
||||||
|
removeLockArtifact(tmp);
|
||||||
|
removeCacheDir(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a lock file with a PID that's almost certainly dead
|
||||||
|
writeLockFile(lockPath(tmp), "99999999");
|
||||||
|
// Acquire should detect the stale lock, remove it, and create a new one
|
||||||
|
assert(acquireLock(tmp));
|
||||||
|
// Read the new lock content
|
||||||
|
auto content = readLockFile(lockPath(tmp));
|
||||||
|
assert(content.to!int != 99999999, "Stale lock should have been replaced");
|
||||||
|
releaseLock(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (16): lock — live lock → LockException ─────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto tmp = makeCacheDir("lock-live");
|
||||||
|
scope (exit) {
|
||||||
|
removeLockArtifact(tmp);
|
||||||
|
removeCacheDir(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the current PID to simulate a live lock
|
||||||
|
writeLockFile(lockPath(tmp), thisProcessID.to!string);
|
||||||
|
|
||||||
|
bool caught = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
acquireLock(tmp);
|
||||||
|
assert(false, "Expected LockException");
|
||||||
|
}
|
||||||
|
catch (LockException e)
|
||||||
|
{
|
||||||
|
caught = true;
|
||||||
|
assert(e.msg.indexOf("another tofu process is running") >= 0,
|
||||||
|
"Expected 'another tofu process is running', got: " ~ e.msg);
|
||||||
|
assert(e.msg.indexOf(thisProcessID.to!string) >= 0,
|
||||||
|
"Expected PID in message");
|
||||||
|
}
|
||||||
|
assert(caught, "Should have thrown LockException");
|
||||||
|
|
||||||
|
// Clean up manually (our PID owns the lock)
|
||||||
|
releaseLock(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (17): lock — corrupted lock content → ignored ──────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto tmp = makeCacheDir("lock-corrupt");
|
||||||
|
scope (exit) {
|
||||||
|
removeLockArtifact(tmp);
|
||||||
|
removeCacheDir(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockFile(lockPath(tmp), "not-a-number");
|
||||||
|
assert(acquireLock(tmp),
|
||||||
|
"Should acquire lock when lock content is corrupted");
|
||||||
|
releaseLock(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (18): lock — isLocked checks liveness ──────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto tmp = makeCacheDir("lock-islocked");
|
||||||
|
scope (exit) {
|
||||||
|
removeLockArtifact(tmp);
|
||||||
|
removeCacheDir(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No lock file → not locked
|
||||||
|
assert(!isLocked(tmp));
|
||||||
|
|
||||||
|
// Dead PID → not locked
|
||||||
|
writeLockFile(lockPath(tmp), "99999999");
|
||||||
|
assert(!isLocked(tmp));
|
||||||
|
|
||||||
|
// Live PID → locked
|
||||||
|
removeLockArtifact(tmp);
|
||||||
|
writeLockFile(lockPath(tmp), thisProcessID.to!string);
|
||||||
|
assert(isLocked(tmp));
|
||||||
|
|
||||||
|
releaseLock(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (19): exitCodeFor — BuildException with stderr details → still 4 ──
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new BuildException("build failed for hello:\nerror: no space left on device");
|
||||||
|
assert(exitCodeFor(e) == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Test (20): exitCodeFor — CliException → 1 ──────────────
|
||||||
|
@safe unittest
|
||||||
|
{
|
||||||
|
auto e = new CliException("unknown option: -Z");
|
||||||
|
assert(exitCodeFor(e) == 1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user