Compare commits

..

24 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
0bc7a3ecc0 ci(nix): retry flake evaluation on GitHub API rate limits
Some checks failed
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-env-virtual (push) Has been cancelled
CI / test-env-nix (push) Has been cancelled
CI / test-e2e (push) Has been cancelled
CI / test-virgin-user (push) Has been cancelled
CI / test-virgin-root (push) Has been cancelled
CI / codesniffer-shellcheck (push) Has been cancelled
CI / codesniffer-ruff (push) Has been cancelled
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Add a reusable retry helper that detects GitHub API 403 rate-limit errors
during Nix flake evaluation and retries with exponential backoff.

Apply the retry logic to flake-only CI tests so transient GitHub rate
limits no longer cause random CI failures while preserving fast failure
for real errors.

https://chatgpt.com/share/693d7ec5-ac70-800f-a627-ef705c653ba1
2025-12-13 15:57:05 +01:00
Kevin Veen-Birkenbach
55a0ae4337 Release version 1.5.0
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:43:19 +01:00
Kevin Veen-Birkenbach
bcf284c5d6 Solved variable naming bug
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:33:38 +01:00
Kevin Veen-Birkenbach
db23b1a445 Solved ruff hints
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:30:10 +01:00
Kevin Veen-Birkenbach
506f69d8a7 Solved variable bug
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:27:06 +01:00
Kevin Veen-Birkenbach
097e64408f Fix repository deinstall logic and add unit tests for repository helpers
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Fix undefined repo_dir usage in repository deinstall action
- Centralize and harden get_repo_dir with strict validation and clear errors
- Expand user paths for repository base and binary directories
- Add unit tests for get_repo_dir and deinstall_repos
- Add comprehensive tests for resolve_repos identifier matching
- Remove obsolete command resolution tests no longer applicable

https://chatgpt.com/share/693d7442-c2d0-800f-9ff3-fb84d60eaeb4
2025-12-13 15:12:12 +01:00
Kevin Veen-Birkenbach
a3913d9489 Solved variable bug
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 15:05:34 +01:00
Kevin Veen-Birkenbach
c92fd44dd3 fix(uninstall): robustly remove pkgmgr venv auto-activation and leftover shell RC entries
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:48:59 +01:00
Kevin Veen-Birkenbach
2c3efa7a27 Solved shellcheck quoting issue
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:38:37 +01:00
Kevin Veen-Birkenbach
f388bc51bc Ruff autofix
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:36:55 +01:00
Kevin Veen-Birkenbach
4e28eba883 refactor(ci,build,test): rename distro to PKGMGR_DISTRO for consistent environment handling
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
https://chatgpt.com/share/693d6b63-12cc-800f-b55f-abc52ee7fb52
2025-12-13 14:34:15 +01:00
Kevin Veen-Birkenbach
b8acd634f8 Improve run_command error diagnostics with live output capture
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Switch run_command to a single-run execution model that streams stdout/stderr
live while capturing both streams in memory using selectors. This guarantees
that command errors (e.g. make install, pip, nix) always show full diagnostics
without re-running commands or risking deadlocks.

Add unit tests for preview mode, success execution, failure handling, and
allow_failure behavior.

Context:
https://chatgpt.com/share/replace-with-this-conversation-link
2025-12-13 14:29:53 +01:00
Kevin Veen-Birkenbach
fb68b325d6 Fix ShellCheck warnings and harden shell scripts
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Quote Docker volume names to avoid word splitting
- Add missing shebangs for proper shell detection
- Annotate sourced scripts for ShellCheck resolution
- Remove unused variables
- Explicitly disable SC2016 where literal RC strings are intended
- Improve robustness of cleanup logic

https://chatgpt.com/share/693d6557-a080-800f-8915-c57476569232
2025-12-13 14:08:35 +01:00
Kevin Veen-Birkenbach
650a22d425 Changed other formatation codesniffer solution
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 14:00:06 +01:00
Kevin Veen-Birkenbach
6a590d8780 Solved save user config bug 2025-12-13 13:55:49 +01:00
Kevin Veen-Birkenbach
5601ea442a **Refactor CI: make Ruff and ShellCheck reusable via workflow_call**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Convert Ruff and ShellCheck workflows to `workflow_call`
* Remove direct `push` / `pull_request` triggers
* Run sniffers only through centralized CI and release pipelines
* Prevent duplicate and uncontrolled sniffer executions

https://chatgpt.com/share/693d5f9a-5e70-800f-95da-837be2aedb4f
2025-12-13 13:44:04 +01:00
Kevin Veen-Birkenbach
5ff15013d7 Fix: remove unnecessary f-strings without interpolation
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Ruff (Python code sniffer) / codesniffer-ruff (push) Has been cancelled
ShellCheck / codesniffer-shellcheck (push) Has been cancelled
Remove extraneous f-string prefixes from string literals that do not contain
placeholders. This resolves Ruff F541 warnings without changing runtime
behavior or output.

https://chatgpt.com/share/693d5f15-f9e8-800f-bf69-b0dee0e4449c
2025-12-13 13:41:26 +01:00
Kevin Veen-Birkenbach
6ccc1c1490 Removed further Optional double imports
Some checks failed
Ruff (Python code sniffer) / codesniffer-ruff (push) Has been cancelled
ShellCheck / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 13:36:11 +01:00
Kevin Veen-Birkenbach
8ead3472dd Removed double import 2025-12-13 13:33:34 +01:00
Kevin Veen-Birkenbach
422ac8b837 **Enable Nix experimental features system-wide and refactor Nix bootstrap config**
Some checks failed
Ruff (Python code sniffer) / codesniffer-ruff (push) Has been cancelled
ShellCheck / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Rename `config.sh` to `bootstrap_config.sh` to clearly separate installer bootstrap config from Nix system config
* Add `nix_conf_file.sh` to manage `/etc/nix/nix.conf` safely and idempotently
* Ensure `nix-command` and `flakes` are enabled without overwriting existing experimental features
* Invoke Nix config enforcement from `nix/init.sh` during root installation
* Update documentation and ShellCheck annotations accordingly
* Extend CLI git proxy to include `git status`

https://chatgpt.com/share/693d5c4a-bad0-800f-adaf-4719dd4ca377
2025-12-13 13:29:48 +01:00
Kevin Veen-Birkenbach
ea84c1b14e Add ShellCheck and Ruff code sniffers to CI and release workflows
Some checks failed
Ruff (Python code sniffer) / codesniffer-ruff (push) Has been cancelled
ShellCheck / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / codesniffer-shellcheck (push) Has been cancelled
Mark stable commit / codesniffer-ruff (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
- Introduce dedicated ShellCheck workflow for Bash scripts
- Add Ruff as Python code sniffer for src/ and tests/
- Integrate both sniffers into main CI pipeline
- Require successful sniffer runs before marking a release as stable
- Ensure consistent code quality checks across CI and release workflows

https://chatgpt.com/share/693d5b26-293c-800f-999d-48b2950b9417
2025-12-13 13:24:58 +01:00
Kevin Veen-Birkenbach
71a4e7e725 Added git status proxy
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 13:13:03 +01:00
Kevin Veen-Birkenbach
fb737ef290 Optimized Changelog
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-13 08:40:37 +01:00
Kevin Veen-Birkenbach
2963a43754 **Refactor README: streamline rationale, features, install and run sections**
* Simplify *Why PKGMGR* into concise prose and add Docker images as reproducible system baselines linked to Infinito.Nexus
* Condense Features into a single, readable overview without command lists
* Clean up Architecture section and keep diagram metadata consistent
* Reorganize Installation with clear download, dependencies, install and setup modes
* Introduce a unified *Run PKGMGR* section differentiating Nix, Docker and venv usage with consistent examples
2025-12-13 08:34:39 +01:00
71 changed files with 796 additions and 412 deletions

View File

@@ -27,3 +27,9 @@ jobs:
test-virgin-root: test-virgin-root:
uses: ./.github/workflows/test-virgin-root.yml uses: ./.github/workflows/test-virgin-root.yml
codesniffer-shellcheck:
uses: ./.github/workflows/codesniffer-shellcheck.yml
codesniffer-ruff:
uses: ./.github/workflows/codesniffer-ruff.yml

23
.github/workflows/codesniffer-ruff.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Ruff (Python code sniffer)
on:
workflow_call:
jobs:
codesniffer-ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install ruff
- name: Run ruff
run: |
ruff check src tests

View File

@@ -0,0 +1,14 @@
name: ShellCheck
on:
workflow_call:
jobs:
codesniffer-shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install ShellCheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Run ShellCheck
run: shellcheck -x $(find scripts -type f -name '*.sh' -print)

View File

@@ -29,8 +29,16 @@ jobs:
test-virgin-root: test-virgin-root:
uses: ./.github/workflows/test-virgin-root.yml uses: ./.github/workflows/test-virgin-root.yml
codesniffer-shellcheck:
uses: ./.github/workflows/codesniffer-shellcheck.yml
codesniffer-ruff:
uses: ./.github/workflows/codesniffer-ruff.yml
mark-stable: mark-stable:
needs: needs:
- codesniffer-shellcheck
- codesniffer-ruff
- test-unit - test-unit
- test-integration - test-integration
- test-env-nix - test-env-nix

View File

@@ -22,4 +22,4 @@ jobs:
- name: Run E2E tests via make (${{ matrix.distro }}) - name: Run E2E tests via make (${{ matrix.distro }})
run: | run: |
set -euo pipefail set -euo pipefail
distro="${{ matrix.distro }}" make test-e2e PKGMGR_DISTRO="${{ matrix.distro }}" make test-e2e

View File

@@ -23,4 +23,4 @@ jobs:
- name: Nix flake-only test (${{ matrix.distro }}) - name: Nix flake-only test (${{ matrix.distro }})
run: | run: |
set -euo pipefail set -euo pipefail
distro="${{ matrix.distro }}" make test-env-nix PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-nix

View File

@@ -25,4 +25,4 @@ jobs:
- name: Run container tests (${{ matrix.distro }}) - name: Run container tests (${{ matrix.distro }})
run: | run: |
set -euo pipefail set -euo pipefail
distro="${{ matrix.distro }}" make test-env-virtual PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-virtual

View File

@@ -16,4 +16,4 @@ jobs:
run: docker version run: docker version
- name: Run integration tests via make (Arch container) - name: Run integration tests via make (Arch container)
run: make test-integration distro="arch" run: make test-integration PKGMGR_DISTRO="arch"

View File

@@ -16,4 +16,4 @@ jobs:
run: docker version run: docker version
- name: Run unit tests via make (Arch container) - name: Run unit tests via make (Arch container)
run: make test-unit distro="arch" run: make test-unit PKGMGR_DISTRO="arch"

View File

@@ -23,7 +23,7 @@ jobs:
- name: Build virgin container (${{ matrix.distro }}) - name: Build virgin container (${{ matrix.distro }})
run: | run: |
set -euo pipefail set -euo pipefail
distro="${{ matrix.distro }}" make build-missing-virgin PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
# 🔹 RUN test inside virgin image # 🔹 RUN test inside virgin image
- name: Virgin ${{ matrix.distro }} pkgmgr test (root) - name: Virgin ${{ matrix.distro }} pkgmgr test (root)

View File

@@ -23,7 +23,7 @@ jobs:
- name: Build virgin container (${{ matrix.distro }}) - name: Build virgin container (${{ matrix.distro }})
run: | run: |
set -euo pipefail set -euo pipefail
distro="${{ matrix.distro }}" make build-missing-virgin PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
# 🔹 RUN test inside virgin image as non-root # 🔹 RUN test inside virgin image as non-root
- name: Virgin ${{ matrix.distro }} pkgmgr test (user) - name: Virgin ${{ matrix.distro }} pkgmgr test (user)

View File

@@ -1,11 +1,24 @@
## [1.5.0] - 2025-12-13
* - Commands now show live output while running, making long operations easier to follow
- Error messages include full command output, making failures easier to understand and debug
- Deinstallation is more complete and predictable, removing CLI links and properly cleaning up repositories
- Preview mode is more trustworthy, clearly showing what would happen without making changes
- Repository configuration problems are detected earlier with clear, user-friendly explanations
- More consistent behavior across different Linux distributions
- More reliable execution in Docker containers and CI environments
- Nix-based execution works more smoothly, especially when running as root or inside containers
- Existing commands, scripts, and workflows continue to work without any breaking changes
## [1.4.1] - 2025-12-12 ## [1.4.1] - 2025-12-12
* Fixed (#1) stable release container publishing * Fixed stable release container publishing
## [1.4.0] - 2025-12-12 ## [1.4.0] - 2025-12-12
* **Docker Container Building** **Docker Container Building**
* New official container images are automatically published on each release. * New official container images are automatically published on each release.
* Images are available per distribution and as a default Arch-based image. * Images are available per distribution and as a default Arch-based image.
@@ -19,7 +32,7 @@
## [1.3.0] - 2025-12-12 ## [1.3.0] - 2025-12-12
* **Minor release Stability & CI hardening** **Stability & CI hardening**
* Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu * Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu
* Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths * Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths
@@ -31,7 +44,7 @@
## [1.2.1] - 2025-12-12 ## [1.2.1] - 2025-12-12
* **Changed** **Changed**
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities. * Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
@@ -48,7 +61,7 @@
## [1.2.0] - 2025-12-12 ## [1.2.0] - 2025-12-12
* **Release workflow overhaul** **Release workflow overhaul**
* Introduced a fully structured release workflow with clear phases and safeguards * Introduced a fully structured release workflow with clear phases and safeguards
* Added preview-first releases with explicit confirmation before execution * Added preview-first releases with explicit confirmation before execution
@@ -65,7 +78,8 @@
## [1.0.0] - 2025-12-11 ## [1.0.0] - 2025-12-11
* **1.0.0 Official Stable Release 🎉** **Official Stable Release 🎉**
*First stable release of PKGMGR, the multi-distro development and package workflow manager.* *First stable release of PKGMGR, the multi-distro development and package workflow manager.*
--- ---
@@ -158,7 +172,7 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
## [0.9.1] - 2025-12-10 ## [0.9.1] - 2025-12-10
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map. * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores. * Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage. * Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`. * Removed deprecated files and updated `.gitignore`.
@@ -253,47 +267,45 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
## [0.7.1] - 2025-12-09 ## [0.7.1] - 2025-12-09
* Fix floating 'latest' tag logic: dereference annotated target (vX.Y.Z^{}), add tag message to avoid Git errors, ensure best-effort update without blocking releases, and update unit tests (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff). * Fix floating 'latest' tag logic
* dereference annotated target (vX.Y.Z^{})
* add tag message to avoid Git errors
* ensure best-effort update without blocking releases
## [0.7.0] - 2025-12-09 ## [0.7.0] - 2025-12-09
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow, ensure main/master are updated from origin before tagging, and extend unit/e2e tests including 'pkgmgr release --help' coverage (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff) * Add Git helpers for branch sync and floating 'latest' tag in the release workflow
* ensure main/master are updated from origin before tagging
## [0.6.0] - 2025-12-09 ## [0.6.0] - 2025-12-09
* Expose DISTROS and BASE_IMAGE_* variables as exported Makefile environment variables so all build and test commands can consume them dynamically. By exporting these values, every Make target (e.g., build, build-no-cache, build-missing, test-container, test-unit, test-e2e) and every delegated script in scripts/build/ and scripts/test/ now receives a consistent view of the supported distributions and their base container images. This change removes duplicated definitions across scripts, ensures reproducible builds, and allows build tooling to react automatically when new distros or base images are added to the Makefile. * Consistent view of the supported distributions and their base container images.
## [0.5.1] - 2025-12-09 ## [0.5.1] - 2025-12-09
* Refine pkgmgr release CLI close wiring and integration tests for --close flag (ChatGPT: https://chatgpt.com/share/69376b4e-8440-800f-9d06-535ec1d7a40e) * Refine pkgmgr release CLI close wiring and integration tests for --close flag
## [0.5.0] - 2025-12-09 ## [0.5.0] - 2025-12-09
* Add pkgmgr branch close subcommand, extend CLI parser wiring, and add unit tests for branch handling and version version-selection logic (see ChatGPT conversation: https://chatgpt.com/share/693762a3-9ea8-800f-a640-bc78170953d1) * Add pkgmgr branch close subcommand, extend CLI parser wiring
## [0.4.3] - 2025-12-09 ## [0.4.3] - 2025-12-09
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject), and add comprehensive unit/e2e tests for release and branch commands (see ChatGPT conversation: https://chatgpt.com/share/69375cfe-9e00-800f-bd65-1bd5937e1696) * Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject)
## [0.4.2] - 2025-12-09 ## [0.4.2] - 2025-12-09
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62) * Wire pkgmgr release CLI to new helpe
## [0.4.1] - 2025-12-08 ## [0.4.1] - 2025-12-08
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62) * Add branch close subcommand and integrate release close/editor flow
## [0.4.0] - 2025-12-08 ## [0.4.0] - 2025-12-08
* Add branch closing helper and --close flag to release command, including CLI wiring and tests (see https://chatgpt.com/share/69374aec-74ec-800f-bde3-5d91dfdb9b91) * Add branch closing helper and --close flag to release command
## [0.3.0] - 2025-12-08 ## [0.3.0] - 2025-12-08
@@ -304,13 +316,10 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
- New config update logic + default YAML sync - New config update logic + default YAML sync
- Improved proxy command handling - Improved proxy command handling
- Full CLI routing refactor - Full CLI routing refactor
- Expanded E2E tests for list, proxy, and selection logic
Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
## [0.2.0] - 2025-12-08 ## [0.2.0] - 2025-12-08
* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630) * Add preview-first release workflow and extended packaging support
## [0.1.0] - 2025-12-08 ## [0.1.0] - 2025-12-08
@@ -319,5 +328,4 @@ Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
## [0.1.0] - 2025-12-08 ## [0.1.0] - 2025-12-08
* Implement unified release helper with preview mode, multi-packaging version bumps, and new integration/unit tests (see ChatGPT conversation 2025-12-08: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630) * Implement unified release helper with preview mode, multi-packaging version bumps

View File

@@ -7,8 +7,8 @@
# Distro # Distro
# Options: arch debian ubuntu fedora centos # Options: arch debian ubuntu fedora centos
DISTROS ?= arch debian ubuntu fedora centos DISTROS ?= arch debian ubuntu fedora centos
distro ?= arch PKGMGR_DISTRO ?= arch
export distro export PKGMGR_DISTRO
# ------------------------------------------------------------ # ------------------------------------------------------------
# Base images # Base images
@@ -75,7 +75,7 @@ build-no-cache-all:
@set -e; \ @set -e; \
for d in $(DISTROS); do \ for d in $(DISTROS); do \
echo "=== build-no-cache: $$d ==="; \ echo "=== build-no-cache: $$d ==="; \
distro="$$d" $(MAKE) build-no-cache; \ PKGMGR_DISTRO="$$d" $(MAKE) build-no-cache; \
done done
# ------------------------------------------------------------ # ------------------------------------------------------------
@@ -101,7 +101,7 @@ test-env-nix: build-missing
test: test-env-virtual test-unit test-integration test-e2e test: test-env-virtual test-unit test-integration test-e2e
delete-volumes: delete-volumes:
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true @docker volume rm "pkgmgr_nix_store_${PKGMGR_DISTRO}" "pkgmgr_nix_cache_${PKGMGR_DISTRO}" || echo "No volumes to delete."
purge: delete-volumes build-no-cache purge: delete-volumes build-no-cache

167
README.md
View File

@@ -25,52 +25,37 @@ together into repeatable development workflows.
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
single operating system. PKGMGR instead focuses on **your repositories and single operating system. PKGMGR instead focuses on **your repositories and
development lifecycle**: development lifecycle**. It provides one configuration for all repositories,
one unified CLI to interact with them, and a Nix-based foundation that keeps
tooling reproducible across distributions.
* one configuration for all your repos, Native package managers are still used where they make sense. PKGMGR coordinates
* one CLI to interact with them, the surrounding development, build and release workflows in a consistent way.
* one Nix-based layer to keep tooling reproducible across distros.
You keep using your native package manager where it makes sense PKGMGR In addition, PKGMGR provides Docker images that can serve as a **reproducible
coordinates the *development and release flow* around it. system baseline**. These images bundle the complete PKGMGR toolchain and are
designed to be reused as a stable execution environment across machines,
pipelines and teams. This approach is specifically used within
[**Infinito.Nexus**](https://s.infinito.nexus/code) to make complex systems
distribution-independent while remaining fully reproducible.
--- ---
## Features 🚀 ## Features 🚀
### Multi-distro development & packaging PKGMGR enables multi-distro development and packaging by managing multiple
repositories from a single configuration file. It drives complete release
pipelines across Linux distributions using Nix flakes, Python build metadata,
native OS packages such as Arch, Debian and RPM formats, and additional ecosystem
integrations like Ansible.
* Manage **many repositories at once** from a single `config/config.yaml`. All functionality is exposed through a unified `pkgmgr` command-line interface
* Drive full **release pipelines** across Linux distributions using: that works identically on every supported distribution. It combines repository
management, Git operations, Docker and Compose orchestration, as well as
versioning, release and changelog workflows. Many commands support a preview
mode, allowing you to inspect the underlying actions before they are executed.
* Nix flakes (`flake.nix`) ---
* PyPI style builds (`pyproject.toml`)
* OS packages (PKGBUILD, Debian control/changelog, RPM spec)
* Ansible Galaxy metadata and more.
### Rich CLI for daily work
All commands are exposed via the `pkgmgr` CLI and are available on every distro:
* **Repository management**
* `clone`, `update`, `install`, `delete`, `deinstall`, `path`, `list`, `config`
* **Git proxies**
* `pull`, `push`, `status`, `diff`, `add`, `show`, `checkout`,
`reset`, `revert`, `rebase`, `commit`, `branch`
* **Docker & Compose orchestration**
* `build`, `up`, `down`, `exec`, `ps`, `start`, `stop`, `restart`
* **Release toolchain**
* `version`, `release`, `changelog`, `make`
* **Mirror & workflow helpers**
* `mirror` (list/diff/merge/setup), `shell`, `terminal`, `code`, `explore`
Many of these commands support `--preview` mode so you can inspect the
underlying Git or Docker calls without executing them.
### Full development workflows ### Full development workflows
@@ -83,10 +68,6 @@ versioning features it can drive **end-to-end workflows**:
4. Build distro-specific packages. 4. Build distro-specific packages.
5. Keep all mirrors and working copies in sync. 5. Keep all mirrors and working copies in sync.
The extensive E2E tests (`tests/e2e/`) and GitHub Actions workflows (including
“virgin user” and “virgin root” Arch tests) validate these flows across
different Linux environments.
--- ---
## Architecture & Setup Map 🗺️ ## Architecture & Setup Map 🗺️
@@ -99,25 +80,26 @@ The following diagram gives a full overview of:
![PKGMGR Architecture](assets/map.png) ![PKGMGR Architecture](assets/map.png)
**Diagram status:** 12 December 2025 **Diagram status:** 12 December 2025
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp) **Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
--- ---
Perfekt, dann hier die **noch kompaktere und korrekt differenzierte Version**, die **nur** zwischen
**`make setup`** und **`make setup-venv`** unterscheidet und exakt deinem Verhalten entspricht.
README-ready, ohne Over-Engineering.
---
## Installation ⚙️ ## Installation ⚙️
PKGMGR can be installed using `make`. PKGMGR can be installed using `make`.
The setup mode defines **which runtime layers are prepared**. The setup mode defines **which runtime layers are prepared**.
--- ---
### Download
```bash
git clone https://github.com/kevinveenbirkenbach/package-manager.git
cd package-manager
```
### Dependency installation (optional) ### Dependency installation (optional)
System dependencies required **before running any *make* commands** are installed via: System dependencies required **before running any *make* commands** are installed via:
@@ -128,8 +110,13 @@ scripts/installation/dependencies.sh
The script detects and normalizes the OS and installs the required **system-level dependencies** accordingly. The script detects and normalizes the OS and installs the required **system-level dependencies** accordingly.
### Install
--- ```bash
git clone https://github.com/kevinveenbirkenbach/package-manager.git
cd package-manager
make install
```
### Setup modes ### Setup modes
@@ -138,17 +125,8 @@ The script detects and normalizes the OS and installs the required **system-leve
| **make setup** | Python venv **and** Nix | Full development & CI | | **make setup** | Python venv **and** Nix | Full development & CI |
| **make setup-venv** | Python venv only | Local user setup | | **make setup-venv** | Python venv only | Local user setup |
---
### Install & setup ##### Full setup (venv + Nix)
```bash
git clone https://github.com/kevinveenbirkenbach/package-manager.git
cd package-manager
make install
```
#### Full setup (venv + Nix)
```bash ```bash
make setup make setup
@@ -156,7 +134,7 @@ make setup
Use this for CI, servers, containers and full development workflows. Use this for CI, servers, containers and full development workflows.
#### Venv-only setup ##### Venv-only setup
```bash ```bash
make setup-venv make setup-venv
@@ -167,38 +145,77 @@ Use this if you want PKGMGR isolated without Nix integration.
--- ---
## Run without installation (Nix) Alles klar 🙂
Hier ist der **RUN-Abschnitt ohne Gedankenstriche**, klar nach **Nix, Docker und venv** getrennt:
Run PKGMGR directly via Nix Flakes. ---
## Run PKGMGR 🧰
PKGMGR can be executed in different environments.
All modes expose the same CLI and commands.
---
### Run via Nix (no installation)
```bash ```bash
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
``` ```
Example: ---
```bash ### Run via Docker 🐳
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- version pkgmgr
```
Notes: PKGMGR can be executed **inside Docker containers** for CI, testing and isolated
workflows.
---
* full flake URL required #### Container types
* `--` separates Nix and PKGMGR arguments
* can be used alongside any setup mode Two container types are available.
| Image type | Contains | Typical use |
| ---------- | ----------------------------- | ----------------------- |
| **Virgin** | Base OS + system dependencies | Clean test environments |
| **Stable** | PKGMGR + Nix (flakes enabled) | Ready-to-use workflows |
Example images:
* Virgin: `pkgmgr-arch-virgin`
* Stable: `ghcr.io/kevinveenbirkenbach/pkgmgr:stable`
Use **virgin images** for isolated test runs,
use the **stable image** for fast, reproducible execution.
--- ---
## Usage 🧰 #### Run examples
After installation, the main entry point is: ```bash
docker run --rm -it \
-v "$PWD":/src \
-w /src \
ghcr.io/kevinveenbirkenbach/pkgmgr:stable \
pkgmgr --help
```
---
### Run via virtual environment (venv)
After activating the venv:
```bash ```bash
pkgmgr --help pkgmgr --help
``` ```
This prints a list of all available subcommands. ---
The help for each command is available via:
This allows you to choose between zero install execution using Nix, fully prebuilt
Docker environments or local isolated venv setups with identical command behavior.
--- ---

View File

@@ -26,17 +26,13 @@
packages = forAllSystems (system: packages = forAllSystems (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
# Single source of truth for pkgmgr: Python 3.11
# - Matches pyproject.toml: requires-python = ">=3.11"
# - Uses python311Packages so that PyYAML etc. are available
python = pkgs.python311; python = pkgs.python311;
pyPkgs = pkgs.python311Packages; pyPkgs = pkgs.python311Packages;
in in
rec { rec {
pkgmgr = pyPkgs.buildPythonApplication { pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager"; pname = "package-manager";
version = "1.4.1"; version = "1.5.0";
# Use the git repo as source # Use the git repo as source
src = ./.; src = ./.;

View File

@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "package-manager" name = "package-manager"
version = "1.4.1" version = "1.5.0"
description = "Kevin's package-manager tool (pkgmgr)" description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"

View File

@@ -8,13 +8,13 @@ set -euo pipefail
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}" : "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
resolve_base_image() { resolve_base_image() {
local distro="$1" local PKGMGR_DISTRO="$1"
case "$distro" in case "$PKGMGR_DISTRO" in
arch) echo "$BASE_IMAGE_ARCH" ;; arch) echo "$BASE_IMAGE_ARCH" ;;
debian) echo "$BASE_IMAGE_DEBIAN" ;; debian) echo "$BASE_IMAGE_DEBIAN" ;;
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;; ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
fedora) echo "$BASE_IMAGE_FEDORA" ;; fedora) echo "$BASE_IMAGE_FEDORA" ;;
centos) echo "$BASE_IMAGE_CENTOS" ;; centos) echo "$BASE_IMAGE_CENTOS" ;;
*) echo "ERROR: Unknown distro '$distro'" >&2; exit 1 ;; *) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
esac esac
} }

View File

@@ -2,9 +2,11 @@
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=./scripts/build/base.sh
source "${SCRIPT_DIR}/base.sh" source "${SCRIPT_DIR}/base.sh"
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}" : "${PKGMGR_DISTRO:?Environment variable 'PKGMGR_DISTRO' must be set (arch|debian|ubuntu|fedora|centos)}"
NO_CACHE=0 NO_CACHE=0
MISSING_ONLY=0 MISSING_ONLY=0
@@ -20,13 +22,13 @@ IS_STABLE="false" # "true" -> publish stable tags
DEFAULT_DISTRO="arch" DEFAULT_DISTRO="arch"
usage() { usage() {
local default_tag="pkgmgr-${distro}" local default_tag="pkgmgr-${PKGMGR_DISTRO}"
if [[ -n "${TARGET:-}" ]]; then if [[ -n "${TARGET:-}" ]]; then
default_tag="${default_tag}-${TARGET}" default_tag="${default_tag}-${TARGET}"
fi fi
cat <<EOF cat <<EOF
Usage: distro=<distro> $0 [options] Usage: PKGMGR_DISTRO=<distro> $0 [options]
Build options: Build options:
--missing Build only if the image does not already exist (local build only) --missing Build only if the image does not already exist (local build only)
@@ -101,13 +103,13 @@ done
# Derive default local tag if not provided # Derive default local tag if not provided
if [[ -z "${IMAGE_TAG}" ]]; then if [[ -z "${IMAGE_TAG}" ]]; then
IMAGE_TAG="${REPO_PREFIX}-${distro}" IMAGE_TAG="${REPO_PREFIX}-${PKGMGR_DISTRO}"
if [[ -n "${TARGET}" ]]; then if [[ -n "${TARGET}" ]]; then
IMAGE_TAG="${IMAGE_TAG}-${TARGET}" IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
fi fi
fi fi
BASE_IMAGE="$(resolve_base_image "$distro")" BASE_IMAGE="$(resolve_base_image "$PKGMGR_DISTRO")"
# Local-only "missing" shortcut # Local-only "missing" shortcut
if [[ "${MISSING_ONLY}" == "1" ]]; then if [[ "${MISSING_ONLY}" == "1" ]]; then
@@ -139,7 +141,7 @@ fi
echo echo
echo "------------------------------------------------------------" echo "------------------------------------------------------------"
echo "[build] Building image" echo "[build] Building image"
echo "distro = ${distro}" echo "distro = ${PKGMGR_DISTRO}"
echo "BASE_IMAGE = ${BASE_IMAGE}" echo "BASE_IMAGE = ${BASE_IMAGE}"
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
@@ -165,14 +167,14 @@ if [[ -n "${TARGET}" ]]; then
fi fi
compute_publish_tags() { compute_publish_tags() {
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${distro}" local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${PKGMGR_DISTRO}"
local alias_tag_base="" local alias_tag_base=""
if [[ -n "${TARGET}" ]]; then if [[ -n "${TARGET}" ]]; then
distro_tag_base="${distro_tag_base}-${TARGET}" distro_tag_base="${distro_tag_base}-${TARGET}"
fi fi
if [[ "${distro}" == "${DEFAULT_DISTRO}" ]]; then if [[ "${PKGMGR_DISTRO}" == "${DEFAULT_DISTRO}" ]]; then
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}" alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
if [[ -n "${TARGET}" ]]; then if [[ -n "${TARGET}" ]]; then
alias_tag_base="${alias_tag_base}-${TARGET}" alias_tag_base="${alias_tag_base}-${TARGET}"

View File

@@ -30,11 +30,11 @@ echo "[publish] DISTROS=${DISTROS}"
for d in ${DISTROS}; do for d in ${DISTROS}; do
echo echo
echo "============================================================" echo "============================================================"
echo "[publish] distro=${d}" echo "[publish] PKGMGR_DISTRO=${d}"
echo "============================================================" echo "============================================================"
# virgin # virgin
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \ PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
--publish \ --publish \
--registry "${REGISTRY}" \ --registry "${REGISTRY}" \
--owner "${OWNER}" \ --owner "${OWNER}" \
@@ -43,7 +43,7 @@ for d in ${DISTROS}; do
--target virgin --target virgin
# full (default target) # full (default target)
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \ PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
--publish \ --publish \
--registry "${REGISTRY}" \ --registry "${REGISTRY}" \
--owner "${OWNER}" \ --owner "${OWNER}" \

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[docker] Starting package-manager container" echo "[docker] Starting package-manager container"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -22,7 +22,7 @@ It is invoked during package installation (Arch/Debian/Fedora scriptlets) and ca
The entry point sources small, focused modules from *scripts/nix/lib/*: The entry point sources small, focused modules from *scripts/nix/lib/*:
- *config.sh* — configuration defaults (installer URL, retry timing) - *bootstrap_config.sh* — configuration defaults (installer URL, retry timing)
- *detect.sh* — container detection helpers - *detect.sh* — container detection helpers
- *path.sh* — PATH adjustments and `nix` binary resolution helpers - *path.sh* — PATH adjustments and `nix` binary resolution helpers
- *symlinks.sh* — user/global symlink helpers for stable `nix` discovery - *symlinks.sh* — user/global symlink helpers for stable `nix` discovery

View File

@@ -1,22 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# shellcheck source=lib/config.sh
# shellcheck source=lib/detect.sh
# shellcheck source=lib/path.sh
# shellcheck source=lib/symlinks.sh
# shellcheck source=lib/users.sh
# shellcheck source=lib/install.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/config.sh" # shellcheck source=./scripts/nix/lib/bootstrap_config.sh
source "${SCRIPT_DIR}/lib/bootstrap_config.sh"
# shellcheck source=./scripts/nix/lib/detect.sh
source "${SCRIPT_DIR}/lib/detect.sh" source "${SCRIPT_DIR}/lib/detect.sh"
# shellcheck source=./scripts/nix/lib/path.sh
source "${SCRIPT_DIR}/lib/path.sh" source "${SCRIPT_DIR}/lib/path.sh"
# shellcheck source=./scripts/nix/lib/symlinks.sh
source "${SCRIPT_DIR}/lib/symlinks.sh" source "${SCRIPT_DIR}/lib/symlinks.sh"
# shellcheck source=./scripts/nix/lib/users.sh
source "${SCRIPT_DIR}/lib/users.sh" source "${SCRIPT_DIR}/lib/users.sh"
# shellcheck source=./scripts/nix/lib/install.sh
source "${SCRIPT_DIR}/lib/install.sh" source "${SCRIPT_DIR}/lib/install.sh"
# shellcheck source=./scripts/nix/lib/nix_conf_file.sh
source "${SCRIPT_DIR}/lib/nix_conf_file.sh"
echo "[init-nix] Starting Nix initialization..." echo "[init-nix] Starting Nix initialization..."
main() { main() {
@@ -26,6 +33,7 @@ main() {
ensure_nix_on_path ensure_nix_on_path
if [[ "${EUID:-0}" -eq 0 ]]; then if [[ "${EUID:-0}" -eq 0 ]]; then
nixconf_ensure_experimental_features
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)" ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
else else
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)" ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
@@ -106,6 +114,10 @@ main() {
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
ensure_nix_on_path ensure_nix_on_path
if [[ "${EUID:-0}" -eq 0 ]]; then
nixconf_ensure_experimental_features
fi
local nix_bin_post local nix_bin_post
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)" nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Prevent double-sourcing
if [[ -n "${PKGMGR_NIX_CONF_FILE_SH:-}" ]]; then
return 0
fi
PKGMGR_NIX_CONF_FILE_SH=1
nixconf_file_path() {
echo "/etc/nix/nix.conf"
}
nixconf_ensure_experimental_features() {
local nix_conf want
nix_conf="$(nixconf_file_path)"
want="experimental-features = nix-command flakes"
mkdir -p /etc/nix
if [[ ! -f "${nix_conf}" ]]; then
echo "[nix-conf] Creating ${nix_conf} with: ${want}"
printf "%s\n" "${want}" >"${nix_conf}"
return 0
fi
if grep -qE '^\s*experimental-features\s*=' "${nix_conf}"; then
if grep -qE '^\s*experimental-features\s*=.*\bnix-command\b' "${nix_conf}" \
&& grep -qE '^\s*experimental-features\s*=.*\bflakes\b' "${nix_conf}"; then
echo "[nix-conf] experimental-features already correct"
return 0
fi
echo "[nix-conf] Extending experimental-features in ${nix_conf}"
local current
current="$(grep -E '^\s*experimental-features\s*=' "${nix_conf}" | head -n1 | cut -d= -f2-)"
current="$(echo "${current}" | xargs)" # trim
# Build a merged feature string without duplicates (simple token set)
local merged="nix-command flakes"
local token
for token in ${current}; do
if [[ " ${merged} " != *" ${token} "* ]]; then
merged="${merged} ${token}"
fi
done
sed -i "s|^\s*experimental-features\s*=.*|experimental-features = ${merged}|" "${nix_conf}"
return 0
fi
echo "[nix-conf] Appending to ${nix_conf}: ${want}"
printf "\n%s\n" "${want}" >>"${nix_conf}"
}

52
scripts/nix/lib/retry_403.sh Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${PKGMGR_NIX_RETRY_403_SH:-}" ]]; then
return 0
fi
PKGMGR_NIX_RETRY_403_SH=1
# Retry only when we see the GitHub API rate limit 403 error during nix flake evaluation.
# Retries 7 times with delays: 10, 30, 50, 80, 130, 210, 420 seconds.
run_with_github_403_retry() {
local -a delays=(10 30 50 80 130 210 420)
local attempt=0
local max_retries="${#delays[@]}"
while true; do
local err tmp
tmp="$(mktemp -t nix-err.XXXXXX)"
err=0
# Run the command; capture stderr for inspection while preserving stdout.
if "$@" 2>"$tmp"; then
rm -f "$tmp"
return 0
else
err=$?
fi
# Only retry on the specific GitHub API rate limit 403 case.
if grep -qE 'HTTP error 403' "$tmp" && grep -qiE 'API rate limit exceeded|api\.github\.com' "$tmp"; then
if (( attempt >= max_retries )); then
cat "$tmp" >&2
rm -f "$tmp"
return "$err"
fi
local sleep_s="${delays[$attempt]}"
attempt=$((attempt + 1))
echo "[nix-retry] GitHub API rate-limit (403). Retry ${attempt}/${max_retries} in ${sleep_s}s: $*" >&2
cat "$tmp" >&2
rm -f "$tmp"
sleep "$sleep_s"
continue
fi
# Not our retry case -> fail fast with original stderr.
cat "$tmp" >&2
rm -f "$tmp"
return "$err"
done
}

View File

@@ -1,3 +1,5 @@
#!/usr/bin/env bash
# ------------------------------------------------------------ # ------------------------------------------------------------
# Nix shell mode: do not touch venv, only run install # Nix shell mode: do not touch venv, only run install
# ------------------------------------------------------------ # ------------------------------------------------------------

View File

@@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${PROJECT_ROOT}" cd "${PROJECT_ROOT}"
VENV_DIR="${HOME}/.venvs/pkgmgr" VENV_DIR="${HOME}/.venvs/pkgmgr"
# shellcheck disable=SC2016
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi' RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
# ------------------------------------------------------------ # ------------------------------------------------------------

View File

@@ -2,17 +2,17 @@
set -euo pipefail set -euo pipefail
echo "============================================================" echo "============================================================"
echo ">>> Running E2E tests: $distro" echo ">>> Running E2E tests: $PKGMGR_DISTRO"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/src" \
-v "pkgmgr_nix_store_${distro}:/nix" \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \ -e TEST_PATTERN="${TEST_PATTERN}" \
--workdir /src \ --workdir /src \
"pkgmgr-${distro}" \ "pkgmgr-${PKGMGR_DISTRO}" \
bash -lc ' bash -lc '
set -euo pipefail set -euo pipefail

View File

@@ -1,17 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
IMAGE="pkgmgr-${distro}" IMAGE="pkgmgr-${PKGMGR_DISTRO}"
echo "============================================================" echo "============================================================"
echo ">>> Running Nix flake-only test in ${distro} container" echo ">>> Running Nix flake-only test in ${PKGMGR_DISTRO} container"
echo ">>> Image: ${IMAGE}" echo ">>> Image: ${IMAGE}"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/src" \
-v "pkgmgr_nix_store_${distro}:/nix" \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
--workdir /src \ --workdir /src \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
"${IMAGE}" \ "${IMAGE}" \
@@ -27,7 +27,7 @@ docker run --rm \
echo ">>> preflight: nix must exist in image" echo ">>> preflight: nix must exist in image"
if ! command -v nix >/dev/null 2>&1; then if ! command -v nix >/dev/null 2>&1; then
echo "NO_NIX" echo "NO_NIX"
echo "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (distro='"${distro}"')" echo "ERROR: nix not found in image '"${IMAGE}"' (PKGMGR_DISTRO='"${PKGMGR_DISTRO}"')"
echo "HINT: Ensure Nix is installed during image build for this distro." echo "HINT: Ensure Nix is installed during image build for this distro."
exit 1 exit 1
fi fi
@@ -35,14 +35,28 @@ docker run --rm \
echo ">>> nix version" echo ">>> nix version"
nix --version nix --version
# ------------------------------------------------------------
# Retry helper for GitHub API rate-limit (HTTP 403)
# ------------------------------------------------------------
if [[ -f /src/scripts/nix/lib/retry_403.sh ]]; then
# shellcheck source=./scripts/nix/lib/retry_403.sh
source /src/scripts/nix/lib/retry_403.sh
elif [[ -f ./scripts/nix/lib/retry_403.sh ]]; then
# shellcheck source=./scripts/nix/lib/retry_403.sh
source ./scripts/nix/lib/retry_403.sh
else
echo "ERROR: retry helper not found: scripts/nix/lib/retry_403.sh"
exit 1
fi
echo ">>> nix flake show" echo ">>> nix flake show"
nix flake show . --no-write-lock-file >/dev/null run_with_github_403_retry nix flake show . --no-write-lock-file >/dev/null
echo ">>> nix build .#default" echo ">>> nix build .#default"
nix build .#default --no-link --no-write-lock-file run_with_github_403_retry nix build .#default --no-link --no-write-lock-file
echo ">>> nix run .#pkgmgr -- --help" echo ">>> nix run .#pkgmgr -- --help"
nix run .#pkgmgr -- --help --no-write-lock-file run_with_github_403_retry nix run .#pkgmgr -- --help --no-write-lock-file
echo ">>> OK: Nix flake-only test succeeded." echo ">>> OK: Nix flake-only test succeeded."
' '

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
IMAGE="pkgmgr-$distro" IMAGE="pkgmgr-$PKGMGR_DISTRO"
echo echo
echo "------------------------------------------------------------" echo "------------------------------------------------------------"
@@ -16,9 +16,9 @@ echo
# Run the command and capture the output # Run the command and capture the output
if OUTPUT=$(docker run --rm \ if OUTPUT=$(docker run --rm \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-v pkgmgr_nix_store_${distro}:/nix \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "$(pwd):/src" \ -v "$(pwd):/src" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
"$IMAGE" 2>&1); then "$IMAGE" 2>&1); then
echo "$OUTPUT" echo "$OUTPUT"
echo echo

View File

@@ -2,17 +2,17 @@
set -euo pipefail set -euo pipefail
echo "============================================================" echo "============================================================"
echo ">>> Running INTEGRATION tests in ${distro} container" echo ">>> Running INTEGRATION tests in ${PKGMGR_DISTRO} container"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/src" \
-v pkgmgr_nix_store_${distro}:/nix \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
--workdir /src \ --workdir /src \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \ -e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${distro}" \ "pkgmgr-${PKGMGR_DISTRO}" \
bash -lc ' bash -lc '
set -e; set -e;
git config --global --add safe.directory /src || true; git config --global --add safe.directory /src || true;

View File

@@ -2,17 +2,17 @@
set -euo pipefail set -euo pipefail
echo "============================================================" echo "============================================================"
echo ">>> Running UNIT tests in ${distro} container" echo ">>> Running UNIT tests in ${PKGMGR_DISTRO} container"
echo "============================================================" echo "============================================================"
docker run --rm \ docker run --rm \
-v "$(pwd):/src" \ -v "$(pwd):/src" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \ -v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
-v pkgmgr_nix_store_${distro}:/nix \ -v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
--workdir /src \ --workdir /src \
-e REINSTALL_PKGMGR=1 \ -e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \ -e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${distro}" \ "pkgmgr-${PKGMGR_DISTRO}" \
bash -lc ' bash -lc '
set -e; set -e;
git config --global --add safe.directory /src || true; git config --global --add safe.directory /src || true;

View File

@@ -19,12 +19,20 @@ fi
# ------------------------------------------------------------ # ------------------------------------------------------------
# Remove auto-activation lines from shell RC files # Remove auto-activation lines from shell RC files
# ------------------------------------------------------------ # ------------------------------------------------------------
RC_PATTERN='\.venvs\/pkgmgr\/bin\/activate"; if \[ -n "\$${PS1:-}" \]; then echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi; fi' # Matches:
# ~/.venvs/pkgmgr/bin/activate
# ./.venvs/pkgmgr/bin/activate
RC_PATTERN='(\./)?\.venvs/pkgmgr/bin/activate'
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..." echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
if [[ -f "$rc" ]]; then if [[ -f "$rc" ]]; then
sed -i "/$RC_PATTERN/d" "$rc" # Remove activation lines (functional)
sed -E -i "/$RC_PATTERN/d" "$rc"
# Remove leftover echo / cosmetic lines referencing pkgmgr venv
sed -i '/\.venvs\/pkgmgr/d' "$rc"
echo "[uninstall] Cleaned $rc" echo "[uninstall] Cleaned $rc"
else else
echo "[uninstall] File not found: $rc (skipped)" echo "[uninstall] File not found: $rc (skipped)"

View File

@@ -1,5 +1,6 @@
import yaml import yaml
import os import os
from pkgmgr.core.config.save import save_user_config
def interactive_add(config,USER_CONFIG_PATH:str): def interactive_add(config,USER_CONFIG_PATH:str):
"""Interactively prompt the user to add a new repository entry to the user config.""" """Interactively prompt the user to add a new repository entry to the user config."""

View File

@@ -45,7 +45,7 @@ def config_init(
# Announce where we will write the result # Announce where we will write the result
# ------------------------------------------------------------ # ------------------------------------------------------------
print("============================================================") print("============================================================")
print(f"[INIT] Writing user configuration to:") print("[INIT] Writing user configuration to:")
print(f" {user_config_path}") print(f" {user_config_path}")
print("============================================================") print("============================================================")
@@ -53,7 +53,7 @@ def config_init(
defaults_config["directories"]["repositories"] defaults_config["directories"]["repositories"]
) )
print(f"[INIT] Scanning repository base directory:") print("[INIT] Scanning repository base directory:")
print(f" {repositories_base_dir}") print(f" {repositories_base_dir}")
print("") print("")
@@ -173,7 +173,7 @@ def config_init(
if new_entries: if new_entries:
user_config.setdefault("repositories", []).extend(new_entries) user_config.setdefault("repositories", []).extend(new_entries)
save_user_config(user_config, user_config_path) save_user_config(user_config, user_config_path)
print(f"[SAVE] Wrote user configuration to:") print("[SAVE] Wrote user configuration to:")
print(f" {user_config_path}") print(f" {user_config_path}")
else: else:
print("[INFO] No new repositories were added.") print("[INFO] No new repositories were added.")

View File

@@ -74,7 +74,7 @@ def _ensure_repo_dir(
if not os.path.exists(repo_dir): if not os.path.exists(repo_dir):
print( print(
f"Repository directory '{repo_dir}' does not exist. " f"Repository directory '{repo_dir}' does not exist. "
f"Cloning it now..." "Cloning it now..."
) )
clone_repos( clone_repos(
[repo], [repo],
@@ -87,7 +87,7 @@ def _ensure_repo_dir(
if not os.path.exists(repo_dir): if not os.path.exists(repo_dir):
print( print(
f"Cloning failed for repository {identifier}. " f"Cloning failed for repository {identifier}. "
f"Skipping installation." "Skipping installation."
) )
return None return None

View File

@@ -90,7 +90,7 @@ class MakefileInstaller(BaseInstaller):
if not ctx.quiet: if not ctx.quiet:
print( print(
f"[pkgmgr] Running 'make install' in {ctx.repo_dir} " f"[pkgmgr] Running 'make install' in {ctx.repo_dir} "
f"(MakefileInstaller)" "(MakefileInstaller)"
) )
cmd = "make install" cmd = "make install"

View File

@@ -160,7 +160,7 @@ class InstallationPipeline:
# so we skip this installer entirely. # so we skip this installer entirely.
if not quiet: if not quiet:
print( print(
f"[pkgmgr] Skipping installer " "[pkgmgr] Skipping installer "
f"{installer.__class__.__name__} for {identifier} " f"{installer.__class__.__name__} for {identifier} "
f"CLI already provided by layer {state.layer.value!r}." f"CLI already provided by layer {state.layer.value!r}."
) )
@@ -171,7 +171,7 @@ class InstallationPipeline:
# need to run another installer on top of it. # need to run another installer on top of it.
if not quiet: if not quiet:
print( print(
f"[pkgmgr] Skipping installer " "[pkgmgr] Skipping installer "
f"{installer.__class__.__name__} for {identifier} " f"{installer.__class__.__name__} for {identifier} "
f"layer {installer_layer.value!r} is already loaded." f"layer {installer_layer.value!r} is already loaded."
) )

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
""" """
High-level mirror actions. High-level mirror actions.
@@ -10,6 +8,7 @@ Public API:
- setup_mirrors - setup_mirrors
""" """
from __future__ import annotations
from .types import Repository, MirrorMap from .types import Repository, MirrorMap
from .list_cmd import list_mirrors from .list_cmd import list_mirrors
from .diff_cmd import diff_mirrors from .diff_cmd import diff_mirrors

View File

@@ -150,7 +150,7 @@ def ensure_origin_remote(
current = current_origin_url(repo_dir) current = current_origin_url(repo_dir)
if current == url or not url: if current == url or not url:
print( print(
f"[INFO] 'origin' already points to " "[INFO] 'origin' already points to "
f"{current or '<unknown>'} (no change needed)." f"{current or '<unknown>'} (no change needed)."
) )
else: else:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import os import os
from urllib.parse import urlparse from urllib.parse import urlparse
from typing import List, Mapping from typing import Mapping
from .types import MirrorMap, Repository from .types import MirrorMap, Repository

View File

@@ -289,7 +289,7 @@ def update_spec_version(
if preview: if preview:
print( print(
f"[PREVIEW] Would update spec file " "[PREVIEW] Would update spec file "
f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..." f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..."
) )
return return

View File

@@ -1,9 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional from typing import Optional
import os import os
import sys import sys
from typing import Optional
from pkgmgr.actions.branch import close_branch from pkgmgr.actions.branch import close_branch
from pkgmgr.core.git import get_current_branch, GitError from pkgmgr.core.git import get_current_branch, GitError

View File

@@ -1,6 +1,5 @@
import os import os
import subprocess import subprocess
import sys
import yaml import yaml
from pkgmgr.core.command.alias import generate_alias from pkgmgr.core.command.alias import generate_alias
from pkgmgr.core.config.save import save_user_config from pkgmgr.core.config.save import save_user_config

View File

@@ -1,15 +1,32 @@
import os import os
import sys
from pkgmgr.core.repository.identifier import get_repo_identifier
from pkgmgr.core.repository.dir import get_repo_dir
def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, preview=False): from pkgmgr.core.command.run import run_command
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.identifier import get_repo_identifier
def deinstall_repos(
selected_repos,
repositories_base_dir,
bin_dir,
all_repos,
preview: bool = False,
) -> None:
for repo in selected_repos: for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos) repo_identifier = get_repo_identifier(repo, all_repos)
alias_path = os.path.join(bin_dir, repo_identifier)
# Resolve repository directory
repo_dir = get_repo_dir(repositories_base_dir, repo)
# Prefer alias if available; fall back to identifier
alias_name = str(repo.get("alias") or repo_identifier)
alias_path = os.path.join(os.path.expanduser(bin_dir), alias_name)
# Remove alias link/file (interactive)
if os.path.exists(alias_path): if os.path.exists(alias_path):
confirm = input(f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: ").strip().lower() confirm = input(
f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: "
).strip().lower()
if confirm == "y": if confirm == "y":
if preview: if preview:
print(f"[Preview] Would remove link '{alias_path}'.") print(f"[Preview] Would remove link '{alias_path}'.")
@@ -19,10 +36,13 @@ def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, p
else: else:
print(f"No link found for {repo_identifier} in {bin_dir}.") print(f"No link found for {repo_identifier} in {bin_dir}.")
# Run make deinstall if repository exists and has a Makefile
makefile_path = os.path.join(repo_dir, "Makefile") makefile_path = os.path.join(repo_dir, "Makefile")
if os.path.exists(makefile_path): if os.path.exists(makefile_path):
print(f"Makefile found in {repo_identifier}, running 'make deinstall'...") print(f"Makefile found in {repo_identifier}, running 'make deinstall'...")
try: try:
run_command("make deinstall", cwd=repo_dir, preview=preview) run_command("make deinstall", cwd=repo_dir, preview=preview)
except SystemExit as e: except SystemExit as e:
print(f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}") print(
f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}"
)

View File

@@ -272,7 +272,7 @@ def list_repositories(
f"{'STATUS'.ljust(status_width)} " f"{'STATUS'.ljust(status_width)} "
f"{'CATEGORIES'.ljust(cat_width)} " f"{'CATEGORIES'.ljust(cat_width)} "
f"{'TAGS'.ljust(tag_width)} " f"{'TAGS'.ljust(tag_width)} "
f"DIR" "DIR"
f"{RESET}" f"{RESET}"
) )
print(header) print(header)

View File

@@ -1,4 +1,3 @@
import sys
import shutil import shutil
from pkgmgr.actions.proxy import exec_proxy_command from pkgmgr.actions.proxy import exec_proxy_command

View File

@@ -1,4 +1,3 @@
import sys
import shutil import shutil
from pkgmgr.actions.repository.pull import pull_with_verification from pkgmgr.actions.repository.pull import pull_with_verification

View File

@@ -1,5 +1,4 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional
import os import os
import sys import sys
@@ -9,7 +8,7 @@ from pkgmgr.cli.context import CLIContext
from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.identifier import get_repo_identifier from pkgmgr.core.repository.identifier import get_repo_identifier
from pkgmgr.core.git import get_tags from pkgmgr.core.git import get_tags
from pkgmgr.core.version.semver import SemVer, extract_semver_from_tags from pkgmgr.core.version.semver import extract_semver_from_tags
from pkgmgr.actions.changelog import generate_changelog from pkgmgr.actions.changelog import generate_changelog

View File

@@ -15,7 +15,6 @@ from pkgmgr.actions.repository.status import status_repos
from pkgmgr.actions.repository.list import list_repositories from pkgmgr.actions.repository.list import list_repositories
from pkgmgr.core.command.run import run_command from pkgmgr.core.command.run import run_command
from pkgmgr.actions.repository.create import create_repo from pkgmgr.actions.repository.create import create_repo
from pkgmgr.core.repository.selected import get_selected_repos
from pkgmgr.core.repository.dir import get_repo_dir from pkgmgr.core.repository.dir import get_repo_dir
Repository = Dict[str, Any] Repository = Dict[str, Any]

View File

@@ -1,5 +1,4 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional
import os import os
import sys import sys

View File

@@ -28,6 +28,7 @@ PROXY_COMMANDS: Dict[str, List[str]] = {
"reset", "reset",
"revert", "revert",
"rebase", "rebase",
"status",
"commit", "commit",
], ],
"docker": [ "docker": [

View File

@@ -1,4 +1,3 @@
from typing import Optional
import os import os
import shutil import shutil
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
@@ -201,8 +200,8 @@ def resolve_command_for_repo(
print( print(
f"[INFO] Repository '{repo_identifier}' appears to be a Python " f"[INFO] Repository '{repo_identifier}' appears to be a Python "
f"package at '{python_package_root}' but no CLI entry point was " f"package at '{python_package_root}' but no CLI entry point was "
f"found (PATH, Nix, main.sh/main.py). Treating it as a " "found (PATH, Nix, main.sh/main.py). Treating it as a "
f"library-only repository with no command." "library-only repository with no command."
) )
return None return None

View File

@@ -1,10 +1,10 @@
from typing import Optional from __future__ import annotations
# pkgmgr/run_command.py
import selectors
import subprocess import subprocess
import sys import sys
from typing import List, Optional, Union from typing import List, Optional, Union
CommandType = Union[str, List[str]] CommandType = Union[str, List[str]]
@@ -15,32 +15,97 @@ def run_command(
allow_failure: bool = False, allow_failure: bool = False,
) -> subprocess.CompletedProcess: ) -> subprocess.CompletedProcess:
""" """
Run a command and optionally exit on error. Run a command with live output while capturing stdout/stderr.
- If `cmd` is a string, it is executed with `shell=True`. - Output is streamed live to the terminal.
- If `cmd` is a list of strings, it is executed without a shell. - Output is captured in memory.
- On failure, captured stdout/stderr are printed again so errors are never lost.
- Command is executed exactly once.
""" """
if isinstance(cmd, str): display = cmd if isinstance(cmd, str) else " ".join(cmd)
display = cmd
else:
display = " ".join(cmd)
where = cwd or "." where = cwd or "."
if preview: if preview:
print(f"[Preview] In '{where}': {display}") print(f"[Preview] In '{where}': {display}")
# Fake a successful result; most callers ignore the return value anyway
return subprocess.CompletedProcess(cmd, 0) # type: ignore[arg-type] return subprocess.CompletedProcess(cmd, 0) # type: ignore[arg-type]
print(f"Running in '{where}': {display}") print(f"Running in '{where}': {display}")
if isinstance(cmd, str): process = subprocess.Popen(
result = subprocess.run(cmd, cwd=cwd, shell=True) cmd,
cwd=cwd,
shell=isinstance(cmd, str),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
)
assert process.stdout is not None
assert process.stderr is not None
sel = selectors.DefaultSelector()
sel.register(process.stdout, selectors.EVENT_READ, data="stdout")
sel.register(process.stderr, selectors.EVENT_READ, data="stderr")
stdout_lines: List[str] = []
stderr_lines: List[str] = []
try:
while sel.get_map():
for key, _ in sel.select():
stream = key.fileobj
which = key.data
line = stream.readline()
if line == "":
# EOF: stop watching this stream
try:
sel.unregister(stream)
except Exception:
pass
continue
if which == "stdout":
stdout_lines.append(line)
print(line, end="")
else: else:
result = subprocess.run(cmd, cwd=cwd) stderr_lines.append(line)
print(line, end="", file=sys.stderr)
finally:
# Ensure we don't leak FDs
try:
sel.close()
finally:
try:
process.stdout.close()
except Exception:
pass
try:
process.stderr.close()
except Exception:
pass
if result.returncode != 0 and not allow_failure: returncode = process.wait()
print(f"Command failed with exit code {result.returncode}. Exiting.")
sys.exit(result.returncode)
return result if returncode != 0 and not allow_failure:
print("\n[pkgmgr] Command failed, captured diagnostics:", file=sys.stderr)
print(f"[pkgmgr] Failed command: {display}", file=sys.stderr)
if stdout_lines:
print("----- stdout -----")
print("".join(stdout_lines), end="")
if stderr_lines:
print("----- stderr -----", file=sys.stderr)
print("".join(stderr_lines), end="", file=sys.stderr)
print(f"Command failed with exit code {returncode}. Exiting.")
sys.exit(returncode)
return subprocess.CompletedProcess(
cmd,
returncode,
stdout="".join(stdout_lines),
stderr="".join(stderr_lines),
)

View File

@@ -1,15 +1,48 @@
import sys
import os import os
import sys
from typing import Any, Dict
def get_repo_dir(repositories_base_dir:str,repo:{})->str:
try: def get_repo_dir(repositories_base_dir: str, repo: Dict[str, Any]) -> str:
return os.path.join(repositories_base_dir, repo.get("provider"), repo.get("account"), repo.get("repository")) """
except TypeError as e: Build the local repository directory path from:
if repositories_base_dir: repositories_base_dir/provider/account/repository
print(f"Error: {e} \nThe repository {repo} seems not correct configured.\nPlease configure it correct.")
for key in ["provider","account","repository"]: Exits with code 3 and prints diagnostics if the input config is invalid.
if not repo.get(key,False): """
print(f"Key '{key}' is missing.") # Base dir must be set and non-empty
else: if not repositories_base_dir:
print(f"Error: {e} \nThe base {base} seems not correct configured.\nPlease configure it correct.") print(
"Error: repositories_base_dir is missing.\n"
"The base directory for repositories seems not correctly configured.\n"
"Please configure it correctly."
)
sys.exit(3) sys.exit(3)
# Repo must be a dict-like object
if not isinstance(repo, dict):
print(
f"Error: invalid repo object '{repo}'.\n"
"The repository entry seems not correctly configured.\n"
"Please configure it correctly."
)
sys.exit(3)
base_dir = os.path.expanduser(str(repositories_base_dir))
provider = repo.get("provider")
account = repo.get("account")
repository = repo.get("repository")
missing = [k for k, v in [("provider", provider), ("account", account), ("repository", repository)] if not v]
if missing:
print(
"Error: repository entry is missing required keys.\n"
f"Repository: {repo}\n"
"Please configure it correctly."
)
for k in missing:
print(f"Key '{k}' is missing.")
sys.exit(3)
return os.path.join(base_dir, str(provider), str(account), str(repository))

View File

@@ -1,4 +1,3 @@
import os
def resolve_repos(identifiers:[], all_repos:[]): def resolve_repos(identifiers:[], all_repos:[]):
""" """

View File

@@ -12,7 +12,6 @@ which we treat as success and suppress in the helper.
from __future__ import annotations from __future__ import annotations
import os
import runpy import runpy
import sys import sys
import unittest import unittest

View File

@@ -1,5 +1,5 @@
import unittest import unittest
from unittest.mock import patch, MagicMock from unittest.mock import patch
from pkgmgr.actions.branch.utils import _resolve_base_branch from pkgmgr.actions.branch.utils import _resolve_base_branch
from pkgmgr.core.git import GitError from pkgmgr.core.git import GitError

View File

@@ -1,5 +1,5 @@
import unittest import unittest
from unittest.mock import patch, MagicMock from unittest.mock import patch
from pkgmgr.actions.branch.close_branch import close_branch from pkgmgr.actions.branch.close_branch import close_branch
from pkgmgr.core.git import GitError from pkgmgr.core.git import GitError

View File

@@ -1,5 +1,5 @@
import unittest import unittest
from unittest.mock import patch, MagicMock from unittest.mock import patch
from pkgmgr.actions.branch.drop_branch import drop_branch from pkgmgr.actions.branch.drop_branch import drop_branch
from pkgmgr.core.git import GitError from pkgmgr.core.git import GitError

View File

@@ -1,5 +1,5 @@
import unittest import unittest
from unittest.mock import patch, MagicMock from unittest.mock import patch
from pkgmgr.actions.branch.open_branch import open_branch from pkgmgr.actions.branch.open_branch import open_branch

View File

@@ -1,6 +1,5 @@
# tests/unit/pkgmgr/test_capabilities.py # tests/unit/pkgmgr/test_capabilities.py
import os
import unittest import unittest
from unittest.mock import patch, mock_open from unittest.mock import patch, mock_open

View File

@@ -0,0 +1,79 @@
import unittest
from unittest.mock import patch
from pkgmgr.actions.repository.deinstall import deinstall_repos
class TestDeinstallRepos(unittest.TestCase):
def test_preview_removes_nothing_but_runs_make_if_makefile_exists(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo", "alias": "demo"}
selected = [repo]
with patch("pkgmgr.actions.repository.deinstall.get_repo_identifier", return_value="demo"), \
patch("pkgmgr.actions.repository.deinstall.get_repo_dir", return_value="/repos/github.com/alice/demo"), \
patch("pkgmgr.actions.repository.deinstall.os.path.expanduser", return_value="/home/u/.local/bin"), \
patch("pkgmgr.actions.repository.deinstall.os.path.exists") as mock_exists, \
patch("pkgmgr.actions.repository.deinstall.os.remove") as mock_remove, \
patch("pkgmgr.actions.repository.deinstall.run_command") as mock_run, \
patch("builtins.input", return_value="y"):
# alias exists, Makefile exists
def exists_side_effect(path):
if path == "/home/u/.local/bin/demo":
return True
if path == "/repos/github.com/alice/demo/Makefile":
return True
return False
mock_exists.side_effect = exists_side_effect
deinstall_repos(
selected_repos=selected,
repositories_base_dir="/repos",
bin_dir="~/.local/bin",
all_repos=selected,
preview=True,
)
# Preview: do not remove
mock_remove.assert_not_called()
# But still "would run" make deinstall via run_command (preview=True)
mock_run.assert_called_once_with(
"make deinstall",
cwd="/repos/github.com/alice/demo",
preview=True,
)
def test_non_preview_removes_alias_when_confirmed(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo", "alias": "demo"}
selected = [repo]
with patch("pkgmgr.actions.repository.deinstall.get_repo_identifier", return_value="demo"), \
patch("pkgmgr.actions.repository.deinstall.get_repo_dir", return_value="/repos/github.com/alice/demo"), \
patch("pkgmgr.actions.repository.deinstall.os.path.expanduser", return_value="/home/u/.local/bin"), \
patch("pkgmgr.actions.repository.deinstall.os.path.exists") as mock_exists, \
patch("pkgmgr.actions.repository.deinstall.os.remove") as mock_remove, \
patch("pkgmgr.actions.repository.deinstall.run_command") as mock_run, \
patch("builtins.input", return_value="y"):
# alias exists, Makefile does NOT exist
def exists_side_effect(path):
if path == "/home/u/.local/bin/demo":
return True
if path == "/repos/github.com/alice/demo/Makefile":
return False
return False
mock_exists.side_effect = exists_side_effect
deinstall_repos(
selected_repos=selected,
repositories_base_dir="/repos",
bin_dir="~/.local/bin",
all_repos=selected,
preview=False,
)
mock_remove.assert_called_once_with("/home/u/.local/bin/demo")
mock_run.assert_not_called()

View File

@@ -24,12 +24,11 @@ Goals:
from __future__ import annotations from __future__ import annotations
import io import io
import sys
import unittest import unittest
from contextlib import redirect_stdout from contextlib import redirect_stdout
from types import SimpleNamespace from types import SimpleNamespace
from typing import Any, Dict, List from typing import Any, Dict, List
from unittest.mock import MagicMock, patch from unittest.mock import patch
from pkgmgr.cli.context import CLIContext from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.commands.repos import handle_repos_command from pkgmgr.cli.commands.repos import handle_repos_command

View File

@@ -0,0 +1,47 @@
import unittest
from unittest.mock import patch
import pkgmgr.core.command.run as run_mod
class TestRunCommand(unittest.TestCase):
def test_preview_returns_success_without_running(self) -> None:
with patch.object(run_mod.subprocess, "Popen") as popen_mock:
result = run_mod.run_command("echo hi", cwd="/tmp", preview=True)
self.assertEqual(result.returncode, 0)
popen_mock.assert_not_called()
def test_success_streams_and_returns_completed_process(self) -> None:
cmd = ["python3", "-c", "print('out'); import sys; print('err', file=sys.stderr)"]
with patch.object(run_mod.sys, "exit") as exit_mock:
result = run_mod.run_command(cmd, allow_failure=False)
self.assertEqual(result.returncode, 0)
self.assertIn("out", result.stdout)
self.assertIn("err", result.stderr)
exit_mock.assert_not_called()
def test_failure_exits_when_not_allowed(self) -> None:
cmd = ["python3", "-c", "import sys; print('oops', file=sys.stderr); sys.exit(2)"]
with patch.object(run_mod.sys, "exit", side_effect=SystemExit(2)) as exit_mock:
with self.assertRaises(SystemExit) as ctx:
run_mod.run_command(cmd, allow_failure=False)
self.assertEqual(ctx.exception.code, 2)
exit_mock.assert_called_once_with(2)
def test_failure_does_not_exit_when_allowed(self) -> None:
cmd = ["python3", "-c", "import sys; print('oops', file=sys.stderr); sys.exit(3)"]
with patch.object(run_mod.sys, "exit") as exit_mock:
result = run_mod.run_command(cmd, allow_failure=True)
self.assertEqual(result.returncode, 3)
self.assertIn("oops", result.stderr)
exit_mock.assert_not_called()
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,26 @@
import unittest
from unittest.mock import patch
from pkgmgr.core.repository.dir import get_repo_dir
class TestGetRepoDir(unittest.TestCase):
def test_builds_path_with_expanded_base_dir(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo"}
with patch("pkgmgr.core.repository.dir.os.path.expanduser", return_value="/home/u/repos"):
result = get_repo_dir("~/repos", repo)
self.assertEqual(result, "/home/u/repos/github.com/alice/demo")
def test_exits_with_code_3_if_base_dir_is_none(self):
repo = {"provider": "github.com", "account": "alice", "repository": "demo"}
with self.assertRaises(SystemExit) as ctx:
get_repo_dir(None, repo) # type: ignore[arg-type]
self.assertEqual(ctx.exception.code, 3)
def test_exits_with_code_3_if_repo_is_invalid_type(self):
with self.assertRaises(SystemExit) as ctx:
get_repo_dir("/repos", None) # type: ignore[arg-type]
self.assertEqual(ctx.exception.code, 3)

View File

@@ -1,166 +0,0 @@
# tests/unit/pkgmgr/test_resolve_command.py
import unittest
from unittest.mock import patch
import pkgmgr.core.command.resolve as resolve_command_module
class TestResolveCommandForRepo(unittest.TestCase):
def test_explicit_command_wins(self):
repo = {"command": "/custom/cmd"}
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, "/custom/cmd")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value="/usr/bin/tool")
def test_system_binary_returns_none_and_no_error(self, mock_which):
repo = {}
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
# System binary → no link
self.assertIsNone(result)
@patch("pkgmgr.core.command.resolve.os.access")
@patch("pkgmgr.core.command.resolve.os.path.exists")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
def test_nix_profile_binary(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
No system/PATH binary, but a Nix profile binary exists:
→ must return the Nix binary path.
"""
repo = {}
fake_home = "/fakehome"
nix_path = f"{fake_home}/.nix-profile/bin/tool"
def fake_exists(path):
# Only the Nix binary exists
return path == nix_path
def fake_access(path, mode):
# Only the Nix binary is executable
return path == nix_path
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, nix_path)
@patch("pkgmgr.core.command.resolve.os.access")
@patch("pkgmgr.core.command.resolve.os.path.exists")
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/home/user")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value="/home/user/.local/bin/tool")
def test_non_system_binary_on_path(
self,
mock_which,
mock_expanduser,
mock_exists,
mock_access,
):
"""
No system (/usr) binary and no Nix binary, but a non-system
PATH binary exists (e.g. venv or ~/.local/bin):
→ must return that PATH binary.
"""
repo = {}
non_system_path = "/home/user/.local/bin/tool"
nix_candidate = "/home/user/.nix-profile/bin/tool"
def fake_exists(path):
# Only the non-system PATH binary "exists".
return path == non_system_path
def fake_access(path, mode):
# Only the non-system PATH binary is executable.
return path == non_system_path
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, non_system_path)
@patch("pkgmgr.core.command.resolve.os.access")
@patch("pkgmgr.core.command.resolve.os.path.exists")
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
def test_fallback_to_main_py(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
No system/non-system PATH binary, no Nix binary, but main.py exists:
→ must fall back to main.py in the repo.
"""
repo = {}
main_py = "/repos/tool/main.py"
def fake_exists(path):
return path == main_py
def fake_access(path, mode):
return path == main_py
mock_exists.side_effect = fake_exists
mock_access.side_effect = fake_access
result = resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
self.assertEqual(result, main_py)
@patch("pkgmgr.core.command.resolve.os.access", return_value=False)
@patch("pkgmgr.core.command.resolve.os.path.exists", return_value=False)
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
def test_no_command_results_in_system_exit(
self,
mock_expanduser,
mock_which,
mock_exists,
mock_access,
):
"""
Nothing available at any layer:
→ must raise SystemExit with a descriptive error message.
"""
repo = {}
with self.assertRaises(SystemExit) as cm:
resolve_command_module.resolve_command_for_repo(
repo=repo,
repo_identifier="tool",
repo_dir="/repos/tool",
)
msg = str(cm.exception)
self.assertIn("No executable command could be resolved for repository 'tool'", msg)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,77 @@
import unittest
from unittest.mock import patch
from pkgmgr.core.repository.resolve import resolve_repos
class TestResolveRepos(unittest.TestCase):
def setUp(self) -> None:
# Two repos share the same repository name "common" to test uniqueness logic
self.repos = [
{
"provider": "github.com",
"account": "alice",
"repository": "demo",
"alias": "d",
},
{
"provider": "github.com",
"account": "bob",
"repository": "common",
"alias": "c1",
},
{
"provider": "gitlab.com",
"account": "carol",
"repository": "common",
"alias": "c2",
},
]
def test_matches_full_identifier(self):
result = resolve_repos(["github.com/alice/demo"], self.repos)
self.assertEqual(result, [self.repos[0]])
def test_matches_alias(self):
result = resolve_repos(["d"], self.repos)
self.assertEqual(result, [self.repos[0]])
def test_matches_unique_repository_name_only_if_unique(self):
# "demo" is unique -> match
result = resolve_repos(["demo"], self.repos)
self.assertEqual(result, [self.repos[0]])
# "common" is NOT unique -> should not match anything
result2 = resolve_repos(["common"], self.repos)
self.assertEqual(result2, [])
def test_multiple_identifiers_accumulate_matches_in_order(self):
result = resolve_repos(["d", "github.com/bob/common"], self.repos)
self.assertEqual(result, [self.repos[0], self.repos[1]])
def test_unknown_identifier_prints_message(self):
with patch("builtins.print") as mock_print:
result = resolve_repos(["does-not-exist"], self.repos)
self.assertEqual(result, [])
mock_print.assert_called_with(
"Identifier 'does-not-exist' did not match any repository in config."
)
def test_duplicate_identifiers_return_duplicates(self):
# Current behavior: duplicates are not de-duplicated
result = resolve_repos(["d", "d"], self.repos)
self.assertEqual(result, [self.repos[0], self.repos[0]])
def test_empty_identifiers_returns_empty_list(self):
result = resolve_repos([], self.repos)
self.assertEqual(result, [])
def test_empty_repo_list_returns_empty_list_and_prints(self):
with patch("builtins.print") as mock_print:
result = resolve_repos(["github.com/alice/demo"], [])
self.assertEqual(result, [])
mock_print.assert_called_with(
"Identifier 'github.com/alice/demo' did not match any repository in config."
)

View File

@@ -79,7 +79,6 @@ class TestCreateInk(unittest.TestCase):
patch("pkgmgr.core.command.ink.os.chmod") as mock_chmod, \ patch("pkgmgr.core.command.ink.os.chmod") as mock_chmod, \
patch("pkgmgr.core.command.ink.os.path.exists", return_value=False), \ patch("pkgmgr.core.command.ink.os.path.exists", return_value=False), \
patch("pkgmgr.core.command.ink.os.path.islink", return_value=False), \ patch("pkgmgr.core.command.ink.os.path.islink", return_value=False), \
patch("pkgmgr.core.command.ink.os.remove") as mock_remove, \
patch("pkgmgr.core.command.ink.os.path.realpath", side_effect=lambda p: p): patch("pkgmgr.core.command.ink.os.path.realpath", side_effect=lambda p: p):
create_ink_module.create_ink( create_ink_module.create_ink(
repo=repo, repo=repo,