Compare commits

...

134 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
3642f92776 Release version 1.3.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 / mark-stable (push) Has been cancelled
2025-12-12 20:35:02 +01:00
Kevin Veen-Birkenbach
8f38edde67 **Fix Nix global symlinks for sudo secure_path without overriding distro paths**
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
* Ensure nix is reachable for sudo on CentOS by providing /usr/bin and /usr/sbin fallbacks when absent
* Keep /usr/local/bin as primary CI path without breaking non-login shells
* Never overwrite distro-managed nix binaries (Arch-safe)
* Stabilize e2e and virgin-user tests across all distros

https://chatgpt.com/share/693c6013-af2c-800f-a1bc-baed0d29fab7
2025-12-12 20:23:29 +01:00
Kevin Veen-Birkenbach
5875441b23 **Fix Nix resolution and symlink handling on Arch without overriding system paths**
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
* Resolve the real *nix* executable to avoid self-referential symlink loops
* Prefer distro-managed paths (*/usr/sbin*, */usr/bin*) over */usr/local*
* Restrict global symlink creation to */usr/local/bin/nix* only
* Never overwrite Arch-managed */usr/bin/nix* or */bin/nix*
* Make CI and non-login shells reliable while preserving native Arch behavior

https://chatgpt.com/share/693c6013-af2c-800f-a1bc-baed0d29fab7
2025-12-12 20:05:17 +01:00
Kevin Veen-Birkenbach
9190f0d901 Fix init-nix so it works for non-root CI shells across distros
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
https://chatgpt.com/share/693c6013-af2c-800f-a1bc-baed0d29fab7
2025-12-12 19:50:25 +01:00
Kevin Veen-Birkenbach
f227734185 **Fix init-nix for CI and Arch shells**
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
* Simplify *init-nix.sh* while keeping container/host install paths
* Prefer canonical *nix* locations and avoid brittle PATH assumptions
* Ensure global *nix* symlinks for non-login shells (CI reliability)
* Keep retry download + nixbld bootstrap logic intact

https://chatgpt.com/share/693c6013-af2c-800f-a1bc-baed0d29fab7
2025-12-12 19:40:21 +01:00
Kevin Veen-Birkenbach
c7ef77559c Ensure nix is reachable in CI shells via robust lookup and global symlinks
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
Add resolve_nix_bin to reliably locate the nix binary in non-login shells.
Create and enforce global nix symlinks for CI environments (/usr/local/bin, best-effort /usr/bin and /bin).
Apply symlink enforcement on fast path, after PATH adjustments, and post-install when running as root.
Improve warnings when nix is installed but not on PATH.

https://chatgpt.com/share/693c6013-af2c-800f-a1bc-baed0d29fab7
2025-12-12 19:33:52 +01:00
Kevin Veen-Birkenbach
2385601ed5 Persist CA bundle configuration on CentOS for Nix and HTTPS tools
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
Move CA bundle detection from the Docker entrypoint to CentOS dependencies and persist it system-wide.
This ensures Nix, Git, curl, and Python HTTPS access works in virgin environments by configuring `/etc/profile.d` and `/etc/nix/nix.conf`.
Removes runtime-only CA exports from the container entrypoint and makes the setup reproducible and distro-correct.

https://chatgpt.com/share/693c5ddf-3260-800f-ac94-38c635dba307
2025-12-12 19:24:12 +01:00
Kevin Veen-Birkenbach
ac5ae95369 fix(py39): replace PEP 604 union types with Optional for Python 3.9 compatibility
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
- Replaced all `X | None` type hints with `Optional[X]`
- Adjusted typing imports across modules
- Fixed import order and removed invalid future-import placements
- Ensured code runs correctly on Python 3.9

https://chatgpt.com/share/693c58e1-ce70-800f-9088-5864571e024a
2025-12-12 19:02:54 +01:00
Kevin Veen-Birkenbach
31f7f47fe2 Downgraded python to 3.9 for CentOS
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-12 18:38:37 +01:00
Kevin Veen-Birkenbach
c8bf1c91ad **test(e2e): split update-all HTTPS integration test into pkgmgr and nix runs**
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
Refactored the E2E update-all test to execute real CLI commands instead of invoking *main.py*.
The test is now split into two independent cases: one running *pkgmgr update* directly and one running the same command via *nix run .#pkgmgr*.
This improves realism, diagnostics, and parity with actual user workflows inside the container.

https://chatgpt.com/share/693c52cb-cc10-800f-994b-5b2940dcf948
2025-12-12 18:37:07 +01:00
Kevin Veen-Birkenbach
f2caa68e3d fix(nix): ensure non-root access to Nix installation with strict error 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 / mark-stable (push) Has been cancelled
Ensure /home/nix and .nix-profile are accessible for non-root users,
create /usr/local/bin/nix symlink with fail-fast behavior, and replace
silent permission fixes with explicit checks, clear error messages,
and deterministic exit codes.

https://chatgpt.com/share/693c29d9-9b28-800f-a549-5661c783d968
2025-12-12 18:19:51 +01:00
Kevin Veen-Birkenbach
03c232c308 Performance optimation for workflows
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-12 18:07:25 +01:00
Kevin Veen-Birkenbach
e882e17737 Changed CentOS to python 3.11
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-12 17:26:39 +01:00
Kevin Veen-Birkenbach
b9edcf7101 Patched python version for centos
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-12 17:12:30 +01:00
Kevin Veen-Birkenbach
8b8ebf329f Added venv to debian and ubuntu virgin
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-12 17:04:34 +01:00
Kevin Veen-Birkenbach
9598c17ea0 Added python dependency to virgin container
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-12 16:58:12 +01:00
Kevin Veen-Birkenbach
67bd358e12 fix(docker): enforce bash shell to support pipefail across distros
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
Use bash as the default shell in Docker build stages to ensure
`set -euo pipefail` works reliably on all base images, including
Ubuntu where /bin/sh does not support pipefail.

https://chatgpt.com/share/693c29d9-9b28-800f-a549-5661c783d968
2025-12-12 16:50:32 +01:00
Kevin Veen-Birkenbach
340c1700dc Added missing 'make' to ubuntu
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-12 16:42:45 +01:00
Kevin Veen-Birkenbach
0dfbaa0f6b ci/docker: unify image build logic and run virgin tests across all distros
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
Refactor Dockerfile into multi-stage virgin/full targets and introduce a single
flag-based image build script. Standardize image naming, remove redundant build
scripts, and update Makefile targets accordingly. CI workflows now build missing
virgin images and run root and user tests consistently across all supported
distributions.

https://chatgpt.com/share/693c29d9-9b28-800f-a549-5661c783d968
2025-12-12 16:40:21 +01:00
Kevin Veen-Birkenbach
08ab9fb142 feat(ci): stabilize virgin Arch tests with Makefile install/setup and Nix Git safety
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
* Switch virgin root/user workflows to use *make install* + *make setup/setup-venv*
* Add Git *safe.directory /src* to avoid flake evaluation failures on mounted repos
* Enable Nix flake run in workflows and prepare */nix* for non-root execution
* Refactor Arch packaging to build in an isolated */tmp* directory via *aur_builder*
* Rename installer scripts (*run-** → *dependencies.sh* / *package.sh*) and adjust Docker entry + env var to *REINSTALL_PKGMGR*

https://chatgpt.com/share/693c29d9-9b28-800f-a549-5661c783d968
2025-12-12 15:42:25 +01:00
Kevin Veen-Birkenbach
804245325d Release version 1.2.1
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-12 12:32:33 +01:00
Kevin Veen-Birkenbach
c05e77658a ci(docker): remove build-time nix check and rely on runtime env test
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
Why:
The Dockerfile previously validated `nix --version` during image build,
which is environment-sensitive and behaves differently in GitHub Actions
vs local/act builds due to PATH and non-login shell differences.

The actual contract is runtime availability of Nix, not build-step PATH
resolution. This is now reliably enforced by the dedicated `test-env-nix`
container test, which validates nix presence and flake execution in the
real execution environment.

This removes flaky CI behavior while keeping stronger, more accurate
coverage of the intended guarantee.

https://chatgpt.com/share/693bfbc7-63d8-800f-9ceb-728c7a58e963
2025-12-12 12:25:36 +01:00
Kevin Veen-Birkenbach
324f6db1f3 ci: split container tests into virtualenv and Nix flake environments
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
Refactor CI to clearly separate virtualenv-based container tests from pure Nix flake tests across all distros (arch, debian, ubuntu, fedora, centos).
Introduce dedicated test-env-nix workflow and Makefile targets, rename former container tests to test-env-virtual, and update stable pipeline dependencies.
Improve Nix reliability in containers by fixing installer permissions and explicitly validating nix availability and version during image build and tests.
2025-12-12 12:15:40 +01:00
Kevin Veen-Birkenbach
2a69a83d71 Release version 1.2.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-container (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-12 10:27:56 +01:00
Kevin Veen-Birkenbach
0ec4ccbe40 **fix(release): force-fetch remote tags and align tests**
* Treat remote tags as the source of truth by force-fetching tags from *origin*
* Update preview output to reflect the real fetch behavior
* Align unit tests with the new forced tag fetch command

https://chatgpt.com/share/693bdfc3-b8b4-800f-8adc-b1dc63c56a89
2025-12-12 10:26:22 +01:00
Kevin Veen-Birkenbach
0d864867cd **feat(release): adjust highest-tag detection tests and improve logging**
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-container (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
* Add debug output for latest vs current version tag in release git ops
* Treat “no version tags yet” as highest by definition
* Align unit tests with current *string-based* `tag >= latest` behavior
* Make tag listing mocks less brittle by matching command patterns
* Rename release init test to `test_init.py` for consistent discovery
2025-12-12 10:17:18 +01:00
Kevin Veen-Birkenbach
3ff0afe828 feat(release): refactor release workflow, tagging logic, and CLI integration
Refactor the release implementation into a dedicated workflow module with clear separation of concerns. Enforce a safe, deterministic Git flow by always syncing with the remote before modifications, pushing only the current branch and the newly created version tag, and updating the floating *latest* tag only when the released version is the highest. Add explicit user prompts for confirmation and optional branch deletion, with a forced mode to skip interaction. Update CLI wiring to pass all relevant flags, add comprehensive unit tests for the new helpers and workflow entry points, and introduce detailed documentation describing the release process, safety rules, and execution flow.
2025-12-12 10:04:24 +01:00
Kevin Veen-Birkenbach
bd74ad41f9 Release version 1.1.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-container (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-12 09:08:22 +01:00
Kevin Veen-Birkenbach
fa2a92481d Merge branch 'main' of github.com:kevinveenbirkenbach/package-manager 2025-12-12 09:08:19 +01:00
Kevin Veen-Birkenbach
6a1e001fc2 test(branch): remove obsolete test_branch.py after branch module refactor
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-container (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
The old test tests/unit/pkgmgr/actions/test_branch.py has been removed because:

- it targeted the previous monolithic pkgmgr.actions.branch module structure
- its patch targets no longer match the refactored code
- its responsibilities are now fully covered by the new, dedicated unit,
  integration, and E2E tests for branch actions and CLI wiring

This avoids redundant coverage and prevents misleading or broken tests
after the branch refactor.

https://chatgpt.com/share/693bcc8d-b84c-800f-8510-8d6c66faf627
2025-12-12 09:04:11 +01:00
Kevin Veen-Birkenbach
60afa92e09 Removed flake.lock 2025-12-12 00:30:17 +01:00
Kevin Veen-Birkenbach
212f3ce5eb Removed _requirements.txt 2025-12-12 00:27:46 +01:00
Kevin Veen-Birkenbach
0d79537033 Added Banner
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-container (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-11 21:01:27 +01:00
Kevin Veen-Birkenbach
72fc69c2f8 Release version 1.0.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-container (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-11 20:41:35 +01:00
Kevin Veen-Birkenbach
6d8c6deae8 **refactor(readme): rewrite README for multi-distro focus and Nix-based workflows**
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-container (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
Expanded and modernized the README to reflect PKGMGR's purpose as a
multi-distro development and packaging orchestrator. Added explanations for
Nix-based cross-distro workflows, clarified installation steps, documented the
full CLI capabilities, and embedded the architecture diagram.

Also replaced the verbose CLI DESCRIPTION_TEXT with a concise summary suitable
for `--help` output.

Included updated `assets/map.png`.

https://chatgpt.com/share/693b1d71-ca08-800f-a000-f3be49f7efb5
2025-12-11 20:37:05 +01:00
Kevin Veen-Birkenbach
6c116a029e Release version 0.10.2
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-container (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-11 20:16:59 +01:00
Kevin Veen-Birkenbach
3eb7c81fa1 **Mark stable only on highest version tag**
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-container (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
Updated the `mark-stable` workflow so that the `stable` tag is only moved when:

* the current push is a version tag (`v*`)
* all tests have passed
* the pushed version tag is the highest semantic version among all existing tags

This ensures that `stable` always reflects the latest valid release and prevents older version tags from overwriting it.

https://chatgpt.com/share/693b163b-0c34-800f-adcb-12cf4744dbe2
2025-12-11 20:06:22 +01:00
Kevin Veen-Birkenbach
0334f477fd Release version 0.10.2
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-container (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-11 20:01:29 +01:00
Kevin Veen-Birkenbach
8bb99c99b7 refactor(init-nix): unify installer logic and add robust retry 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-container (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
Refactored the Nix initialization script to reduce duplicated code and
centralize the installation workflow. The core functionality remains
unchanged, but all installer calls now use a unified function with retry
support to ensure resilient downloads in CI and container environments.

Key improvements:
- Added download retry logic (5 minutes total, 20-second intervals)
- Consolidated installer invocation into `install_nix_with_retry`
- Reduced code duplication across container/host install paths
- Preserved existing installation behavior for all environments
- Maintained `nixbld` group and build-user handling
- Improved consistency and readability without altering semantics

This prevents intermittent failures such as:
“curl: (6) Could not resolve host: nixos.org”
and ensures stable, deterministic Nix setup in CI pipelines.

https://chatgpt.com/share/693b13ce-fdcc-800f-a7bc-81c67478edff
2025-12-11 19:56:10 +01:00
Kevin Veen-Birkenbach
587cb2e516 Removed comments
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-container (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-11 19:44:36 +01:00
Kevin Veen-Birkenbach
fcf9d4b59b **Aur builder: add retry logic for yay clone to recover from GitHub 504 errors**
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-container (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
Implemented a robust retry mechanism for cloning the yay AUR helper during Arch dependency installation.
The new logic retries the git clone operation for up to 5 minutes with a 20-second pause between attempts, allowing the build to proceed even when GitHub intermittently returns HTTP 504 errors.

This improves the stability of Arch container builds, especially under network pressure or transient upstream outages.
The yay build process now only starts once the clone step completes successfully.

https://chatgpt.com/share/693b102b-fdb0-800f-9f2e-d4840f14d329
2025-12-11 19:40:25 +01:00
Kevin Veen-Birkenbach
b483dbfaad **fix(init-nix): ensure nixbld group/users exist on Ubuntu root-without-systemd installs**
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-container (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
Implement `ensure_nix_build_group()` and use it in all code paths where Nix is installed as root.
This resolves Nix installation failures on Ubuntu containers (root, no systemd) where the installer aborts with:

```
error: the group 'nixbld' specified in 'build-users-group' does not exist
```

The fix standardizes creation of the `nixbld` group and `nixbld1..10` build users across:

* container root mode
* systemd host daemon installs
* root-on-host without systemd (Debian/Ubuntu CI case)

This makes Nix initialization deterministic across all test distros and fixes failing Ubuntu E2E runs.

https://chatgpt.com/share/693b0e1a-e5d4-800f-8a89-7d91108b0368
2025-12-11 19:31:25 +01:00
Kevin Veen-Birkenbach
9630917570 **refactor(nix-flake): replace run_command wrapper with direct os.system execution and extend test coverage**
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-container (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
This commit removes the `run_command`-based execution model for Nix flake
installations and replaces it with a direct `os.system` invocation.
This ensures that *all* Nix diagnostics (stdout/stderr) are fully visible and
no longer suppressed by wrapper logic.

Key changes:

* Directly run `nix profile install` via `os.system` for full error output
* Correctly decode real exit codes via `os.WIFEXITED` / `os.WEXITSTATUS`
* Preserve mandatory/optional behavior for flake outputs
* Update unit tests to the new execution model using `unittest`
* Add complete coverage for:

  * successful installs
  * mandatory failures → raise SystemExit(code)
  * optional failures → warn and continue
  * environment-based disabling via `PKGMGR_DISABLE_NIX_FLAKE_INSTALLER`
* Remove obsolete mocks and legacy test logic that assumed `run_command`

Overall, this improves transparency, debuggability, and correctness of the
Nix flake installer while maintaining full backward compatibility at the
interface level.

https://chatgpt.com/share/693b0a20-99f4-800f-b789-b00a50413612
2025-12-11 19:14:25 +01:00
Kevin Veen-Birkenbach
6a4432dd04 Added required sudo to debian
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-container (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-11 18:42:33 +01:00
Kevin Veen-Birkenbach
cfb91d825a Release version 0.10.1
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-container (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-11 18:38:15 +01:00
Kevin Veen-Birkenbach
a3b21f23fc pkgmgr-wrapper: improve Nix detection and auto-initialization
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-container (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
- Extend PATH probing to include /home/nix/.nix-profile/bin/nix (container mode).
- Automatically invoke init-nix.sh when nix is not found before first run.
- Ensure pkgmgr always attempts a one-time Nix initialization instead of failing prematurely.
- Improve error message to clarify that nix was still missing *after* initialization attempt.
- Keep existing flake-based execution path unchanged (exec nix run …).

This makes the wrapper fully reliable across Debian/Ubuntu package installs,
fresh containers, and minimal systems where Nix is not yet initialized.

https://chatgpt.com/share/693b005d-b250-800f-8830-ab71685f51b3
2025-12-11 18:33:02 +01:00
Kevin Veen-Birkenbach
e49dd85200 Release version 0.10.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-container (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-11 18:17:21 +01:00
Kevin Veen-Birkenbach
c9dec5ecd6 Merge branch 'feature/mirror'
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-container (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-11 17:50:53 +01:00
Kevin Veen-Birkenbach
f3c5460e48 feat(mirror): support SSH MIRRORS, multi-push origin and remote probe
Some checks failed
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-container (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
- Switch MIRRORS to SSH-based URLs including custom ports/domains
  (GitHub, git.veen.world, code.cymais.cloud)
- Extend mirror IO:
  - load_config_mirrors filters empty values
  - read_mirrors_file now supports:
    * "name url" lines
    * "url" lines with auto-generated names from URL host (host[:port])
  - write_mirrors_file prints full preview content
- Enhance git_remote:
  - determine_primary_remote_url used for origin bootstrap
  - ensure_origin_remote keeps existing origin URL and
    adds all mirror URLs as additional push URLs
  - add is_remote_reachable() helper based on `git ls-remote --exit-code`
- Implement non-destructive remote mirror checks in setup_cmd:
  - `_probe_mirror()` wraps `git ls-remote` and returns (ok, message)
  - `pkgmgr mirror setup --remote` now probes each mirror URL and
    prints [OK]/[WARN] with details instead of placeholder text
- Add unit tests for mirror actions:
  - test_git_remote: default SSH URL building and primary URL selection
  - test_io: config + MIRRORS parsing including auto-named URL-only entries
  - test_setup_cmd: probe_mirror success/failure handling

https://chatgpt.com/share/693adee0-aa3c-800f-b72a-98473fdaf760
2025-12-11 17:49:31 +01:00
Kevin Veen-Birkenbach
39b16b87a8 CI: Add debugging instrumentation to identify container build/run anomalies
Some checks failed
CI / test-unit (push) Has been cancelled
CI / test-integration (push) Has been cancelled
CI / test-container (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
- Added `git rev-parse HEAD` to test-container workflow to confirm the exact
  commit SHA used during CI runs.
- Updated Dockerfile to print BASE_IMAGE and OS release information during
  build for better reproducibility diagnostics.
- Extended test-container script to dump the first 40 lines of
  `docker image inspect` output, allowing verification of the image ID,
  creation time, and applied build args.

These additions help trace discrepancies between local builds and GitHub
Actions, ensuring we can detect mismatches in commit SHA, base image,
or container metadata.

https://chatgpt.com/share/693ae07a-8c58-800f-88e6-254cdb00b676
2025-12-11 17:27:57 +01:00
Kevin Veen-Birkenbach
26c9d79814 Added mirrors 2025-12-11 16:47:23 +01:00
Kevin Veen-Birkenbach
2776d18a42 Implemented arch support 2025-12-11 16:31:00 +01:00
Kevin Veen-Birkenbach
7057ccfb95 CI: Always rebuild test images with --no-cache before container and E2E tests
This ensures that GitHub Actions never reuses outdated Docker layers and that
each test run starts from a fully clean environment. The workflows for
test-container and test-e2e now explicitly invoke:

    distro="${{ matrix.distro }}" make build-no-cache

before executing the actual tests.
This aligns the CI behaviour with local testing, eliminates hidden caching
differences, and guarantees deterministic test results across all distros.

https://chatgpt.com/share/693ae07a-8c58-800f-88e6-254cdb00b676
2025-12-11 16:17:10 +01:00
Kevin Veen-Birkenbach
1807949c6f Add mirror management commands and refactor CLI parser into modules
- Implement new mirror actions:
  - list_mirrors: show mirrors from config, MIRRORS file, or merged view
  - diff_mirrors: compare config mirrors with MIRRORS file (ONLY IN CONFIG,
    ONLY IN FILE, URL MISMATCH, OK)
  - merge_mirrors: merge mirrors between config and MIRRORS file in both
    directions, with preview mode and user config writing via save_user_config
  - setup_mirrors: prepare local Git remotes (ensure origin) and print
    provider-URL suggestions for remote repositories
- Introduce mirror utilities:
  - RepoMirrorContext with resolved_mirrors (config + file, file wins)
  - load_config_mirrors supporting dict and list-of-dicts shapes
  - read/write MIRRORS file with simple "name url" format and preview mode
  - helper for building default SSH URLs from provider/account/repository
- Wire mirror commands into CLI:
  - Add handle_mirror_command and integrate "mirror" into dispatch
  - Add dedicated CLI parser modules under pkgmgr.cli.parser:
    * common, install_update, config_cmd, navigation_cmd,
      branch_cmd, release_cmd, version_cmd, changelog_cmd,
      list_cmd, make_cmd, mirror_cmd
  - Replace old flat cli/parser.py with modular parser package and
    SortedSubParsersAction in common.py
- Update TODO.md to mark MIRROR as implemented
- Add E2E tests for mirror commands:
  - test_mirror_help
  - test_mirror_list_preview_all
  - test_mirror_diff_preview_all
  - test_mirror_merge_config_to_file_preview_all
  - test_mirror_setup_preview_all

https://chatgpt.com/share/693adee0-aa3c-800f-b72a-98473fdaf760
2025-12-11 16:10:19 +01:00
Kevin Veen-Birkenbach
d611720b8f Solved bug when volumes don't exist 2025-12-11 15:46:45 +01:00
Kevin Veen-Birkenbach
bf871650a8 Added purge option to makefile 2025-12-11 15:29:51 +01:00
Kevin Veen-Birkenbach
5ca1adda7b Refactor CI distro handling and container build scripts
- Introduce a GitHub Actions matrix for `test-container` and `test-e2e`
  to run against arch, debian, ubuntu, fedora, and centos
- Run unit and integration tests only in the Arch container by passing
  `distro="arch"` via make in the corresponding workflows
- Replace the global DISTROS loop with a single `distro` variable in
  the Makefile, defaulting to `arch`, and export it for all scripts
- Update build scripts (build-image, build-image-no-cache, build-image-missing)
  to build images for the selected distro only
- Simplify test-container script to validate a single distro image using
  the `distro` environment variable
- Simplify E2E, unit, and integration test scripts to run against a
  single distro container instead of iterating over all distros

https://chatgpt.com/share/693acbba-9e30-800f-94fb-fea4489e9078
2025-12-11 14:48:36 +01:00
Kevin Veen-Birkenbach
acb18adf76 test: restore Dockerfile ENTRYPOINT for all test runs (fix Nix TLS on CentOS)
All test scripts (unit, integration, e2e) previously overwrote the Docker
ENTRYPOINT by using `--entrypoint bash`, which bypassed the container’s
startup logic in `docker-entry.sh`.

`docker-entry.sh` performs essential initialization steps such as:

- CA bundle auto-detection (NIX_SSL_CERT_FILE, SSL_CERT_FILE, etc.)
- Nix environment setup
- PATH adjustments and distro logging

By removing the explicit `--entrypoint bash` and invoking:

  bash -lc '...'

directly as the container command, the Dockerfile’s ENTRYPOINT is restored
and runs as intended before executing the test logic.

This fixes TLS issues in CentOS E2E runs where Nix was unable to fetch
flake inputs due to missing CA configuration.

https://chatgpt.com/share/693ac1f3-fb7c-800f-9e5c-b40c351a9f04
2025-12-11 14:06:39 +01:00
Kevin Veen-Birkenbach
c18490f5d3 deb: remove hard dependency on distro-provided Nix
The Debian Nix package causes flake builds to fail inside the test and
container environment due to sandboxing and patched Nix behavior.

To ensure consistent behaviour across all distributions and align
container logic with production logic, pkgmgr now relies on its own
`init-nix.sh` bootstrap script instead of the distro’s `nix` package.

Dropping `Depends: nix` guarantees that both Debian containers and real
Debian systems install and initialize Nix via the upstream installer,
matching the behaviour on Arch, Fedora, and Ubuntu.

https://chatgpt.com/share/693ab9bf-e6ac-800f-83ba-a4abd1bfe407
2025-12-11 13:31:56 +01:00
Kevin Veen-Birkenbach
eeda944b73 ci: migrate tests to reusable workflows and introduce stable-tag pipeline
- convert all test workflows to reusable workflow_call
- add central CI workflow for branches and PRs
- add mark-stable workflow triggered on main pushes
- ensure stable tag updates only after all tests succeed
- remove duplicated triggers from test workflows
`

https://chatgpt.com/share/693aa4a6-7460-800f-ba47-cfc15b1b2236
2025-12-11 13:04:44 +01:00
Kevin Veen-Birkenbach
52cfbebba4 ci: make mark-stable robust for workflow_run
- fetch workflow_run runs without head_sha filter
- match by workflow name and head_sha in jq
- keep tagging logic and permissions unchanged

https://chatgpt.com/share/693aa4a6-7460-800f-ba47-cfc15b1b2236
2025-12-11 12:46:42 +01:00
Kevin Veen-Birkenbach
f4385807f1 e2e: disable Nix sandbox for cross-distro flake build test
- Update test_nix_build_pkgmgr.py to invoke
    nix --option sandbox false build .#pkgmgr -L
  to avoid sandbox/permission issues in Debian and Ubuntu containers.
- Keeps the test logic identical across all distros while ensuring
  consistent flake build behaviour during E2E runs.

https://chatgpt.com/share/693aa33f-4e3c-800f-86ec-99c38a07eacb
2025-12-11 12:45:04 +01:00
Kevin Veen-Birkenbach
e9e083c9dd ci: finalize mark-stable workflow fixes
- use correct GitHub API path (/repos/.../actions/runs)
- resolve repository via workflow_run.repository.full_name
- improve logging and safe no-tag exits
- ensure correct token handling and tag update logic

https://chatgpt.com/share/693aa4a6-7460-800f-ba47-cfc15b1b2236
2025-12-11 12:38:12 +01:00
Kevin Veen-Birkenbach
3218b2b39f ci: fix mark-stable workflow for workflow_run events
- use workflow_run.repository.full_name for gh API queries
- expose GITHUB_TOKEN as GH_TOKEN for the GitHub CLI
- improve log messages and keep tag skipped when checks are missing or failing
2025-12-11 12:26:29 +01:00
Kevin Veen-Birkenbach
ba296a79c9 ci: fix mark-stable permissions and ignore Nix result symlink
https://chatgpt.com/share/693aa4a6-7460-800f-ba47-cfc15b1b2236
2025-12-11 12:16:34 +01:00
Kevin Veen-Birkenbach
62e05e2f5b ci: tag commit as stable after full test matrix
- add mark-stable workflow that runs on workflow_run for all test pipelines
- use GitHub API to ensure all required workflows succeeded before moving the 'stable' tag
- add Nix flake.lock to pin nixpkgs for reproducible builds

https://chatgpt.com/share/693aa4a6-7460-800f-ba47-cfc15b1b2236
2025-12-11 12:01:21 +01:00
Kevin Veen-Birkenbach
77d8b68ba5 Add E2E Nix flake build test across all distro containers
- Introduce tests/e2e/test_nix_build_pkgmgr.py to inspect the Nix environment
  and build the pkgmgr flake inside the container started by test-e2e.sh
- Run the same commands in every distro container: nix --version, sandbox
  config, id, and nix build .#pkgmgr -L
- Print stdout/stderr and assert the flake build succeeds for easier
  cross-distro Nix debugging

https://chatgpt.com/share/693aa33f-4e3c-800f-86ec-99c38a07eacb
2025-12-11 11:55:43 +01:00
Kevin Veen-Birkenbach
bb0a801396 Fix Git safe.directory handling in E2E containers
- Mark /src and /src/.git as safe to satisfy newer Git ownership checks
- Add '*' as safe.directory for ephemeral test containers to avoid Nix flake failures

https://chatgpt.com/share/693a9e1f-1cc8-800f-9df4-90813cbb6bd5
2025-12-11 11:33:51 +01:00
Kevin Veen-Birkenbach
ee968efc4b Harden E2E test runner and fix Git safe.directory in containers
- Quote Nix store/cache volumes and distro image name in docker run
- Use strict bash flags (set -euo pipefail) inside test container
- Print distro ID robustly with fallback
- Configure /src as Git safe.directory when git is available

https://chatgpt.com/share/693a9c0e-59ec-800f-83a1-eec31bd76962
2025-12-11 11:25:11 +01:00
Kevin Veen-Birkenbach
644b2b8fa0 Align Nix Python environment and add lazy CLI import
- Switch flake package and dev shell to Python 3.11 to match pyproject
- Ensure the python-with-deps environment is preferred on PATH in nix develop
- Introduce a lightweight pkgmgr __init__ with lazy loading of pkgmgr.cli
- Avoid pulling in CLI/config dependencies on plain `import pkgmgr`, fixing
  unit test imports and PyYAML availability in the Nix test containers

https://chatgpt.com/share/693a9723-27ac-800f-a6c2-c1bcc91b7dff
2025-12-11 11:04:12 +01:00
Kevin Veen-Birkenbach
0f74907f82 flake.nix: switch to generic python3 and remove side-effects from pkgmgr package root
- Replace hardcoded python311 references with generic python3 to avoid minor
  version pinning and ensure consistent interpreter selection across systems.
- Use python.pkgs instead of python311Packages in the build pipeline.
- Update devShell to use python3.withPackages, including pip and pyyaml.
- Add Python version echo in shellHook for improved debugging.
- Remove cli re-export from src/pkgmgr/__init__.py to eliminate heavy
  side-effects during import and prevent premature config loading in tests.
2025-12-11 10:30:19 +01:00
Kevin Veen-Birkenbach
5a8b1b11de arch packaging: exclude assets from PKGBUILD rsync
Exclude the assets/ directory from the PKGBUILD rsync step to avoid
permission issues (e.g. map.png) when building the Arch package in
Docker as aur_builder.

https://chatgpt.com/share/693a8c25-4464-800f-8d5e-5c4579d78b52
2025-12-11 10:17:14 +01:00
Kevin Veen-Birkenbach
389ec40163 Refine Nix dev shell, ensure PyYAML availability, fix Python invocation, and
expose pkgmgr.cli for Python 3.13 compatibility

- Add `.nix-dev-installed` to .gitignore
- Improve flake.nix:
  * unify pkgs/pyPkgs definitions
  * provide python311.withPackages including pip + PyYAML
  * remove unused pkgmgrPkg reference from devShell
  * fix PYTHONPATH export and devShell help message
- Update unit/integration test scripts to use `python3 -m unittest`
- Add top-level pkgmgr.__init__ exposing `cli` attribute for
  pkgutil.resolve_name compatibility under Python 3.13+
2025-12-11 09:33:55 +01:00
Kevin Veen-Birkenbach
1d03055491 Removed ignore files 2025-12-11 09:07:18 +01:00
Kevin Veen-Birkenbach
7775c6d974 Refine packaging layout and Arch build paths
* Move Arch-specific ignore rules into `packaging/arch/.gitignore` and simplify top-level `.gitignore`/`.dockerignore`.
* Update Arch `PKGBUILD` to sync from the project root and drop `packaging/` from the installed tree.
* Fix OS-specific `package.sh` helpers to resolve the new `packaging/*` locations correctly for Arch, Debian/Ubuntu, Fedora, and CentOS.
2025-12-11 09:04:17 +01:00
Kevin Veen-Birkenbach
a24a819511 Restructure repo layout, wiring src/ and packaging for local and distro builds
- Add dev runner main.py that prefers local src/ over installed pkgmgr
- Move Arch/Debian/Fedora packaging files under packaging/* and update build scripts
- Adjust .gitignore/.dockerignore for new packaging paths and src/source/
- Improve config defaults discovery to support src/ layout and installed packages
- Update architecture diagram and add TODO overview for TAGS/MIRROR/SIGNING_KEY

https://chatgpt.com/share/693a76a0-e408-800f-9939-868524cbef4d
2025-12-11 08:45:07 +01:00
Kevin Veen-Birkenbach
0a6c2f2988 Release version 0.9.1 2025-12-10 22:56:04 +01:00
Kevin Veen-Birkenbach
0c90e984ad Refine setup workflows and add architecture map
- Split virgin tests into separate root and user GitHub Actions workflows
  (test-virgin-root, test-virgin-user) and adjust Arch container flows
- Introduce scripts/installation/venv-create.sh and reuse it from
  scripts/installation/main.sh with separate root/system and user/dev paths
- Add PKGMGR architecture & setup map (assets/map.png) and section in README
  with link to the up-to-date master page
- Simplify README by removing outdated Docker quickstart, usage examples,
  and AI footer
- Extend .gitignore to exclude src/source artifacts

https://chatgpt.com/share/6939bbfe-5cb0-800f-8ea8-95628dc911f5
2025-12-10 22:51:40 +01:00
Kevin Veen-Birkenbach
0a0cbbfe6d fix(init-nix): create 'nix' user with a valid shell across all distros
The init-nix.sh script previously hardcoded /usr/bin/bash as the login shell
for the 'nix' user, which exists on Arch but not on Debian. This caused the
Nix single-user installer (run via `su - nix`) to fail silently or break in
unpredictable ways on Debian-based images.

We now resolve the shell dynamically via `command -v bash` and fall back to
/bin/sh on minimal systems. This makes Nix installation deterministic across
Arch, Debian, Ubuntu, Fedora, CentOS and CI containers.

https://chatgpt.com/share/6939e97f-c93c-800f-887b-27c7e67ec46d
2025-12-10 22:43:20 +01:00
Kevin Veen-Birkenbach
15c44cd484 Removed deprecated pkgmgr.yml 2025-12-10 21:34:33 +01:00
Kevin Veen-Birkenbach
6d7ee6fc04 Fix test scripts: ensure default distro and always run via bash
- Remove Makefile inline variable export (distro=arch) and invoke scripts via bash
- Add robust default in test-unit.sh and test-integration.sh:
    : "${distro:=arch}"
- Prevent "unbound variable" errors under `set -u` when no distro is provided
2025-12-10 21:09:18 +01:00
Kevin Veen-Birkenbach
5a022db0db Use dynamic distro selection for UNIT and INTEGRATION tests
- Pass `distro=arch` from Makefile into test scripts
- Replace hardcoded "arch" references with "${distro}"
- Update test-unit.sh and test-integration.sh to use dynamic image names
- Improve log output to reflect selected distro

https://chatgpt.com/share/6939c98a-d428-800f-8bb8-cf72e80ba80c
2025-12-10 20:27:03 +01:00
Kevin Veen-Birkenbach
37ac22e0b4 test: isolate Nix store/cache per distro to fix cross-distro manifest conflicts
- Replace shared Nix volumes with distro-specific volumes
  (pkgmgr_nix_store_<distro>, pkgmgr_nix_cache_<distro>)
- Prevent incompatible profile manifest versions between Ubuntu and Debian
- Update all test scripts (unit, integration, container, e2e)
- Remove unused global Nix volume variables from Makefile
- Improve consistency of test-e2e.sh formatting and environment handling
- Add Git safe.directory configuration for mounted /src to avoid ownership warnings
2025-12-10 20:07:41 +01:00
Kevin Veen-Birkenbach
bcea440e40 Fix path and shell repo directory resolution + add unit/E2E tests
- Introduce `_resolve_repository_directory()` to unify directory lookup
  (explicit `directory` key → fallback to `get_repo_dir()` using base dir)
- Fix `pkgmgr path` to avoid KeyError and behave consistently with
  other commands using lazy directory resolution
- Fix `pkgmgr shell` to use resolved directory and correctly emit cwd
- Add full E2E tests for `pkgmgr path --all` and `pkgmgr path pkgmgr`
- Add unit tests covering:
    * explicit directory usage
    * fallback resolution via get_repo_dir()
    * empty selection behavior
    * shell command cwd resolution
    * missing shell command error handling
2025-12-10 19:47:26 +01:00
Kevin Veen-Birkenbach
6edde2d65b Release version 0.9.0 2025-12-10 18:38:10 +01:00
Kevin Veen-Birkenbach
74189c1e14 Add virgin Nix flake E2E workflow and update .gitignore
- Introduce `test-nix-flake-e2e.yml` workflow to run a full Arch-based virgin
  environment test with Nix flakes enabled and shared Docker caches
- Ensure pkgmgr self-installation and flake-based installer path are exercised
- Update .gitignore with additional build artifacts, Debian packaging files,
  and pkgmgr output directories
2025-12-10 18:37:29 +01:00
Kevin Veen-Birkenbach
b5ddf7402a Release version 0.8.0 2025-12-10 17:32:00 +01:00
Kevin Veen-Birkenbach
900224ed2e Moved installer dir 2025-12-10 17:27:26 +01:00
Kevin Veen-Birkenbach
e290043089 Refine installer capability integration tests and documentation
- Adjust install_repos integration test to patch resolve_command_for_repo
  in the pipeline module and tighten DummyInstaller overrides
- Rewrite recursive capability integration tests to focus on layer
  ordering and capability shadowing across Makefile, Python, Nix
  and OS-package installers
- Extend recursive capabilities markdown with hierarchy diagram,
  capability matrix, scenario matrix and link to the external
  setup controller schema

https://chatgpt.com/share/69399857-4d84-800f-a636-6bcd1ab5e192
2025-12-10 17:23:33 +01:00
Kevin Veen-Birkenbach
a7fd37d646 Add unit tests for install pipeline, Nix flake installer, and command resolution
https://chatgpt.com/share/69399857-4d84-800f-a636-6bcd1ab5e192
2025-12-10 16:57:02 +01:00
Kevin Veen-Birkenbach
d4b00046d3 Refine installer layering and Python/Nix integration
- Introduce explicit CLI layer model (os-packages, nix, python, makefile)
  and central InstallationPipeline to orchestrate installers.
- Move installer orchestration out of install_repos() into
  pkgmgr.actions.repository.install.pipeline, using layer precedence and
  capability tracking.
- Add pkgmgr.actions.repository.install.layers to classify commands into
  layers and compare priorities.
- Rework PythonInstaller to always use isolated environments:
  PKGMGR_PIP override → active venv → per-repo venv under ~/.venvs/<identifier>,
  avoiding system Python and PEP 668 conflicts.
- Adjust NixFlakeInstaller to install flake outputs based on repository
  identity: pkgmgr/package-manager → pkgmgr (mandatory) + default (optional),
  all other repos → default (mandatory).
- Tighten MakefileInstaller behaviour, add global
  PKGMGR_DISABLE_MAKEFILE_INSTALLER switch, and simplify install target
  detection.
- Rewrite resolve_command_for_repo() with explicit Repository typing,
  better Python package detection, Nix/PATH resolution, and a
  library-only fallback instead of raising on missing CLI.
- Update flake.nix devShell to provide python3 with pip and add pip as a
  propagated build input.
- Remove deprecated/wip repository entries from config defaults and drop
  the unused config/wip.yml.

https://chatgpt.com/share/69399157-86d8-800f-9935-1a820893e908
2025-12-10 16:26:23 +01:00
Kevin Veen-Birkenbach
545d345ea4 core(command): implement explicit command=None bypass and add unit tests
This update introduces Variant B behavior in the command resolver:

- If a repository explicitly defines the key \"command\" (even if its value is None),
  resolve_command_for_repo() treats it as authoritative and returns immediately.
  This allows library-only repositories to declare:
      command: null
  which disables CLI resolution entirely.

- As a result, Python package repositories without installed CLI entry points
  no longer trigger SystemExit during update/install flows, as long as they set
  command: null in their repo configuration.

The resolution logic is now bypassed for such repositories, skipping:
  - Python package detection (src/*/__main__.py)
  - PATH/Nix/venv binary lookup
  - main.sh/main.py fallback evaluation

A new unit test suite has been added under
  tests/unit/pkgmgr/core/command/test_resolve.py
covering:

 1) Python package without installed command → SystemExit
 2) Python package with installed command → returned correctly
 3) Script repository fallback to main.py
 4) Explicit command overrides all logic

This commit stabilizes update/install flows and ensures library-only
repositories behave as intended when no CLI command is provided.

https://chatgpt.com/share/69394a53-bc78-800f-995d-21099a68dd60
2025-12-10 11:23:57 +01:00
Kevin Veen-Birkenbach
a29b831e41 Release version 0.7.14 2025-12-10 10:38:36 +01:00
Kevin Veen-Birkenbach
bc9ca140bd fix(e2e): treat SystemExit(0) as successful CLI termination in clone-all test
The pkgmgr proxy layer may intentionally terminate the process via
SystemExit(0). The previous test logic interpreted any SystemExit as a failure,
causing false negatives during `pkgmgr clone --all` E2E runs.

This patch updates `test_clone_all.py` to:
- accept SystemExit(0) as a successful run,
- only fail on non-zero exit codes,
- preserve diagnostic output for real failures.

This stabilizes the clone-all E2E test across proxy-triggered exits.

https://chatgpt.com/share/69393f6b-b854-800f-aabb-25811bbb8c74
2025-12-10 10:37:40 +01:00
Kevin Veen-Birkenbach
ad8e3cd07c Updated CHANGELOG.md 2025-12-10 10:28:20 +01:00
Kevin Veen-Birkenbach
22efe0b32e Release version 0.7.13 2025-12-10 10:27:27 +01:00
Kevin Veen-Birkenbach
d23a0a94d5 Fix tools path resolution and add tests
- Use _resolve_repository_path() for explore, terminal and code commands
  so tools no longer rely on a 'directory' key in the repository dict.
- Fall back to repositories_base_dir/repositories_dir via get_repo_dir()
  when no explicit path-like key is present.
- Make VS Code workspace creation more robust (safe default for
  directories.workspaces and UTF-8 when writing JSON).
- Add unit tests for handle_tools_command (explore, terminal, code) under
  tests/unit/pkgmgr/cli/commands/test_tools.py.
- Add E2E/integration-style tests for the tools subcommands' --help
  output under tests/e2e/test_tools_help.py, treating SystemExit(0) as
  success.

This change fixes the KeyError: 'directory' when running 'pkgmgr code'
and verifies the behavior via unit and integration tests.

https://chatgpt.com/share/69393ca1-b554-800f-9967-abf8c4e3fea3
2025-12-10 10:25:29 +01:00
Kevin Veen-Birkenbach
e42b79c9d8 Add E2E tests for 'clone --all' and 'update --all' using HTTPS mode
This commit introduces two new end-to-end integration tests:

  • tests/e2e/test_clone_all.py
      Runs: pkgmgr clone --all --clone-mode https --no-verification
      Verifies that full HTTPS cloning of all configured repositories
      works inside the test container environment.

  • tests/e2e/test_update_all.py
      Runs: pkgmgr update --all --clone-mode https --no-verification
      Ensures that updating all repositories with HTTPS mode completes
      successfully without raising exceptions.

Both tests:
  - Provide extended diagnostics on SystemExit
  - Reuse nix-profile cleanup helpers for consistent test environments
  - Validate that `pkgmgr --help` works after execution

These tests complement the existing shallow-install integration test
and improve overall reliability of HTTPS clone/update workflows.
2025-12-09 23:47:43 +01:00
Kevin Veen-Birkenbach
3b2c657bfa Release version 0.7.12 2025-12-09 23:36:38 +01:00
Kevin Veen-Birkenbach
e335ab05a1 fix(core/ink): prevent self-referential symlinks + add unit tests
This commit adds a safety guard to create_ink() to prevent creation of
self-referential symlinks when the resolved command already lives at the
intended link target (e.g. ~/.local/bin/package-manager). Such a situation
previously resulted in broken shells with the error:

    "zsh: too many levels of symbolic links"

Key changes:
  - create_ink():
      • Introduce early-abort guard when command == link_path
      • Improve function signature and formatting
      • Enhance alias creation messaging

  - Added comprehensive unit tests under:
        tests/unit/pkgmgr/core/command/test_ink.py
    Tests cover:
      • Self-referential command path → skip symlink creation
      • Standard symlink + alias creation behaviour

This prevents pkgmgr from overwriting user-managed binaries inside ~/.local/bin
and ensures predictable, safe behaviour across all installer layers.

https://chatgpt.com/share/6938a43b-0eb8-800f-9545-6cb555ab406d
2025-12-09 23:35:29 +01:00
Kevin Veen-Birkenbach
75f963d6e2 Removed tests/e2e/test_install_all_shallow.py 2025-12-09 23:18:49 +01:00
Kevin Veen-Birkenbach
94b998741f Release version 0.7.11 2025-12-09 23:16:48 +01:00
Kevin Veen-Birkenbach
172c734866 test: fix installer unit tests for OS packages and Nix dev shell
Update Debian, RPM, Nix flake, and Python installer unit tests to match the current
installer behavior and to run correctly inside the Nix development shell.

- DebianControlInstaller:
  - Add clearer docstrings for supports() behavior.
  - Relax final install assertion to accept dpkg -i, sudo dpkg -i, or
    sudo apt-get install -y.
  - Keep checks for apt-get update, apt-get build-dep, and dpkg-buildpackage.

- RpmSpecInstaller:
  - Add docstrings for supports() conditions.
  - Mock _prepare_source_tarball() to avoid touching the filesystem.
  - Assert builddep, rpmbuild -ba, and sudo dnf install -y commands.

- NixFlakeInstaller:
  - Ensure supports() and run() tests simulate a non-Nix-shell environment
    via IN_NIX_SHELL and PKGMGR_DISABLE_NIX_FLAKE_INSTALLER.
  - Verify that the old profile entry is removed and both pkgmgr and default
    flake outputs are installed.
  - Confirm _ensure_old_profile_removed() swallows SystemExit.

- PythonInstaller:
  - Make supports() and run() tests ignore the real IN_NIX_SHELL environment.
  - Assert that pip install . is invoked with cwd set to the repository
    directory.

These changes make the unit tests stable in the Nix dev shell and align them
with the current installer implementations.
2025-12-09 23:15:56 +01:00
Kevin Veen-Birkenbach
1b483e178d Release version 0.7.10 2025-12-09 22:57:11 +01:00
Kevin Veen-Birkenbach
78693225f1 test: share persistent Nix store across all test containers
This commit adds the `pkgmgr_nix_store` volume mount (`/nix`) to all test
runners (unit, integration, container sanity checks, and E2E tests).

Previously only the Arch-based E2E container mounted a persistent `/nix`
store, causing all other distros (Debian, Ubuntu, Fedora, CentOS, etc.)
to download the entire Nix closure repeatedly during test runs.

Changes:
- Add `-v pkgmgr_nix_store:/nix` to:
  - scripts/test/test-container.sh
  - scripts/test/test-e2e.sh (remove Arch-only condition)
  - scripts/test/test-unit.sh
  - scripts/test/test-integration.sh
- Ensures all test containers reuse the same Nix store.

Benefits:
- Significantly faster test execution after the first run.
- Prevents redundant downloads from cache.nixos.org.
- Ensures consistent Nix environments across all test distros.

No functional changes to pkgmgr itself; only test infrastructure improved.

https://chatgpt.com/share/693890f5-2f54-800f-b47e-1925da85b434
2025-12-09 22:13:01 +01:00
Kevin Veen-Birkenbach
ca08c84789 Merge branch 'fix/branch-master' 2025-12-09 21:19:53 +01:00
Kevin Veen-Birkenbach
e930b422e5 Release version 0.7.9 2025-12-09 21:19:13 +01:00
Kevin Veen-Birkenbach
0833d04376 Improve branch helpers with main/master base resolution
- Update pkgmgr.actions.branch.open_branch() to resolve the base branch
  via _resolve_base_branch(), preferring 'main' and falling back to
  'master' when the preferred branch does not exist.
- Adjust the open_branch logic to:
  - fetch from origin
  - checkout the resolved base branch
  - pull the resolved base branch
  - create the feature branch
  - push the new branch with upstream tracking
- Add and refine unit tests in tests/unit/pkgmgr/actions/test_branch.py
  to cover:
  - normal branch creation with explicit name and default base
  - interactive name prompting when no name is provided
  - error handling when fetch fails after successful base resolution
  - fallback to 'master' when 'main' is missing.
- Clean up and clarify docstrings and comments for open_branch(),
  close_branch(), and _resolve_base_branch(), and fix the module header
  comment to match the new package path.

This fixes branch opening in repositories that still use 'master' as
their primary branch while keeping the default behavior for 'main'.

https://chatgpt.com/share/6938838f-7aac-800f-b130-924e07ef48b9
2025-12-09 21:16:10 +01:00
Kevin Veen-Birkenbach
55f36d76ec Merge branch 'fix/file-error' 2025-12-09 21:09:48 +01:00
Kevin Veen-Birkenbach
6a838ee84f Release version 0.7.8 2025-12-09 21:03:24 +01:00
Kevin Veen-Birkenbach
4285bf4a54 Fix: release now skips missing pyproject.toml without failing
- Updated update_pyproject_version() to gracefully skip missing or unreadable pyproject.toml
- Added corresponding unit test ensuring missing file triggers no exception and no file creation
- Updated test wording for spec changelog section
- Ref: adjustments discussed in ChatGPT conversation (2025-12-09) - https://chatgpt.com/share/69388024-93e4-800f-a09f-bf78a6b9a53f
2025-12-09 21:02:01 +01:00
Kevin Veen-Birkenbach
640b1042c2 git commit -m "Harden installers for Nix, OS packages and Docker CA handling
- NixFlakeInstaller:
  - Skip when running inside a Nix dev shell (IN_NIX_SHELL).
  - Add PKGMGR_DISABLE_NIX_FLAKE_INSTALLER kill-switch for CI/debugging.
  - Ensure run() respects supports() and handles preview/allow_failure cleanly.

- DebianControlInstaller:
  - Introduce _privileged_prefix() to handle sudo vs. root vs. no elevation.
  - Avoid hard-coded sudo usage and degrade gracefully when neither sudo nor
    root is available.
  - Improve messaging around build-dep and .deb installation.

- RpmSpecInstaller:
  - Prepare rpmbuild tree and source tarball in ~/rpmbuild/SOURCES based on
    Name/Version from the spec file.
  - Reuse a helper to resolve the rpmbuild topdir.
  - Install built RPMs via dnf/yum when available, falling back to rpm -Uvh
    to avoid file conflicts during upgrades.

- PythonInstaller:
  - Skip pip-based installation inside Nix dev shells (IN_NIX_SHELL).
  - Add PKGMGR_DISABLE_PYTHON_INSTALLER kill-switch.
  - Make pip command resolution explicit and overridable via PKGMGR_PIP.
  - Type-hint supports() and run() with RepoContext/InstallContext.

- Docker entrypoint:
  - Add robust CA bundle detection for Nix, Git, Python requests and curl.
  - Export NIX_SSL_CERT_FILE, SSL_CERT_FILE, REQUESTS_CA_BUNDLE and
    GIT_SSL_CAINFO from a single detected CA path.
  - Improve logging and section comments in the entrypoint script."

https://chatgpt.com/share/69387df8-bda0-800f-a053-aa9e2999dc84
2025-12-09 20:52:07 +01:00
Kevin Veen-Birkenbach
9357c4632e Release version 0.7.7 2025-12-09 17:54:41 +01:00
Kevin Veen-Birkenbach
ca5d0d22f3 feat(test): make unittest pattern configurable and pass TEST_PATTERN into containers
This update introduces a configurable TEST_PATTERN variable in the Makefile,
allowing selective execution of unit, integration, and E2E tests without
modifying scripts.

Key changes:
- Add TEST_PATTERN (default: test_*.py) to Makefile and export it.
- Inject TEST_PATTERN into all test containers via `-e TEST_PATTERN=...`.
- Update test-unit.sh, test-integration.sh, and test-e2e.sh to use
  `-p "$TEST_PATTERN"` instead of a hardcoded pattern.
- Ensure flexible test selection via:
      make test-e2e TEST_PATTERN=test_install_pkgmgr_shallow.py

This enables fast debugging, selective test runs, and better developer
experience while keeping full compatibility with CI defaults.

https://chatgpt.com/share/69385400-2f14-800f-b093-bb03c8ef9c7f
2025-12-09 17:53:10 +01:00
Kevin Veen-Birkenbach
3875338fb7 Release version 0.7.6 2025-12-09 17:14:22 +01:00
Kevin Veen-Birkenbach
196f55c58e feat(repository/pull): improve verification logic and add full unit test suite
This commit enhances the behaviour of pull_with_verification() and adds a
comprehensive unit test suite covering all control flows.

Changes:
- Added `preview` parameter to fully disable interaction and execution.
- Improved verification logic:
  * Prompt only when not in preview, verification is enabled,
    verification info exists, and verification failed.
  * Skip prompts entirely when --no-verification is set.
- More explicit construction of `git pull` command with optional extra args.
- Improved messaging and formatting for clarity.
- Ensured directory existence is checked before any verification logic.
- Added detailed comments explaining logic and conditions.

Tests:
- New file tests/unit/pkgmgr/actions/repos/test_pull_with_verification.py
- Covers:
  * Preview mode (no input, no subprocess)
  * Verification failure – user rejects
  * Verification failure – user accepts
  * Verification success – immediate git call
  * Missing repository directory – skip silently
  * --no-verification flag bypasses prompts
  * Command formatting with extra args
- Uses systematic mocking for identifier, repo-dir, verify_repository(),
  subprocess.run(), and user input.

This significantly strengthens correctness, UX, and test coverage of the
repository pull workflow.

https://chatgpt.com/share/69384aaa-0c80-800f-b4b4-64e6fbdebd3b
2025-12-09 17:12:23 +01:00
Kevin Veen-Birkenbach
9a149715f6 Release version 0.7.5 2025-12-09 16:45:45 +01:00
Kevin Veen-Birkenbach
bf40533469 fix(init-nix): ensure /nix is always owned by nix:nixbld in container root mode
In GitHub's Fedora-based CI containers the directory /nix may already exist
(e.g. from the base image or a previous build layer) and is often owned by
root:root. In this situation the Nix single-user installer aborts with:

    "directory /nix exists, but is not writable by you"

This caused the container build to fail during `init-nix.sh`, leaving no
working `nix` binary on PATH. As a result, the runtime wrapper
(pkmgr-wrapper.sh) reported:

    "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH."

Local runs did not show the issue because a previous installation had already
created /nix with correct ownership.

This commit makes container-mode Nix initialization fully idempotent:

  • If /nix does not exist → create it with owner nix:nixbld (existing logic).
  • If /nix exists but has wrong owner/group → forcibly chown -R nix:nixbld.
  • A warning is emitted if /nix remains non-writable after correction.

This guarantees that the Nix installer always has writable access to /nix
and prevents the installer from aborting in CI. As a result, `pkgmgr --help`
works again inside Fedora CI containers.

https://chatgpt.com/share/69384149-9dc8-800f-8148-55817ece8e21
2025-12-09 16:33:22 +01:00
Kevin Veen-Birkenbach
7bc7259988 Release version 0.7.4 2025-12-09 16:22:03 +01:00
Kevin Veen-Birkenbach
66b96ac3a5 Refactor CI workflows and Makefile to unify container builds and simplify test execution
This commit updates all GitHub Actions workflows and the Makefile to ensure
consistent behavior across unit, integration, end-to-end, and OS-container
tests.

Changes include:

CI Workflows:
  - Rename workflows for clearer, more professional naming:
        * "Test Distribution Containers" → "Test OS Containers"
        * "Test package-manager (e2e)" → "Test End-To-End"
        * "Test package-manager (unit)" → "Test Units"
        * "Test package-manager (integration)" → "Test Code Integration"
  - Remove explicit build steps from workflows; container creation is now
    delegated to the Makefile via build-missing.
  - Restrict test jobs to only build the Arch test container by setting:
        DISTROS="arch"

Makefile:
  - Add build-missing as a dependency to all test targets:
        test-unit, test-integration, test-e2e, test-container
  - Remove redundant build-missing call from the combined 'test' target,
    since Make now ensures build-missing runs exactly once per invocation.
  - Preserve existing target structure while ensuring container images are
    built automatically on demand.

This makes the CI pipeline faster, more predictable, and removes duplicated
container build logic. All tests now use the same unified mechanism for
building missing images.
2025-12-09 16:18:15 +01:00
Kevin Veen-Birkenbach
f974e0b14a Release version 0.7.3 2025-12-09 16:08:34 +01:00
Kevin Veen-Birkenbach
de8c3f768d feat(repository): integrate ignore filtering into selection pipeline + add unit tests
This commit introduces proper handling of the `ignore: true` flag in the
repository selection mechanism and adds comprehensive unit tests for both
`ignored.py` and `selected.py`.

- `get_selected_repos()` now filters ignored repositories in all implicit
  selection modes:
    • filter-only mode (string/category/tag)
    • `--all` mode
    • CWD-based selection

- Explicit identifiers (e.g. `pkgmgr install ignored-repo`) **bypass**
  ignore filtering, so the user can still operate directly on ignored
  repositories if they ask for them explicitly.

- Added `_maybe_filter_ignored()` helper to handle logic cleanly and allow
  future extension (e.g. integrating a CLI flag `--include-ignored`).

Under `tests/unit/pkgmgr/core/repository`:

1. **test_ignored.py**
   • Ensures `filter_ignored()` removes repos with `ignore: true`
   • Ensures empty lists are handled correctly

2. **test_selected.py**
   Comprehensive coverage of the selection logic:
   • Explicit identifiers bypass ignore filtering
   • Filter-only mode excludes ignored repos unless `include_ignored=True`
   • `--all` mode excludes ignored repos unless explicitly overridden
   • CWD-based detection filters ignored repos unless explicitly overridden

Before this change, ignored repositories still appeared in `pkgmgr list`,
`pkgmgr status`, and other commands using `get_selected_repos()`.
This was unintuitive and broke the expected semantics of the `ignore`
attribute.
The new logic ensures ignored repositories are truly invisible unless
explicitly requested.

https://chatgpt.com/share/69383b41-50a0-800f-a2b9-c680cd96d9e9
2025-12-09 16:07:39 +01:00
Kevin Veen-Birkenbach
05ff250251 Release version 0.7.2 2025-12-09 15:49:01 +01:00
Kevin Veen-Birkenbach
ab52d37467 Refactor release helper into actions package and add RPM changelog support
- Move the monolithic pkgmgr/actions/release.py implementation into the
  pkgmgr.actions.release package, splitting concerns into versioning,
  git_ops and files helpers.
- Extend the release orchestration to update Fedora/RPM %changelog
  entries via update_spec_changelog(), reusing the same effective
  release message as for CHANGELOG.md and debian/changelog.
- Wire the new update_spec_changelog() helper into _release_impl() so
  every release keeps project, Debian and RPM metadata in sync.
- Add unit tests for update_spec_changelog() and for the updated release
  orchestration behaviour in preview and real modes.
- Remove the deprecated pkgmgr/actions/release.py module.

See ChatGPT discussion: https://chatgpt.com/share/6938368e-0940-800f-92d3-f2ccfddab794
2025-12-09 15:47:37 +01:00
Kevin Veen-Birkenbach
80329b85fb Release version 0.7.1 2025-12-09 15:26:56 +01:00
Kevin Veen-Birkenbach
44ff0a6cd9 Release version 0.7.0 2025-12-09 15:21:06 +01:00
Kevin Veen-Birkenbach
e00b1a7b69 Solved import bug 2025-12-09 15:03:31 +01:00
Kevin Veen-Birkenbach
14f0188efd Solved e2e naming bugs 2025-12-09 15:02:04 +01:00
Kevin Veen-Birkenbach
a4efb847ba Cleaned Up tests 2025-12-09 14:33:32 +01:00
Kevin Veen-Birkenbach
d50891dfe5 Refactor: Restructure pkgmgr into actions/, core/, and cli/ (full module breakup)
This commit introduces a large-scale structural refactor of the pkgmgr
codebase. All functionality has been moved from the previous flat
top-level layout into three clearly separated namespaces:

  • pkgmgr.actions      – high-level operations invoked by the CLI
  • pkgmgr.core         – pure logic, helpers, repository utilities,
                          versioning, git helpers, config IO, and
                          command resolution
  • pkgmgr.cli          – parser, dispatch, context, and command
                          handlers

Key improvements:
  - Moved all “branch”, “release”, “changelog”, repo-management
    actions, installer pipelines, and proxy execution logic into
    pkgmgr.actions.<domain>.
  - Reworked installer structure under
        pkgmgr.actions.repository.install.installers
    including OS-package installers, Nix, Python, and Makefile.
  - Consolidated all low-level functionality under pkgmgr.core:
        • git helpers → core/git
        • config load/save → core/config
        • repository helpers → core/repository
        • versioning & semver → core/version
        • command helpers (alias, resolve, run, ink) → core/command
  - Replaced pkgmgr.cli_core with pkgmgr.cli and updated all imports.
  - Added minimal __init__.py files for clean package exposure.
  - Updated all E2E, integration, and unit tests with new module paths.
  - Fixed patch targets so mocks point to the new structure.
  - Ensured backward compatibility at the CLI boundary (pkgmgr entry point unchanged).

This refactor produces a cleaner, layered architecture:
  - `core` = logic
  - `actions` = orchestrated behaviour
  - `cli` = user interface

Reference: ChatGPT-assisted refactor discussion
https://chatgpt.com/share/6938221c-e24c-800f-8317-7732cedf39b9
2025-12-09 14:20:19 +01:00
Kevin Veen-Birkenbach
59d0355b91 Release version 0.6.0 2025-12-09 05:59:58 +01:00
Kevin Veen-Birkenbach
da9d5cfa6b Fix container tests, unify RPM install path, and ensure Nix TLS truststore detection
Changes included:
• GitHub Actions workflow: rename job from 'test-unit' to 'test-container' to match intent.
• RPM packaging: replace %{_libdir}/package-manager with a fixed /usr/lib/package-manager
  to avoid lib/lib64 divergence on CentOS and ensure pkgmgr + Nix flake resolution works
  consistently across distros.
• Docker entrypoint: add automatic CA-bundle detection and set NIX_SSL_CERT_FILE to fix
  TLS issues on CentOS ('unable to get local issuer certificate') when Nix fetches flake
  inputs.

These updates stabilize container-based tests and unify the runtime environment
for Fedora, CentOS, and other distributions.

Reference:
ChatGPT conversation: https://chatgpt.com/share/6937aa72-d33c-800f-a63f-c353e92de6b3
2025-12-09 05:50:08 +01:00
Kevin Veen-Birkenbach
f9943fafae Refactor container build and installation pipeline to use configurable Makefile parameters (e.g. DISTROS, base images) and propagate them through all build, install, and test scripts 2025-12-09 05:31:55 +01:00
Kevin Veen-Birkenbach
7d73007181 Release version 0.5.1 2025-12-09 01:21:31 +01:00
263 changed files with 12510 additions and 4730 deletions

View File

@@ -25,7 +25,5 @@ venv/
.DS_Store
Thumbs.db
# Arch pkg artifacts
*.pkg.tar.*
*.log
package-manager-*
# Logs
*.log

29
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: CI
on:
push:
branches-ignore:
- main
pull_request:
jobs:
test-unit:
uses: ./.github/workflows/test-unit.yml
test-integration:
uses: ./.github/workflows/test-integration.yml
test-env-virtual:
uses: ./.github/workflows/test-env-virtual.yml
test-env-nix:
uses: ./.github/workflows/test-env-nix.yml
test-e2e:
uses: ./.github/workflows/test-e2e.yml
test-virgin-user:
uses: ./.github/workflows/test-virgin-user.yml
test-virgin-root:
uses: ./.github/workflows/test-virgin-root.yml

102
.github/workflows/mark-stable.yml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: Mark stable commit
on:
push:
branches:
- main # still run tests for main
tags:
- 'v*' # run tests for version tags (e.g. v0.9.1)
jobs:
test-unit:
uses: ./.github/workflows/test-unit.yml
test-integration:
uses: ./.github/workflows/test-integration.yml
test-env-virtual:
uses: ./.github/workflows/test-env-virtual.yml
test-env-nix:
uses: ./.github/workflows/test-env-nix.yml
test-e2e:
uses: ./.github/workflows/test-e2e.yml
test-virgin-user:
uses: ./.github/workflows/test-virgin-user.yml
test-virgin-root:
uses: ./.github/workflows/test-virgin-root.yml
mark-stable:
needs:
- test-unit
- test-integration
- test-env-nix
- test-env-virtual
- test-e2e
- test-virgin-user
- test-virgin-root
runs-on: ubuntu-latest
# Only run this job if the push is for a version tag (v*)
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write # Required to move/update the tag
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true # We need all tags for version comparison
- name: Move 'stable' tag only if this version is the highest
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
echo "Ref: $GITHUB_REF"
echo "SHA: $GITHUB_SHA"
VERSION="${GITHUB_REF#refs/tags/}"
echo "Current version tag: ${VERSION}"
echo "Collecting all version tags..."
ALL_V_TAGS="$(git tag --list 'v*' || true)"
if [[ -z "${ALL_V_TAGS}" ]]; then
echo "No version tags found. Skipping stable update."
exit 0
fi
echo "All version tags:"
echo "${ALL_V_TAGS}"
# Determine highest version using natural version sorting
LATEST_TAG="$(printf '%s\n' ${ALL_V_TAGS} | sort -V | tail -n1)"
echo "Highest version tag: ${LATEST_TAG}"
if [[ "${VERSION}" != "${LATEST_TAG}" ]]; then
echo "Current version ${VERSION} is NOT the highest version."
echo "Stable tag will NOT be updated."
exit 0
fi
echo "Current version ${VERSION} IS the highest version."
echo "Updating 'stable' tag..."
# Delete existing stable tag (local + remote)
git tag -d stable 2>/dev/null || true
git push origin :refs/tags/stable || true
# Create new stable tag
git tag stable "$GITHUB_SHA"
git push origin stable
echo "✅ Stable tag updated to ${VERSION}."

View File

@@ -1,18 +1,16 @@
name: Test package-manager (e2e)
name: Test End-To-End
on:
push:
branches:
- main
- master
- develop
- "*"
pull_request:
workflow_call:
jobs:
test-e2e:
runs-on: ubuntu-latest
timeout-minutes: 60 # E2E + all distros can be heavier
timeout-minutes: 60 # E2E can be heavier
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
@@ -21,5 +19,7 @@ jobs:
- name: Show Docker version
run: docker version
- name: Run E2E tests via make (all distros)
run: make test-e2e
- name: Run E2E tests via make (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make test-e2e

26
.github/workflows/test-env-nix.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Test Virgin Nix (flake only)
on:
workflow_call:
jobs:
test-env-nix:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show Docker version
run: docker version
- name: Nix flake-only test (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make test-env-nix

28
.github/workflows/test-env-virtual.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Test OS Containers
on:
workflow_call:
jobs:
test-env-virtual:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show commit SHA
run: git rev-parse HEAD
- name: Show Docker version
run: docker version
- name: Run container tests (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make test-env-virtual

View File

@@ -1,13 +1,7 @@
name: Test package-manager (integration)
name: Test Code Integration
on:
push:
branches:
- main
- master
- develop
- "*"
pull_request:
workflow_call:
jobs:
test-integration:
@@ -21,9 +15,5 @@ jobs:
- name: Show Docker version
run: docker version
# Build Arch test image (same as used in test-unit and test-e2e)
- name: Build test images
run: make build
- name: Run integration tests via make (Arch container)
run: make test-integration
run: make test-integration distro="arch"

View File

@@ -1,13 +1,7 @@
name: Test package-manager (unit)
name: Test Units
on:
push:
branches:
- main
- master
- develop
- "*"
pull_request:
workflow_call:
jobs:
test-unit:
@@ -22,4 +16,4 @@ jobs:
run: docker version
- name: Run unit tests via make (Arch container)
run: make test-unit
run: make test-unit distro="arch"

56
.github/workflows/test-virgin-root.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: Test Virgin Root
on:
workflow_call:
jobs:
test-virgin-root:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show Docker version
run: docker version
# 🔹 BUILD virgin image if missing
- name: Build virgin container (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make build-missing-virgin
# 🔹 RUN test inside virgin image
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
run: |
set -euo pipefail
docker run --rm \
-v "$PWD":/src \
-v pkgmgr_repos:/root/Repositories \
-v pkgmgr_pip_cache:/root/.cache/pip \
-w /src \
"pkgmgr-${{ matrix.distro }}-virgin" \
bash -lc '
set -euo pipefail
git config --global --add safe.directory /src
make install
make setup
. "$HOME/.venvs/pkgmgr/bin/activate"
export NIX_CONFIG="experimental-features = nix-command flakes"
pkgmgr update pkgmgr --clone-mode shallow --no-verification
pkgmgr version pkgmgr
echo ">>> Running Nix-based: nix run .#pkgmgr -- version pkgmgr"
nix run /src#pkgmgr -- version pkgmgr
'

65
.github/workflows/test-virgin-user.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Test Virgin User
on:
workflow_call:
jobs:
test-virgin-user:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
distro: [arch, debian, ubuntu, fedora, centos]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show Docker version
run: docker version
# 🔹 BUILD virgin image if missing
- name: Build virgin container (${{ matrix.distro }})
run: |
set -euo pipefail
distro="${{ matrix.distro }}" make build-missing-virgin
# 🔹 RUN test inside virgin image as non-root
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
run: |
set -euo pipefail
docker run --rm \
-v "$PWD":/src \
-w /src \
"pkgmgr-${{ matrix.distro }}-virgin" \
bash -lc '
set -euo pipefail
make install
useradd -m dev
echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
chmod 0440 /etc/sudoers.d/dev
chown -R dev:dev /src
mkdir -p /nix/store /nix/var/nix /nix/var/log/nix /nix/var/nix/profiles
chown -R dev:dev /nix
chmod 0755 /nix
chmod 1777 /nix/store
sudo -H -u dev env HOME=/home/dev PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 bash -lc "
set -euo pipefail
cd /src
make setup-venv
. \"\$HOME/.venvs/pkgmgr/bin/activate\"
pkgmgr version pkgmgr
export NIX_REMOTE=local
export NIX_CONFIG=\"experimental-features = nix-command flakes\"
nix run /src#pkgmgr -- version pkgmgr
"
'

11
.gitignore vendored
View File

@@ -1,9 +1,6 @@
# Prevents unwanted files from being committed to version control.
# Custom Config file
config/config.yaml
# Python bytecode
__pycache__/
*.pyc
@@ -15,8 +12,9 @@ venv/
# Build artifacts
dist/
build/
build/*
*.egg-info/
package-manager-*
# Editor files
.vscode/
@@ -28,7 +26,10 @@ Thumbs.db
# Nix Cache to speed up tests
.nix/
.nix-dev-installed
flake.lock
# Ignore logs
*.log
package-manager-*
result

View File

@@ -1,3 +1,257 @@
## [1.3.0] - 2025-12-12
* **Minor release Stability & CI hardening**
* 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
* Improved error handling and deterministic behavior for non-root environments
* Refactored Docker and CI workflows for reproducible multi-distro virgin tests
* Made E2E tests more realistic by executing real CLI commands
* Fixed Python compatibility and missing dependencies on affected distros
## [1.2.1] - 2025-12-12
* **Changed**
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
**Fixed**
* Fixed Nix installer permission issues when running under a different user in containers.
* Improved reliability of post-install Nix initialization across all distro packages.
**CI**
* Replaced generic container tests with explicit environment checks.
* Validate Nix availability via *nix flake* tests instead of Docker build-time side effects.
## [1.2.0] - 2025-12-12
* **Release workflow overhaul**
* Introduced a fully structured release workflow with clear phases and safeguards
* Added preview-first releases with explicit confirmation before execution
* Automatic handling of *latest* tag when a release is the newest version
* Optional branch closing after successful releases with interactive confirmation
* Improved safety by syncing with remote before any changes
* Clear separation of concerns (workflow, git handling, prompts, versioning)
## [1.1.0] - 2025-12-12
* Added *branch drop* for destructive branch deletion and introduced *--force/-f* flags for branch close and branch drop to skip confirmation prompts.
## [1.0.0] - 2025-12-11
* **1.0.0 Official Stable Release 🎉**
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
---
**Key Features**
**Core Functionality**
* Manage many repositories with one CLI: `clone`, `update`, `install`, `list`, `path`, `config`
* Proxy wrappers for Git, Docker/Compose and Make
* Multi-repo execution with safe *preview mode*
* Mirror management: `mirror list/diff/merge/setup`
**Releases & Versioning**
* Automated SemVer bumps, tagging and changelog generation
* Supports PKGBUILD, Debian, RPM, pyproject.toml, flake.nix
**Developer Tools**
* Open repositories in VS Code, file manager or terminal
* Unified workflows across all major Linux distros
**Nix Integration**
* Cross-distro reproducible builds via Nix flakes
* CI-tested across all supported environments
---
**Summary**
PKGMGR 1.0.0 unifies repository management, build tooling, release automation and reproducible multi-distro workflows into one cohesive CLI tool.
*This is the first official stable release.*
## [0.10.2] - 2025-12-11
* * Stable tag now updates only when a new highest version is released.
* Debian package now includes sudo to ensure privilege escalation works reliably.
* Nix setup is significantly more resilient with retries, correct permissions, and better environment handling.
* AUR builder setup uses retries so yay installs succeed even under network instability.
* Nix flake installation now fails only on mandatory parts; optional outputs no longer block installation.
## [0.10.1] - 2025-12-11
* Fixed Debian\Ubuntu to pass container e2e tests
## [0.10.0] - 2025-12-11
**Mirror System**
* Added SSH mirror support including multi-push and remote probing
* Introduced mirror management commands and refactored the CLI parser into modules
**CI/CD**
* Migrated to reusable workflows with improved debugging instrumentation
* Made stable-tag automation reliable for workflow_run events and permissions
* Ensured deterministic test results by rebuilding all test containers with no-cache
**E2E and Container Tests**
* Fixed Git safe.directory handling across all containers
* Restored Dockerfile ENTRYPOINT to resolve Nix TLS issues
* Fixed missing volume errors and hardened the E2E runner
* Added full Nix flake E2E test matrix across all distro containers
* Disabled Nix sandboxing for cross-distro builds where required
**Nix and Python Environment**
* Unified Nix Python environment and introduced lazy CLI imports
* Ensured PyYAML availability and improved Python 3.13 compatibility
* Refactored flake.nix to remove side effects and rely on generic python3
**Packaging**
* Removed Debians hard dependency on Nix
* Restructured packaging layout and refined build paths
* Excluded assets from Arch PKGBUILD rsync
* Cleaned up obsolete ignore files
**Repository Layout**
* Restructured repository to align local, Nix-based, and distro-based build workflows
* Added Arch support and refined build/purge scripts
## [0.9.1] - 2025-12-10
* * 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.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`.
## [0.9.0] - 2025-12-10
* Introduce a virgin Arch-based Nix flake E2E workflow that validates pkgmgrs full flake installation path using shared caches for faster and reproducible CI runs.
## [0.8.0] - 2025-12-10
* **v0.7.15 — Installer & Command Resolution Improvements**
* Introduced a unified **layer-based installer pipeline** with clear precedence (OS-packages, Nix, Python, Makefile).
* Reworked installer structure and improved Python/Nix/Makefile installers, including isolated Python venvs and refined flake-output handling.
* Fully rewrote **command resolution** with stronger typing, safer fallbacks, and explicit support for `command: null` to mark library-only repositories.
* Added extensive **unit and integration tests** for installer capability ordering, command resolution, and Nix/Python installer behavior.
* Expanded documentation with capability hierarchy diagrams and scenario matrices.
* Removed deprecated repository entries and obsolete configuration files.
## [0.7.14] - 2025-12-10
* Fixed the clone-all integration test so that `SystemExit(0)` from the proxy is treated as a successful command instead of a failure.
## [0.7.13] - 2025-12-10
### Fix tools path resolution and add tests
- Fixed a crash in `pkgmgr code` caused by missing `directory` metadata by introducing `_resolve_repository_path()` with proper fallbacks to `repositories_base_dir` / `repositories_dir`.
- Updated `explore`, `terminal` and `code` tool commands to use the new resolver.
- Improved VS Code workspace generation and path handling.
- Added unit & E2E tests for tool commands.
## [0.7.12] - 2025-12-09
* Fixed self refering alias during setup
## [0.7.11] - 2025-12-09
* test: fix installer unit tests for OS packages and Nix dev shell
## [0.7.10] - 2025-12-09
* Fixed test_install_pkgmgr_shallow.py
## [0.7.9] - 2025-12-09
* 'main' and 'master' are now both accepted as branches for branch close merge
## [0.7.8] - 2025-12-09
* Missing pyproject.toml doesn't lead to an error during release
## [0.7.7] - 2025-12-09
* Added TEST_PATTERN parameter to execute dedicated tests
## [0.7.6] - 2025-12-09
* Fixed pull --preview bug in e2e test
## [0.7.5] - 2025-12-09
* Fixed wrong directory permissions for nix
## [0.7.4] - 2025-12-09
* Fixed missing build in test workflow -> Tests pass now
## [0.7.3] - 2025-12-09
* Fixed bug: Ignored packages are now ignored
## [0.7.2] - 2025-12-09
* Implemented Changelog Support for Fedora and Debian
## [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).
## [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)
## [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.
## [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)
## [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)

View File

@@ -1,198 +1,58 @@
# ------------------------------------------------------------
# Base image selector — overridden by Makefile
# ------------------------------------------------------------
ARG BASE_IMAGE=archlinux:latest
FROM ${BASE_IMAGE}
# syntax=docker/dockerfile:1
# ------------------------------------------------------------
# System base + conditional package tool installation
#
# Important:
# - We do NOT install Nix directly here via curl.
# - Nix is installed/initialized by init-nix.sh, which is invoked
# from the system packaging hooks (Arch .install, Debian postinst,
# RPM %post).
# Base image selector — overridden by build args / Makefile
# ------------------------------------------------------------
RUN set -e; \
if [ -f /etc/os-release ]; then . /etc/os-release; else echo "No /etc/os-release found" && exit 1; fi; \
echo "Detected base image: ${ID:-unknown} (like: ${ID_LIKE:-})"; \
\
if [ "$ID" = "arch" ]; then \
pacman -Syu --noconfirm && \
pacman -S --noconfirm --needed \
base-devel \
git \
rsync \
curl \
ca-certificates \
xz && \
pacman -Scc --noconfirm; \
elif [ "$ID" = "debian" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
debhelper \
dpkg-dev \
git \
rsync \
bash \
curl \
ca-certificates \
xz-utils && \
rm -rf /var/lib/apt/lists/*; \
elif [ "$ID" = "ubuntu" ]; then \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
debhelper \
dpkg-dev \
git \
tzdata \
lsb-release \
rsync \
bash \
curl \
ca-certificates \
xz-utils && \
rm -rf /var/lib/apt/lists/*; \
elif [ "$ID" = "fedora" ]; then \
dnf -y update && \
dnf -y install \
git \
rsync \
rpm-build \
make \
gcc \
bash \
curl \
ca-certificates \
xz && \
dnf clean all; \
elif [ "$ID" = "centos" ]; then \
dnf -y update && \
dnf -y install \
git \
rsync \
rpm-build \
make \
gcc \
bash \
curl-minimal \
ca-certificates \
xz && \
dnf clean all; \
else \
echo "Unsupported base image: ${ID}" && exit 1; \
fi
ARG BASE_IMAGE
# ------------------------------------------------------------
# Nix environment defaults
#
# Nix itself is installed by your system packages (via init-nix.sh).
# Here we only define default configuration options.
# ------------------------------------------------------------
# ============================================================
# Target: virgin
# - installs distro deps (incl. make)
# - no pkgmgr build
# - no entrypoint
# ============================================================
FROM ${BASE_IMAGE} AS virgin
SHELL ["/bin/bash", "-lc"]
RUN echo "BASE_IMAGE=${BASE_IMAGE}" && cat /etc/os-release || true
WORKDIR /build
# Copy scripts first so dependency installation can be cached
COPY scripts/installation/ scripts/installation/
# Install distro-specific build dependencies (including make)
RUN bash scripts/installation/dependencies.sh
# Virgin default
CMD ["bash"]
# ============================================================
# Target: full
# - inherits from virgin
# - builds + installs pkgmgr
# - sets entrypoint + default cmd
# ============================================================
FROM virgin AS full
# Nix environment defaults (only config; nix itself comes from deps/install flow)
ENV NIX_CONFIG="experimental-features = nix-command flakes"
# ------------------------------------------------------------
# Unprivileged user for Arch package build (makepkg)
# ------------------------------------------------------------
RUN useradd -m builder || true
# ------------------------------------------------------------
# Build and install distro-native package-manager package
#
# - Arch: PKGBUILD -> pacman -U
# - Debian: debhelper -> dpkg-buildpackage -> apt install ./package-manager_*.deb
# - Ubuntu: same as Debian
# - Fedora: rpmbuild -> dnf/dnf5/yum install package-manager-*.rpm
# - CentOS: rpmbuild -> dnf/yum install package-manager-*.rpm
#
# Nix is NOT manually installed here; it is handled by init-nix.sh.
# ------------------------------------------------------------
WORKDIR /build
# Copy full repository for build
COPY . .
RUN set -e; \
. /etc/os-release; \
if [ "$ID" = "arch" ]; then \
echo 'Building Arch package (makepkg --nodeps)...'; \
chown -R builder:builder /build; \
su builder -c "cd /build && rm -f package-manager-*.pkg.tar.* && makepkg --noconfirm --clean --nodeps"; \
\
echo 'Installing generated Arch package...'; \
pacman -U --noconfirm package-manager-*.pkg.tar.*; \
elif [ "$ID" = "debian" ] || [ "$ID" = "ubuntu" ]; then \
echo 'Building Debian/Ubuntu package...'; \
dpkg-buildpackage -us -uc -b; \
\
echo 'Installing generated DEB package...'; \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y ./../package-manager_*.deb && \
rm -rf /var/lib/apt/lists/*; \
elif [ "$ID" = "fedora" ] || [ "$ID" = "centos" ]; then \
echo 'Setting up rpmbuild dirs...'; \
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}; \
\
echo "Extracting version from package-manager.spec..."; \
version=$(grep -E '^Version:' /build/package-manager.spec | awk '{print $2}'); \
if [ -z "$version" ]; then echo 'ERROR: Version missing!' && exit 1; fi; \
srcdir="package-manager-${version}"; \
\
echo "Preparing source tree for RPM: $srcdir"; \
rm -rf "/tmp/$srcdir"; \
mkdir -p "/tmp/$srcdir"; \
cp -a /build/. "/tmp/$srcdir/"; \
\
echo "Creating source tarball: /root/rpmbuild/SOURCES/$srcdir.tar.gz"; \
tar czf "/root/rpmbuild/SOURCES/$srcdir.tar.gz" -C /tmp "$srcdir"; \
\
echo 'Copying SPEC...'; \
cp /build/package-manager.spec /root/rpmbuild/SPECS/; \
\
echo 'Running rpmbuild...'; \
cd /root/rpmbuild/SPECS && rpmbuild -bb package-manager.spec; \
\
echo 'Installing generated RPM (local, offline)...'; \
rpm_path=$(find /root/rpmbuild/RPMS -name "package-manager-*.rpm" | head -n1); \
if [ -z "$rpm_path" ]; then echo 'ERROR: RPM not found!' && exit 1; fi; \
\
if command -v dnf5 >/dev/null 2>&1; then \
echo 'Using dnf5 to install local RPM (no remote repos)...'; \
if ! dnf5 install -y --disablerepo='*' "$rpm_path"; then \
echo 'dnf5 failed, falling back to rpm -i --nodeps'; \
rpm -i --nodeps "$rpm_path"; \
fi; \
elif command -v dnf >/dev/null 2>&1; then \
echo 'Using dnf to install local RPM (no remote repos)...'; \
if ! dnf install -y --disablerepo='*' "$rpm_path"; then \
echo 'dnf failed, falling back to rpm -i --nodeps'; \
rpm -i --nodeps "$rpm_path"; \
fi; \
elif command -v yum >/dev/null 2>&1; then \
echo 'Using yum to install local RPM (no remote repos)...'; \
if ! yum localinstall -y --disablerepo='*' "$rpm_path"; then \
echo 'yum failed, falling back to rpm -i --nodeps'; \
rpm -i --nodeps "$rpm_path"; \
fi; \
else \
echo 'No dnf/dnf5/yum found, falling back to rpm -i --nodeps...'; \
rpm -i --nodeps "$rpm_path"; \
fi; \
\
rm -rf "/tmp/$srcdir"; \
else \
echo "Unsupported distro: ${ID}"; \
exit 1; \
fi; \
rm -rf /build
# Build and install distro-native package-manager package
RUN set -euo pipefail; \
echo "Building and installing package-manager via make install..."; \
make install; \
cd /; rm -rf /build
# Entry point
COPY scripts/docker/entry.sh /usr/local/bin/docker-entry.sh
# ------------------------------------------------------------
# Runtime working directory and dev entrypoint
# ------------------------------------------------------------
WORKDIR /src
COPY scripts/docker-entry-dev.sh /usr/local/bin/docker-entry-dev.sh
RUN chmod +x /usr/local/bin/docker-entry-dev.sh
ENTRYPOINT ["/usr/local/bin/docker-entry-dev.sh"]
CMD ["--help"]
ENTRYPOINT ["/usr/local/bin/docker-entry.sh"]
CMD ["pkgmgr", "--help"]

3
MIRRORS Normal file
View File

@@ -0,0 +1,3 @@
git@github.com:kevinveenbirkenbach/package-manager.git
ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git
ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git

356
Makefile
View File

@@ -1,275 +1,111 @@
.PHONY: install setup uninstall aur_builder_setup \
test build build-no-cache test-unit test-e2e test-integration
.PHONY: install uninstall \
build build-no-cache build-no-cache-all build-missing \
delete-volumes purge \
test test-unit test-e2e test-integration test-env-virtual test-env-nix \
setup setup-venv setup-nix
# Distro
# Options: arch debian ubuntu fedora centos
DISTROS ?= arch debian ubuntu fedora centos
distro ?= arch
export distro
# ------------------------------------------------------------
# Local Nix cache directories in the repo
# Base images
# (kept for documentation/reference; actual build logic is in scripts/build)
# ------------------------------------------------------------
NIX_STORE_VOLUME := pkgmgr_nix_store
NIX_CACHE_VOLUME := pkgmgr_nix_cache
BASE_IMAGE_ARCH := archlinux:latest
BASE_IMAGE_DEBIAN := debian:stable-slim
BASE_IMAGE_UBUNTU := ubuntu:latest
BASE_IMAGE_FEDORA := fedora:latest
BASE_IMAGE_CENTOS := quay.io/centos/centos:stream9
# Make them available in scripts
export BASE_IMAGE_ARCH
export BASE_IMAGE_DEBIAN
export BASE_IMAGE_UBUNTU
export BASE_IMAGE_FEDORA
export BASE_IMAGE_CENTOS
# PYthon Unittest Pattern
TEST_PATTERN := test_*.py
export TEST_PATTERN
# ------------------------------------------------------------
# Distro list and base images
# ------------------------------------------------------------
DISTROS := arch debian ubuntu fedora centos
BASE_IMAGE_arch := archlinux:latest
BASE_IMAGE_debian := debian:stable-slim
BASE_IMAGE_ubuntu := ubuntu:latest
BASE_IMAGE_fedora := fedora:latest
BASE_IMAGE_centos := quay.io/centos/centos:stream9
# Helper to echo which image is used for which distro (purely informational)
define echo_build_info
@echo "Building image for distro '$(1)' with base image '$(2)'..."
endef
# ------------------------------------------------------------
# PKGMGR setup (wrapper)
# ------------------------------------------------------------
setup: install
@echo "Running pkgmgr setup via main.py..."
@if [ -x "$$HOME/.venvs/pkgmgr/bin/python" ]; then \
echo "Using virtualenv Python at $$HOME/.venvs/pkgmgr/bin/python"; \
"$$HOME/.venvs/pkgmgr/bin/python" main.py install; \
else \
echo "Virtualenv not found, falling back to system python3"; \
python3 main.py install; \
fi
# ------------------------------------------------------------
# Docker build targets: build all images
# ------------------------------------------------------------
build-no-cache:
@for distro in $(DISTROS); do \
case "$$distro" in \
arch) base_image="$(BASE_IMAGE_arch)" ;; \
debian) base_image="$(BASE_IMAGE_debian)" ;; \
ubuntu) base_image="$(BASE_IMAGE_ubuntu)" ;; \
fedora) base_image="$(BASE_IMAGE_fedora)" ;; \
centos) base_image="$(BASE_IMAGE_centos)" ;; \
*) echo "Unknown distro '$$distro'" >&2; exit 1 ;; \
esac; \
echo "Building test image 'package-manager-test-$$distro' with no cache (BASE_IMAGE=$$base_image)..."; \
docker build --no-cache \
--build-arg BASE_IMAGE="$$base_image" \
-t "package-manager-test-$$distro" . || exit $$?; \
done
build:
@for distro in $(DISTROS); do \
case "$$distro" in \
arch) base_image="$(BASE_IMAGE_arch)" ;; \
debian) base_image="$(BASE_IMAGE_debian)" ;; \
ubuntu) base_image="$(BASE_IMAGE_ubuntu)" ;; \
fedora) base_image="$(BASE_IMAGE_fedora)" ;; \
centos) base_image="$(BASE_IMAGE_centos)" ;; \
*) echo "Unknown distro '$$distro'" >&2; exit 1 ;; \
esac; \
echo "Building test image 'package-manager-test-$$distro' (BASE_IMAGE=$$base_image)..."; \
docker build \
--build-arg BASE_IMAGE="$$base_image" \
-t "package-manager-test-$$distro" . || exit $$?; \
done
build-arch:
@base_image="$(BASE_IMAGE_arch)"; \
echo "Building test image 'package-manager-test-arch' (BASE_IMAGE=$$base_image)..."; \
docker build \
--build-arg BASE_IMAGE="$$base_image" \
-t "package-manager-test-arch" . || exit $$?;
# ------------------------------------------------------------
# Test targets
# ------------------------------------------------------------
# Unit tests: only in Arch container (fastest feedback), via Nix devShell
test-unit: build-arch
@echo "============================================================"
@echo ">>> Running UNIT tests in Arch container (via Nix devShell)"
@echo "============================================================"
docker run --rm \
-v "$$(pwd):/src" \
--workdir /src \
--entrypoint bash \
"package-manager-test-arch" \
-c '\
set -e; \
if [ -f /etc/os-release ]; then . /etc/os-release; fi; \
echo "Detected container distro: $${ID:-unknown} (like: $${ID_LIKE:-})"; \
echo "Running Python unit tests (tests/unit) via nix develop..."; \
git config --global --add safe.directory /src || true; \
cd /src; \
nix develop .#default --no-write-lock-file -c \
python -m unittest discover \
-s tests/unit \
-t /src \
-p "test_*.py"; \
'
# Integration tests: also in Arch container, via Nix devShell
test-integration: build-arch
@echo "============================================================"
@echo ">>> Running INTEGRATION tests in Arch container (via Nix devShell)"
@echo "============================================================"
docker run --rm \
-v "$$(pwd):/src" \
--workdir /src \
--entrypoint bash \
"package-manager-test-arch" \
-c '\
set -e; \
if [ -f /etc/os-release ]; then . /etc/os-release; fi; \
echo "Detected container distro: $${ID:-unknown} (like: $${ID_LIKE:-})"; \
echo "Running Python integration tests (tests/integration) via nix develop..."; \
git config --global --add safe.directory /src || true; \
cd /src; \
nix develop .#default --no-write-lock-file -c \
python -m unittest discover \
-s tests/integration \
-t /src \
-p "test_*.py"; \
'
# End-to-end tests: run in all distros via Nix devShell (tests/e2e)
test-e2e: build
@echo "Ensuring Docker Nix volumes exist (auto-created if missing)..."
@echo "Running E2E tests inside Nix devShell with cached store for all distros: $(DISTROS)"
@for distro in $(DISTROS); do \
echo "============================================================"; \
echo ">>> Running E2E tests in container for distro: $$distro"; \
echo "============================================================"; \
# Only for Arch: mount /nix as volume, for others use image-installed Nix \
if [ "$$distro" = "arch" ]; then \
NIX_STORE_MOUNT="-v $(NIX_STORE_VOLUME):/nix"; \
else \
NIX_STORE_MOUNT=""; \
fi; \
docker run --rm \
-v "$$(pwd):/src" \
$$NIX_STORE_MOUNT \
-v "$(NIX_CACHE_VOLUME):/root/.cache/nix" \
--workdir /src \
--entrypoint bash \
"package-manager-test-$$distro" \
-c '\
set -e; \
if [ -f /etc/os-release ]; then . /etc/os-release; fi; \
echo "Detected container distro: $${ID:-unknown} (like: $${ID_LIKE:-})"; \
echo "Preparing Nix environment..."; \
if [ -f "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" ]; then \
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"; \
fi; \
if [ -f "$$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then \
. "$$HOME/.nix-profile/etc/profile.d/nix.sh"; \
fi; \
PATH="/nix/var/nix/profiles/default/bin:$$HOME/.nix-profile/bin:$$PATH"; \
export PATH; \
echo "PATH is now:"; \
echo "$$PATH"; \
NIX_CMD=""; \
if command -v nix >/dev/null 2>&1; then \
echo "Found nix on PATH:"; \
command -v nix; \
NIX_CMD="nix"; \
else \
echo "nix not found on PATH, scanning /nix/store for a nix binary..."; \
for path in /nix/store/*-nix-*/bin/nix; do \
if [ -x "$$path" ]; then \
echo "Found nix binary at $$path"; \
NIX_CMD="$$path"; \
break; \
fi; \
done; \
fi; \
if [ -z "$$NIX_CMD" ]; then \
echo "ERROR: nix binary not found anywhere cannot run devShell"; \
exit 1; \
fi; \
echo "Using Nix command: $$NIX_CMD"; \
echo "Run E2E tests inside Nix devShell (tests/e2e)..."; \
git config --global --add safe.directory /src || true; \
cd /src; \
"$$NIX_CMD" develop .#default --no-write-lock-file -c \
python3 -m unittest discover \
-s /src/tests/e2e \
-p "test_*.py"; \
' || exit $$?; \
done
# Combined test target for local + CI (unit + e2e + integration)
test: build test-unit test-e2e test-integration
# ------------------------------------------------------------
# Installer for host systems (original logic)
# System install
# ------------------------------------------------------------
install:
@if [ -n "$$IN_NIX_SHELL" ]; then \
echo "Nix shell detected (IN_NIX_SHELL=1). Skipping venv/pip install handled by Nix flake."; \
else \
echo "Making 'main.py' executable..."; \
chmod +x main.py; \
echo "Checking if global user virtual environment exists..."; \
mkdir -p "$$HOME/.venvs"; \
if [ ! -d "$$HOME/.venvs/pkgmgr" ]; then \
echo "Creating global venv at $$HOME/.venvs/pkgmgr..."; \
python3 -m venv "$$HOME/.venvs/pkgmgr"; \
fi; \
echo "Installing required Python packages into $$HOME/.venvs/pkgmgr..."; \
"$$HOME/.venvs/pkgmgr/bin/python" -m ensurepip --upgrade; \
"$$HOME/.venvs/pkgmgr/bin/pip" install --upgrade pip setuptools wheel; \
echo "Looking for requirements.txt / _requirements.txt..."; \
if [ -f requirements.txt ]; then \
echo "Installing Python packages from requirements.txt..."; \
"$$HOME/.venvs/pkgmgr/bin/pip" install -r requirements.txt; \
elif [ -f _requirements.txt ]; then \
echo "Installing Python packages from _requirements.txt..."; \
"$$HOME/.venvs/pkgmgr/bin/pip" install -r _requirements.txt; \
else \
echo "No requirements.txt or _requirements.txt found, skipping dependency installation."; \
fi; \
echo "Ensuring $$HOME/.bashrc and $$HOME/.zshrc exist..."; \
touch "$$HOME/.bashrc" "$$HOME/.zshrc"; \
echo "Ensuring automatic activation of $$HOME/.venvs/pkgmgr for this user..."; \
for rc in "$$HOME/.bashrc" "$$HOME/.zshrc"; do \
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'; \
grep -qxF "$${rc_line}" "$$rc" || echo "$${rc_line}" >> "$$rc"; \
done; \
echo "Arch/Manjaro detection and optional AUR setup..."; \
if command -v pacman >/dev/null 2>&1; then \
$(MAKE) aur_builder_setup; \
else \
echo "Not Arch-based (no pacman). Skipping aur_builder/yay setup."; \
fi; \
echo "Installation complete. Please restart your shell (or 'exec bash' or 'exec zsh') for the changes to take effect."; \
fi
@echo "Building and installing distro-native package-manager for this system..."
@bash scripts/installation/main.sh
# ------------------------------------------------------------
# AUR builder setup — only on Arch/Manjaro
# PKGMGR setup
# ------------------------------------------------------------
aur_builder_setup:
@echo "Setting up aur_builder and yay (Arch/Manjaro)..."
@sudo pacman -Syu --noconfirm
@sudo pacman -S --needed --noconfirm base-devel git sudo
@if ! getent group aur_builder >/dev/null; then sudo groupadd -r aur_builder; fi
@if ! id -u aur_builder >/dev/null 2>&1; then sudo useradd -m -r -g aur_builder -s /bin/bash aur_builder; fi
@echo '%aur_builder ALL=(ALL) NOPASSWD: /usr/bin/pacman' | sudo tee /etc/sudoers.d/aur_builder >/dev/null
@sudo chmod 0440 /etc/sudoers.d/aur_builder
@if ! sudo -u aur_builder bash -lc 'command -v yay >/dev/null'; then \
sudo -u aur_builder bash -lc 'cd ~ && rm -rf yay && git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -si --noconfirm'; \
else \
echo "yay already installed."; \
fi
@echo "aur_builder/yay setup complete."
# Default: keep current auto-detection behavior
setup: setup-nix setup-venv
# Explicit: developer setup (Python venv + shell RC + main.py install)
setup-venv: setup-nix
@bash scripts/setup/venv.sh
# Explicit: Nix shell mode (no venv, no RC changes)
setup-nix:
@bash scripts/setup/nix.sh
# ------------------------------------------------------------
# Docker build targets (delegated to scripts/build)
# ------------------------------------------------------------
build:
@bash scripts/build/image.sh --target virgin
@bash scripts/build/image.sh
build-missing-virgin:
@bash scripts/build/image.sh --target virgin --missing
build-missing: build-missing-virgin
@bash scripts/build/image.sh --missing
build-no-cache:
@bash scripts/build/image.sh --target virgin --no-cache
@bash scripts/build/image.sh --no-cache
build-no-cache-all:
@set -e; \
for d in $(DISTROS); do \
echo "=== build-no-cache: $$d ==="; \
distro="$$d" $(MAKE) build-no-cache; \
done
# ------------------------------------------------------------
# Test targets (delegated to scripts/test)
# ------------------------------------------------------------
test-unit: build-missing
@bash scripts/test/test-unit.sh
test-integration: build-missing
@bash scripts/test/test-integration.sh
test-e2e: build-missing
@bash scripts/test/test-e2e.sh
test-env-virtual: build-missing
@bash scripts/test/test-env-virtual.sh
test-env-nix: build-missing
@bash scripts/test/test-env-nix.sh
# Combined test target for local + CI (unit + integration + e2e)
test: test-env-virtual test-unit test-integration test-e2e
delete-volumes:
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true
purge: delete-volumes build-no-cache
# ------------------------------------------------------------
# Uninstall target
# ------------------------------------------------------------
uninstall:
@echo "Removing global user virtual environment if it exists..."
@rm -rf "$$HOME/.venvs/pkgmgr"
@echo "Cleaning up $$HOME/.bashrc and $$HOME/.zshrc entries..."
@for rc in "$$HOME/.bashrc" "$$HOME/.zshrc"; do \
sed -i '/\.venvs\/pkgmgr\/bin\/activate"; if \[ -n "\$${PS1:-}" \]; then echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi; fi/d' "$$rc"; \
done
@echo "Uninstallation complete. Please restart your shell (or 'exec bash' or 'exec zsh') for the changes to fully apply."
@bash scripts/uninstall.sh

216
README.md
View File

@@ -1,116 +1,188 @@
# Package Manager🤖📦
# Package Manager 🤖📦
![PKGMGR Banner](assets/banner.jpg)
[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-blue?logo=github)](https://github.com/sponsors/kevinveenbirkenbach)
[![Patreon](https://img.shields.io/badge/Support-Patreon-orange?logo=patreon)](https://www.patreon.com/c/kevinveenbirkenbach)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20Coffee-Funding-yellow?logo=buymeacoffee)](https://buymeacoffee.com/kevinveenbirkenbach) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](https://s.veen.world/paypaldonate)
[![Patreon](https://img.shields.io/badge/Support-Patreon-orange?logo=patreon)](https://www.patreon.com/c/kevinveenbirkenbach)
[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20Coffee-Funding-yellow?logo=buymeacoffee)](https://buymeacoffee.com/kevinveenbirkenbach)
[![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](https://s.veen.world/paypaldonate)
[![GitHub license](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![GitHub repo size](https://img.shields.io/github/repo-size/kevinveenbirkenbach/package-manager)](https://github.com/kevinveenbirkenbach/package-manager)
*Kevins's* Package Manager is a configurable Python tool designed to manage multiple repositories via Bash. It automates common Git operations such as clone, pull, push, status, and more. Additionally, it handles the creation of executable wrappers and alias links for your repositories.
**Kevin's Package Manager (PKGMGR)** is a *multi-distro* package manager and workflow orchestrator.
It helps you **develop, package, release and manage projects across multiple Linux-based
operating systems** (Arch, Debian, Ubuntu, Fedora, CentOS, …).
PKGMGR is implemented in **Python** and uses **Nix (flakes)** as a foundation for
distribution-independent builds and tooling. On top of that it provides a rich
CLI that proxies common developer tools (Git, Docker, Make, …) and glues them
together into repeatable development workflows.
---
## Why PKGMGR? 🧠
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
single operating system. PKGMGR instead focuses on **your repositories and
development lifecycle**:
* one configuration for all your repos,
* one CLI to interact with them,
* one Nix-based layer to keep tooling reproducible across distros.
You keep using your native package manager where it makes sense PKGMGR
coordinates the *development and release flow* around it.
---
## Features 🚀
- **Installation & Setup:**
Create executable wrappers with auto-detected commands (e.g. `main.sh` or `main.py`).
- **Git Operations:**
Easily perform `git pull`, `push`, `status`, `commit`, `diff`, `add`, `show`, and `checkout` with extra parameters passed through.
- **Configuration Management:**
Manage repository configurations via a default file (`config/defaults.yaml`) and a user-specific file (`config/config.yaml`). Initialize, add, delete, or ignore entries using subcommands.
- **Path & Listing:**
Display repository paths or list all configured packages with their details.
- **Custom Aliases:**
Generate and manage custom aliases for easy command invocation.
### Multi-distro development & packaging
* Manage **many repositories at once** from a single `config/config.yaml`.
* Drive full **release pipelines** across Linux distributions using:
* 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
PKGMGR is not just a helper around Git commands. Combined with its release and
versioning features it can drive **end-to-end workflows**:
1. Clone and mirror repositories.
2. Run tests and builds through `make` or Nix.
3. Bump versions, update changelogs and tags.
4. Build distro-specific packages.
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 🗺️
The following diagram gives a full overview of:
* PKGMGRs package structure,
* the layered installers (OS, foundation, Python, Makefile),
* and the setup controller that decides which layer to use on a given system.
![PKGMGR Architecture](assets/map.png)
**Diagram status:** 12 December 2025
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
---
## Installation ⚙️
Clone the repository and ensure your `~/.local/bin` is in your system PATH:
### 1. Get the latest stable version
For a stable setup, use the **latest tagged release** (the tag pointed to by
`latest`):
```bash
git clone https://github.com/kevinveenbirkenbach/package-manager.git
cd package-manager
# Optional but recommended: checkout the latest stable tag
git fetch --tags
git checkout "$(git describe --tags --abbrev=0)"
```
Install make and pip if not installed yet:
### 2. Install via Make
The project ships with a Makefile that encapsulates the typical installation
flow. On most systems you only need:
```bash
pacman -S make python-pip
# Ensure make, Python and pip are installed via your distro package manager
# (e.g. pacman -S make python python-pip, apt install make python3-pip, ...)
make install
```
Then, run the following command to set up the project:
This will:
* create or reuse a Python virtual environment,
* install PKGMGR (and its Python dependencies) into that environment,
* expose the `pkgmgr` executable on your PATH (usually via `~/.local/bin`),
* prepare Nix-based integration where available so PKGMGR can build and manage
packages distribution-independently.
For development use, you can also run:
```bash
make setup
```
The `make setup` command will:
- Make `main.py` executable.
- Install required packages from `requirements.txt`.
- Execute `python main.py install` to complete the installation.
which prepares the environment and leaves you with a fully wired development
workspace (including Nix, tests and scripts).
## Docker Quickstart 🐳
---
Alternatively to installing locally, you can use Docker: build the image with
## Usage 🧰
After installation, the main entry point is:
```bash
docker build --no-cache -t pkgmgr .
pkgmgr --help
```
or alternativ pull it via
This prints a list of all available subcommands, for example:
* `pkgmgr list --all` show all repositories in the config
* `pkgmgr update --all --clone-mode https` update every repository
* `pkgmgr release patch --preview` simulate a patch release
* `pkgmgr version --all` show version information for all repositories
* `pkgmgr mirror setup --preview --all` prepare Git mirrors (no changes in preview)
* `pkgmgr make install --preview pkgmgr` preview make install for the pkgmgr repo
The help for each command is available via:
```bash
docker pull kevinveenbirkenbach/pkgmgr:latest
pkgmgr <command> --help
```
and then run
```bash
docker run --rm pkgmgr --help
```
## Usage 📖
Run the script with different commands. For example:
- **Install all packages:**
```bash
pkgmgr install --all
```
- **Pull updates for a specific repository:**
```bash
pkgmgr pull pkgmgr
```
- **Commit changes with extra Git parameters:**
```bash
pkgmgr commit pkgmgr -- -m "Your commit message"
```
- **List all configured packages:**
```bash
pkgmgr config show
```
- **Manage configuration:**
```bash
pkgmgr config init
pkgmgr config add
pkgmgr config edit
pkgmgr config delete <identifier>
pkgmgr config ignore <identifier> --set true
```
---
## License 📄
This project is licensed under the MIT License.
## Author 👤
Kevin Veen-Birkenbach
[https://www.veen.world](https://www.veen.world)
See the [LICENSE](LICENSE) file for details.
---
**Repository:** [github.com/kevinveenbirkenbach/package-manager](https://github.com/kevinveenbirkenbach/package-manager)
## Author 👤
*Created with AI 🤖 - [View conversation](https://chatgpt.com/share/67c728c4-92d0-800f-8945-003fa9bf27c6)*
Kevin Veen-Birkenbach
[https://www.veen.world](https://www.veen.world)

6
TODO.md Normal file
View File

@@ -0,0 +1,6 @@
# to-dos
For the following checkout the implementation map:
- Implement TAGS
- Implement SIGNING_KEY

View File

@@ -1,4 +0,0 @@
# Legacy file used only if pip still installs from requirements.txt.
# You may delete this file once you switch entirely to pyproject.toml.
PyYAML

BIN
assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
assets/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -380,17 +380,6 @@ repositories:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach
alias: infinito-presentation
description: This repository contains a Infinito.Nexus presentation designed for customers, end-users, investors, developers, and administrators, offering tailored content and insights for each group.
homepage: https://github.com/kevinveenbirkenbach/infinito-presentation
provider: github.com
repository: infinito-presentation
verified:
gpg_keys:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach
description: A lightweight Python utility to generate dynamic color schemes from a single base color. Provides HSL-based color transformations for theming, UI design, and CSS variable generation. Optimized for integration in Python projects, Flask applications, and Ansible roles.
homepage: https://github.com/kevinveenbirkenbach/colorscheme-generator
@@ -599,17 +588,6 @@ repositories:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach
desciption: Infinito Inventory Builder — a containerized web application that dynamically generates Ansible inventory files from invokable Infinito.Nexus roles through an interactive, browser-based interface.
homepage: https://github.com/kevinveenbirkenbach/infinito-inventory-builder
alias: invbuild
provider: github.com
repository: infinito-inventory-builder
verified:
gpg_keys:
- 44D8F11FD62F878E
- B5690EEEBB952194
- account: kevinveenbirkenbach
desciption: A simple Python CLI tool to safely rename Linux user accounts using usermod — including home directory migration and validation checks.
homepage: https://github.com/kevinveenbirkenbach/user-rename

View File

@@ -1,7 +0,0 @@
- account: kevinveenbirkenbach
alias: gkfdrtdtcntr
provider: github.com
repository: federated-to-central-social-network-bridge
verified:
gpg_keys:
- 44D8F11FD62F878E

67
debian/changelog vendored
View File

@@ -1,67 +0,0 @@
package-manager (0.5.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:44:16 +0100
package-manager (0.4.3-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:29:08 +0100
package-manager (0.4.2-1) unstable; urgency=medium
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:03:46 +0100
package-manager (0.4.1-1) unstable; urgency=medium
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 23:20:28 +0100
package-manager (0.4.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 23:02:43 +0100
package-manager (0.3.0-1) unstable; urgency=medium
* Massive refactor and feature expansion:
- Complete rewrite of config loading system (layered defaults + user config)
- New selection engine (--string, --category, --tag)
- Overhauled list output (colored statuses, alias highlight)
- New config update logic + default YAML sync
- Improved proxy command handling
- Full CLI routing refactor
- Expanded E2E tests for list, proxy, and selection logic
Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 22:40:49 +0100
package-manager (0.2.0-1) unstable; urgency=medium
* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 20:31:19 +0100
package-manager (0.1.0-1) unstable; urgency=medium
* Updated to correct version
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 20:24:49 +0100
package-manager (0.1.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 20:15:13 +0100
package-manager (0.1.1-1) unstable; urgency=medium
* Initial release.
-- Kevin Veen-Birkenbach <info@veen.world> Sat, 06 Dec 2025 16:30:00 +0100

14
debian/postinst vendored
View File

@@ -1,14 +0,0 @@
#!/bin/sh
set -e
case "$1" in
configure)
if [ -x /usr/lib/package-manager/init-nix.sh ]; then
/usr/lib/package-manager/init-nix.sh || true
else
echo ">>> Warning: /usr/lib/package-manager/init-nix.sh not found or not executable."
fi
;;
esac
exit 0

View File

@@ -26,12 +26,17 @@
packages = forAllSystems (system:
let
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;
pyPkgs = pkgs.python311Packages;
in
rec {
pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager";
version = "0.5.0";
version = "1.3.0";
# Use the git repo as source
src = ./.;
@@ -45,18 +50,17 @@
pyPkgs.wheel
];
# Runtime dependencies (matches [project.dependencies])
# Runtime dependencies (matches [project.dependencies] in pyproject.toml)
propagatedBuildInputs = [
pyPkgs.pyyaml
# Add more here if needed, e.g.:
# pyPkgs.click
# pyPkgs.rich
pyPkgs.pip
];
doCheck = false;
pythonImportsCheck = [ "pkgmgr" ];
};
default = pkgmgr;
}
);
@@ -67,23 +71,42 @@
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
pkgmgrPkg = self.packages.${system}.pkgmgr;
ansiblePkg =
if pkgs ? ansible-core then pkgs.ansible-core
else pkgs.ansible;
# Use the same Python version as the package (3.11)
python = pkgs.python311;
pythonWithDeps = python.withPackages (ps: [
ps.pip
ps.pyyaml
]);
in
{
default = pkgs.mkShell {
buildInputs = [
pkgmgrPkg
pythonWithDeps
pkgs.git
ansiblePkg
];
shellHook = ''
# Ensure our Python with dependencies is preferred on PATH
export PATH=${pythonWithDeps}/bin:$PATH
# Ensure src/ layout is importable:
# pkgmgr lives in ./src/pkgmgr
export PYTHONPATH="$PWD/src:${PYTHONPATH:-}"
# Also add repo root in case tools/tests rely on it
export PYTHONPATH="$PWD:$PYTHONPATH"
echo "Entered pkgmgr development shell for ${system}"
echo "pkgmgr CLI is available via the flake build"
echo "Python used in this shell:"
python --version
echo "pkgmgr CLI (from source) is available via:"
echo " python -m pkgmgr.cli --help"
'';
};
}

10
main.py
View File

@@ -1,6 +1,14 @@
#!/usr/bin/env python3
import sys
from pathlib import Path
# Ensure local src/ overrides installed package
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "src"
if SRC.is_dir():
sys.path.insert(0, str(SRC))
from pkgmgr.cli import main
if __name__ == "__main__":
main()
main()

View File

@@ -1,11 +0,0 @@
post_install() {
/usr/lib/package-manager/init-nix.sh || true
}
post_upgrade() {
/usr/lib/package-manager/init-nix.sh || true
}
post_remove() {
echo ">>> package-manager removed. Nix itself was not removed."
}

View File

@@ -1,80 +0,0 @@
Name: package-manager
Version: 0.5.0
Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake
License: MIT
URL: https://github.com/kevinveenbirkenbach/package-manager
Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
# NOTE:
# Nix is a runtime requirement, but it is *not* declared here as a hard
# RPM dependency, because many distributions do not ship a "nix" RPM.
# Instead, Nix is installed and initialized by init-nix.sh, which is
# called in the %post scriptlet below.
%description
This package provides the `pkgmgr` command, which runs Kevin's package
manager via a local Nix flake:
nix run /usr/lib/package-manager#pkgmgr -- ...
Nix is a runtime requirement and is installed/initialized by the
init-nix.sh helper during package installation if it is not yet
available on the system.
%prep
%setup -q
%build
# No build step required; we ship the project tree as-is.
:
%install
rm -rf %{buildroot}
install -d %{buildroot}%{_bindir}
install -d %{buildroot}%{_libdir}/package-manager
# Copy full project source into /usr/lib/package-manager
cp -a . %{buildroot}%{_libdir}/package-manager/
# Wrapper
install -m0755 scripts/pkgmgr-wrapper.sh %{buildroot}%{_bindir}/pkgmgr
# Shared Nix init script (ensure it is executable in the installed tree)
install -m0755 scripts/init-nix.sh %{buildroot}%{_libdir}/package-manager/init-nix.sh
# Remove packaging-only and development artefacts from the installed tree
rm -rf \
%{buildroot}%{_libdir}/package-manager/PKGBUILD \
%{buildroot}%{_libdir}/package-manager/Dockerfile \
%{buildroot}%{_libdir}/package-manager/debian \
%{buildroot}%{_libdir}/package-manager/.git \
%{buildroot}%{_libdir}/package-manager/.github \
%{buildroot}%{_libdir}/package-manager/tests \
%{buildroot}%{_libdir}/package-manager/.gitignore \
%{buildroot}%{_libdir}/package-manager/__pycache__ \
%{buildroot}%{_libdir}/package-manager/.gitkeep || true
%post
# Initialize Nix (if needed) after installing the package-manager files.
if [ -x %{_libdir}/package-manager/init-nix.sh ]; then
%{_libdir}/package-manager/init-nix.sh || true
else
echo ">>> Warning: %{_libdir}/package-manager/init-nix.sh not found or not executable."
fi
%postun
echo ">>> package-manager removed. Nix itself was not removed."
%files
%doc README.md
%license LICENSE
%{_bindir}/pkgmgr
%{_libdir}/package-manager/
%changelog
* Sat Dec 06 2025 Kevin Veen-Birkenbach <info@veen.world> - 0.1.1-1
- Initial RPM packaging for package-manager

6
packaging/arch/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# Arch pkg artifacts
*.pkg.tar.*
*.log
package-manager-*
src/
pkg/

View File

@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager
pkgver=0.5.0
pkgver=0.9.1
pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any')
@@ -15,7 +15,7 @@ makedepends=('rsync')
install=${pkgname}.install
# Local source checkout — avoids the tarball requirement.
# This assumes you build the package from inside the main project repository.
# We build from the project root (two levels above packaging/arch/).
source=()
sha256sums=()
@@ -24,12 +24,18 @@ _srcdir_name="source"
prepare() {
mkdir -p "$srcdir/$_srcdir_name"
local project_root
project_root="$(cd "$startdir/../.." && pwd)"
rsync -a \
--exclude=".git" \
--exclude=".github" \
--exclude="pkg" \
--exclude="srcpkg" \
"$startdir/" "$srcdir/$_srcdir_name/"
--exclude="packaging" \
--exclude="assets" \
"$project_root/" "$srcdir/$_srcdir_name/"
}
build() {
@@ -62,7 +68,8 @@ package() {
"$pkgdir/usr/lib/package-manager/PKGBUILD" \
"$pkgdir/usr/lib/package-manager/Dockerfile" \
"$pkgdir/usr/lib/package-manager/debian" \
"$pkgdir/usr/lib/package-manager/packaging" \
"$pkgdir/usr/lib/package-manager/.gitignore" \
"$pkgdir/usr/lib/package-manager/__pycache__" \
"$pkgdir/usr/lib/package-manager/.gitkeep"
"$pkgdir/usr/lib/package-manager/.gitkeep" || true
}

View File

@@ -0,0 +1,11 @@
post_install() {
/usr/lib/package-manager/init-nix.sh || echo ">>> ERROR: /usr/lib/package-manager/init-nix.sh not found or not executable."
}
post_upgrade() {
/usr/lib/package-manager/init-nix.sh || echo ">>> ERROR: /usr/lib/package-manager/init-nix.sh not found or not executable."
}
post_remove() {
echo ">>> package-manager removed. Nix itself was not removed."
}

6
packaging/debian/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
# debian
package-manager/
debhelper-build-stamp
files
.debhelper/
package-manager.substvars

197
packaging/debian/changelog Normal file
View File

@@ -0,0 +1,197 @@
package-manager (0.9.1-1) unstable; urgency=medium
* * 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.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 22:56:01 +0100
package-manager (0.9.0-1) unstable; urgency=medium
* Introduce a virgin Arch-based Nix flake E2E workflow that validates pkgmgrs full flake installation path using shared caches for faster and reproducible CI runs.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 18:38:07 +0100
package-manager (0.8.0-1) unstable; urgency=medium
* **v0.7.15 — Installer & Command Resolution Improvements**
* Introduced a unified **layer-based installer pipeline** with clear precedence (OS-packages, Nix, Python, Makefile).
* Reworked installer structure and improved Python/Nix/Makefile installers, including isolated Python venvs and refined flake-output handling.
* Fully rewrote **command resolution** with stronger typing, safer fallbacks, and explicit support for `command: null` to mark library-only repositories.
* Added extensive **unit and integration tests** for installer capability ordering, command resolution, and Nix/Python installer behavior.
* Expanded documentation with capability hierarchy diagrams and scenario matrices.
* Removed deprecated repository entries and obsolete configuration files.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 17:31:57 +0100
package-manager (0.7.14-1) unstable; urgency=medium
* Fixed the clone-all integration test so that `SystemExit(0)` from the proxy is treated as a successful command instead of a failure.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 10:38:33 +0100
package-manager (0.7.13-1) unstable; urgency=medium
* Automated release.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 10:27:24 +0100
package-manager (0.7.12-1) unstable; urgency=medium
* Fixed self refering alias during setup
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 23:36:35 +0100
package-manager (0.7.11-1) unstable; urgency=medium
* test: fix installer unit tests for OS packages and Nix dev shell
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 23:16:46 +0100
package-manager (0.7.10-1) unstable; urgency=medium
* Fixed test_install_pkgmgr_shallow.py
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 22:57:08 +0100
package-manager (0.7.9-1) unstable; urgency=medium
* 'main' and 'master' are now both accepted as branches for branch close merge
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 21:19:13 +0100
package-manager (0.7.8-1) unstable; urgency=medium
* Missing pyproject.toml doesn't lead to an error during release
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 21:03:24 +0100
package-manager (0.7.7-1) unstable; urgency=medium
* Added TEST_PATTERN parameter to execute dedicated tests
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 17:54:38 +0100
package-manager (0.7.6-1) unstable; urgency=medium
* Fixed pull --preview bug in e2e test
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 17:14:19 +0100
package-manager (0.7.5-1) unstable; urgency=medium
* Fixed wrong directory permissions for nix
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:45:42 +0100
package-manager (0.7.4-1) unstable; urgency=medium
* Fixed missing build in test workflow -> Tests pass now
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:22:00 +0100
package-manager (0.7.3-1) unstable; urgency=medium
* Fixed bug: Ignored packages are now ignored
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 16:08:31 +0100
package-manager (0.7.2-1) unstable; urgency=medium
* Implemented Changelog Support for Fedora and Debian
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 15:48:58 +0100
package-manager (0.7.1-1) unstable; urgency=medium
* 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).
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 15:26:54 +0100
package-manager (0.7.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 15:21:03 +0100
package-manager (0.6.0-1) unstable; urgency=medium
* 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.
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 05:59:58 +0100
package-manager (0.5.1-1) unstable; urgency=medium
* Refine pkgmgr release CLI close wiring and integration tests for --close flag (ChatGPT: https://chatgpt.com/share/69376b4e-8440-800f-9d06-535ec1d7a40e)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 01:21:31 +0100
package-manager (0.5.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:44:16 +0100
package-manager (0.4.3-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:29:08 +0100
package-manager (0.4.2-1) unstable; urgency=medium
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
-- Kevin Veen-Birkenbach <kevin@veen.world> Tue, 09 Dec 2025 00:03:46 +0100
package-manager (0.4.1-1) unstable; urgency=medium
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 23:20:28 +0100
package-manager (0.4.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 23:02:43 +0100
package-manager (0.3.0-1) unstable; urgency=medium
* Massive refactor and feature expansion:
- Complete rewrite of config loading system (layered defaults + user config)
- New selection engine (--string, --category, --tag)
- Overhauled list output (colored statuses, alias highlight)
- New config update logic + default YAML sync
- Improved proxy command handling
- Full CLI routing refactor
- Expanded E2E tests for list, proxy, and selection logic
Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 22:40:49 +0100
package-manager (0.2.0-1) unstable; urgency=medium
* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 20:31:19 +0100
package-manager (0.1.0-1) unstable; urgency=medium
* Updated to correct version
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 20:24:49 +0100
package-manager (0.1.0-1) unstable; urgency=medium
* 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)
-- Kevin Veen-Birkenbach <kevin@veen.world> Mon, 08 Dec 2025 20:15:13 +0100
package-manager (0.1.1-1) unstable; urgency=medium
* Initial release.
-- Kevin Veen-Birkenbach <info@veen.world> Sat, 06 Dec 2025 16:30:00 +0100

View File

@@ -9,7 +9,7 @@ Homepage: https://github.com/kevinveenbirkenbach/package-manager
Package: package-manager
Architecture: any
Depends: nix, ${misc:Depends}
Depends: sudo, ${misc:Depends}
Description: Wrapper that runs Kevin's package-manager via Nix flake
This package provides the `pkgmgr` command, which runs Kevin's package
manager via a local Nix flake

10
packaging/debian/postinst Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
set -e
case "$1" in
configure)
/usr/lib/package-manager/init-nix.sh || echo ">>> ERROR: /usr/lib/package-manager/init-nix.sh not found or not executable."
;;
esac
exit 0

View File

@@ -0,0 +1,134 @@
Name: package-manager
Version: 0.9.1
Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake
License: MIT
URL: https://github.com/kevinveenbirkenbach/package-manager
Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
# NOTE:
# Nix is a runtime requirement, but it is *not* declared here as a hard
# RPM dependency, because many distributions do not ship a "nix" RPM.
# Instead, Nix is installed and initialized by init-nix.sh, which is
# called in the %post scriptlet below.
%description
This package provides the `pkgmgr` command, which runs Kevin's package
manager via a local Nix flake:
nix run /usr/lib/package-manager#pkgmgr -- ...
Nix is a runtime requirement and is installed/initialized by the
init-nix.sh helper during package installation if it is not yet
available on the system.
%prep
%setup -q
%build
# No build step required; we ship the project tree as-is.
:
%install
rm -rf %{buildroot}
install -d %{buildroot}%{_bindir}
# Install project tree into a fixed, architecture-independent location.
install -d %{buildroot}/usr/lib/package-manager
# Copy full project source into /usr/lib/package-manager
cp -a . %{buildroot}/usr/lib/package-manager/
# Wrapper
install -m0755 scripts/pkgmgr-wrapper.sh %{buildroot}%{_bindir}/pkgmgr
# Shared Nix init script (ensure it is executable in the installed tree)
install -m0755 scripts/init-nix.sh %{buildroot}/usr/lib/package-manager/init-nix.sh
# Remove packaging-only and development artefacts from the installed tree
rm -rf \
%{buildroot}/usr/lib/package-manager/PKGBUILD \
%{buildroot}/usr/lib/package-manager/Dockerfile \
%{buildroot}/usr/lib/package-manager/debian \
%{buildroot}/usr/lib/package-manager/.git \
%{buildroot}/usr/lib/package-manager/.github \
%{buildroot}/usr/lib/package-manager/tests \
%{buildroot}/usr/lib/package-manager/.gitignore \
%{buildroot}/usr/lib/package-manager/__pycache__ \
%{buildroot}/usr/lib/package-manager/.gitkeep || true
%post
/usr/lib/package-manager/init-nix.sh || echo ">>> ERROR: /usr/lib/package-manager/init-nix.sh not found or not executable."
%postun
echo ">>> package-manager removed. Nix itself was not removed."
%files
%doc README.md
%license LICENSE
%{_bindir}/pkgmgr
/usr/lib/package-manager/
%changelog
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.1-1
- * 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.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`.
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.0-1
- Introduce a virgin Arch-based Nix flake E2E workflow that validates pkgmgrs full flake installation path using shared caches for faster and reproducible CI runs.
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.8.0-1
- **v0.7.15 — Installer & Command Resolution Improvements**
* Introduced a unified **layer-based installer pipeline** with clear precedence (OS-packages, Nix, Python, Makefile).
* Reworked installer structure and improved Python/Nix/Makefile installers, including isolated Python venvs and refined flake-output handling.
* Fully rewrote **command resolution** with stronger typing, safer fallbacks, and explicit support for `command: null` to mark library-only repositories.
* Added extensive **unit and integration tests** for installer capability ordering, command resolution, and Nix/Python installer behavior.
* Expanded documentation with capability hierarchy diagrams and scenario matrices.
* Removed deprecated repository entries and obsolete configuration files.
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.14-1
- Fixed the clone-all integration test so that `SystemExit(0)` from the proxy is treated as a successful command instead of a failure.
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.13-1
- Automated release.
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.12-1
- Fixed self refering alias during setup
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.11-1
- test: fix installer unit tests for OS packages and Nix dev shell
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.10-1
- Fixed test_install_pkgmgr_shallow.py
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.9-1
- 'main' and 'master' are now both accepted as branches for branch close merge
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.8-1
- Missing pyproject.toml doesn't lead to an error during release
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.7-1
- Added TEST_PATTERN parameter to execute dedicated tests
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.6-1
- Fixed pull --preview bug in e2e test
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.5-1
- Fixed wrong directory permissions for nix
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.4-1
- Fixed missing build in test workflow -> Tests pass now
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.3-1
- Fixed bug: Ignored packages are now ignored
* Tue Dec 09 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.7.2-1
- Implemented Changelog Support for Fedora and Debian
* Sat Dec 06 2025 Kevin Veen-Birkenbach <info@veen.world> - 0.1.1-1
- Initial RPM packaging for package-manager

View File

@@ -1,7 +0,0 @@
version: 1
author: "Kevin Veen-Birkenbach"
url: "https://github.com/kevinveenbirkenbach/package-manager"
description: "A configurable Python-based package manager for managing multiple repositories via Bash."
dependencies: []

View File

@@ -1,214 +0,0 @@
# pkgmgr/branch_commands.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
High-level helpers for branch-related operations.
This module encapsulates the actual Git logic so the CLI layer
(pkgmgr.cli_core.commands.branch) stays thin and testable.
"""
from __future__ import annotations
from typing import Optional
from pkgmgr.git_utils import run_git, GitError, get_current_branch
def open_branch(
name: Optional[str],
base_branch: str = "main",
cwd: str = ".",
) -> None:
"""
Create and push a new feature branch on top of `base_branch`.
Steps:
1) git fetch origin
2) git checkout <base_branch>
3) git pull origin <base_branch>
4) git checkout -b <name>
5) git push -u origin <name>
If `name` is None or empty, the user is prompted on stdin.
"""
if not name:
name = input("Enter new branch name: ").strip()
if not name:
raise RuntimeError("Branch name must not be empty.")
# 1) Fetch from origin
try:
run_git(["fetch", "origin"], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to fetch from origin before creating branch {name!r}: {exc}"
) from exc
# 2) Checkout base branch
try:
run_git(["checkout", base_branch], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {base_branch!r}: {exc}"
) from exc
# 3) Pull latest changes on base
try:
run_git(["pull", "origin", base_branch], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {base_branch!r}: {exc}"
) from exc
# 4) Create new branch
try:
run_git(["checkout", "-b", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to create new branch {name!r} from base {base_branch!r}: {exc}"
) from exc
# 5) Push and set upstream
try:
run_git(["push", "-u", "origin", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to push new branch {name!r} to origin: {exc}"
) from exc
def _resolve_base_branch(
preferred: str,
fallback: str,
cwd: str,
) -> str:
"""
Resolve the base branch to use for merging.
Try `preferred` (default: main) first, then `fallback` (default: master).
Raise RuntimeError if neither exists.
"""
for candidate in (preferred, fallback):
try:
run_git(["rev-parse", "--verify", candidate], cwd=cwd)
return candidate
except GitError:
continue
raise RuntimeError(
f"Neither {preferred!r} nor {fallback!r} exist in this repository."
)
def close_branch(
name: Optional[str],
base_branch: str = "main",
fallback_base: str = "master",
cwd: str = ".",
) -> None:
"""
Merge a feature branch into the main/master branch and optionally delete it.
Steps:
1) Determine branch name (argument or current branch)
2) Resolve base branch (prefers `base_branch`, falls back to `fallback_base`)
3) Ask for confirmation (y/N)
4) git fetch origin
5) git checkout <base>
6) git pull origin <base>
7) git merge --no-ff <name>
8) git push origin <base>
9) Delete branch locally and on origin
If the user does not confirm with 'y', the operation is aborted.
"""
# 1) Determine which branch to close
if not name:
try:
name = get_current_branch(cwd=cwd)
except GitError as exc:
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
if not name:
raise RuntimeError("Branch name must not be empty.")
# 2) Resolve base branch (main/master)
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
if name == target_base:
raise RuntimeError(
f"Refusing to close base branch {target_base!r}. "
"Please specify a feature branch."
)
# 3) Confirmation prompt
prompt = (
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? "
"(y/N): "
)
answer = input(prompt).strip().lower()
if answer != "y":
print("Aborted closing branch.")
return
# 4) Fetch from origin
try:
run_git(["fetch", "origin"], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
) from exc
# 5) Checkout base branch
try:
run_git(["checkout", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {target_base!r}: {exc}"
) from exc
# 6) Pull latest base
try:
run_git(["pull", "origin", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
) from exc
# 7) Merge feature branch into base
try:
run_git(["merge", "--no-ff", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to merge branch {name!r} into {target_base!r}: {exc}"
) from exc
# 8) Push updated base
try:
run_git(["push", "origin", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to push base branch {target_base!r} to origin after merge: {exc}"
) from exc
# 9) Delete feature branch locally
try:
run_git(["branch", "-d", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to delete local branch {name!r} after merge: {exc}"
) from exc
# 10) Delete feature branch on origin (best effort)
try:
run_git(["push", "origin", "--delete", name], cwd=cwd)
except GitError as exc:
# Remote delete is nice-to-have; surface as RuntimeError for clarity.
raise RuntimeError(
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
) from exc

View File

@@ -1,107 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import os
import sys
from pkgmgr.load_config import load_config
from pkgmgr.cli_core import CLIContext, create_parser, dispatch_command
# User config lives in the home directory:
# ~/.config/pkgmgr/config.yaml
USER_CONFIG_PATH = os.path.expanduser("~/.config/pkgmgr/config.yaml")
DESCRIPTION_TEXT = """\
\033[1;32mPackage Manager 🤖📦\033[0m
\033[3mKevin's Package Manager is a multi-repository, multi-package, and multi-format
development tool crafted by and designed for:\033[0m
\033[1;34mKevin Veen-Birkenbach\033[0m
\033[4mhttps://www.veen.world/\033[0m
\033[1mOverview:\033[0m
A powerful toolchain that unifies and automates workflows across heterogeneous
project ecosystems. pkgmgr is not only a package manager — it is a full
developer-oriented orchestration tool.
It automatically detects, merges, and processes metadata from multiple
dependency formats, including:
\033[1;33mPython:\033[0m pyproject.toml, requirements.txt
\033[1;33mNix:\033[0m flake.nix
\033[1;33mArch Linux:\033[0m PKGBUILD
\033[1;33mAnsible:\033[0m requirements.yml
\033[1;33mpkgmgr-native:\033[0m pkgmgr.yml
This allows pkgmgr to perform installation, updates, verification, dependency
resolution, and synchronization across complex multi-repo environments — with a
single unified command-line interface.
\033[1mDeveloper Tools:\033[0m
pkgmgr includes an integrated toolbox to enhance daily development workflows:
\033[1;33mVS Code integration:\033[0m Auto-generate and open multi-repo workspaces
\033[1;33mTerminal integration:\033[0m Open repositories in new GNOME Terminal tabs
\033[1;33mExplorer integration:\033[0m Open repositories in your file manager
\033[1;33mRelease automation:\033[0m Version bumping, changelog updates, and tagging
\033[1;33mBatch operations:\033[0m Execute shell commands across multiple repositories
\033[1;33mGit/Docker/Make wrappers:\033[0m Unified command proxying for many tools
\033[1mCapabilities:\033[0m
• Clone, pull, verify, update, and manage many repositories at once
• Resolve dependencies across languages and ecosystems
• Standardize install/update workflows
• Create symbolic executable wrappers for any project
• Merge configuration from default + user config layers
Use pkgmgr as both a robust package management framework and a versatile
development orchestration tool.
For detailed help on each command, use:
\033[1mpkgmgr <command> --help\033[0m
"""
def main() -> None:
"""
Entry point for the pkgmgr CLI.
"""
config_merged = load_config(USER_CONFIG_PATH)
# Directories: be robust and provide sane defaults if missing
directories = config_merged.get("directories") or {}
repositories_dir = os.path.expanduser(
directories.get("repositories", "~/Repositories")
)
binaries_dir = os.path.expanduser(
directories.get("binaries", "~/.local/bin")
)
# Ensure the merged config actually contains the resolved directories
config_merged.setdefault("directories", {})
config_merged["directories"]["repositories"] = repositories_dir
config_merged["directories"]["binaries"] = binaries_dir
all_repositories = config_merged.get("repositories", [])
ctx = CLIContext(
config_merged=config_merged,
repositories_base_dir=repositories_dir,
all_repositories=all_repositories,
binaries_dir=binaries_dir,
user_config_path=USER_CONFIG_PATH,
)
parser = create_parser(DESCRIPTION_TEXT)
args = parser.parse_args()
if not getattr(args, "command", None):
parser.print_help()
return
dispatch_command(args, ctx)
if __name__ == "__main__":
main()

View File

@@ -1,5 +0,0 @@
from .context import CLIContext
from .parser import create_parser
from .dispatch import dispatch_command
__all__ = ["CLIContext", "create_parser", "dispatch_command"]

View File

@@ -1,83 +0,0 @@
from __future__ import annotations
import json
import os
from typing import Any, Dict, List
from pkgmgr.cli_core.context import CLIContext
from pkgmgr.run_command import run_command
from pkgmgr.get_repo_identifier import get_repo_identifier
Repository = Dict[str, Any]
def handle_tools_command(
args,
ctx: CLIContext,
selected: List[Repository],
) -> None:
"""
Handle integration commands:
- explore (file manager)
- terminal (GNOME Terminal)
- code (VS Code workspace)
"""
# --------------------------------------------------------
# explore
# --------------------------------------------------------
if args.command == "explore":
for repository in selected:
run_command(
f"nautilus {repository['directory']} & disown"
)
return
# --------------------------------------------------------
# terminal
# --------------------------------------------------------
if args.command == "terminal":
for repository in selected:
run_command(
f'gnome-terminal --tab --working-directory="{repository["directory"]}"'
)
return
# --------------------------------------------------------
# code
# --------------------------------------------------------
if args.command == "code":
if not selected:
print("No repositories selected.")
return
identifiers = [
get_repo_identifier(repo, ctx.all_repositories)
for repo in selected
]
sorted_identifiers = sorted(identifiers)
workspace_name = "_".join(sorted_identifiers) + ".code-workspace"
workspaces_dir = os.path.expanduser(
ctx.config_merged.get("directories").get("workspaces")
)
os.makedirs(workspaces_dir, exist_ok=True)
workspace_file = os.path.join(workspaces_dir, workspace_name)
folders = [{"path": repository["directory"]} for repository in selected]
workspace_data = {
"folders": folders,
"settings": {},
}
if not os.path.exists(workspace_file):
with open(workspace_file, "w") as f:
json.dump(workspace_data, f, indent=4)
print(f"Created workspace file: {workspace_file}")
else:
print(f"Using existing workspace file: {workspace_file}")
run_command(f'code "{workspace_file}"')
return

View File

@@ -1,505 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
import argparse
from pkgmgr.cli_core.proxy import register_proxy_commands
class SortedSubParsersAction(argparse._SubParsersAction):
"""
Subparsers action that keeps choices sorted alphabetically.
"""
def add_parser(self, name, **kwargs):
parser = super().add_parser(name, **kwargs)
# Sort choices alphabetically by dest (subcommand name)
self._choices_actions.sort(key=lambda a: a.dest)
return parser
def add_identifier_arguments(subparser: argparse.ArgumentParser) -> None:
"""
Common identifier / selection arguments for many subcommands.
Selection modes (mutual intent, not hard-enforced):
- identifiers (positional): select by alias / provider/account/repo
- --all: select all repositories
- --category / --string / --tag: filter-based selection on top
of the full repository set
"""
subparser.add_argument(
"identifiers",
nargs="*",
help=(
"Identifier(s) for repositories. "
"Default: Repository of current folder."
),
)
subparser.add_argument(
"--all",
action="store_true",
default=False,
help=(
"Apply the subcommand to all repositories in the config. "
"Some subcommands ask for confirmation. If you want to give this "
"confirmation for all repositories, pipe 'yes'. E.g: "
"yes | pkgmgr {subcommand} --all"
),
)
subparser.add_argument(
"--category",
nargs="+",
default=[],
help=(
"Filter repositories by category patterns derived from config "
"filenames or repo metadata (use filename without .yml/.yaml, "
"or /regex/ to use a regular expression)."
),
)
subparser.add_argument(
"--string",
default="",
help=(
"Filter repositories whose identifier / name / path contains this "
"substring (case-insensitive). Use /regex/ for regular expressions."
),
)
subparser.add_argument(
"--tag",
action="append",
default=[],
help=(
"Filter repositories by tag. Matches tags from the repository "
"collector and category tags. Use /regex/ for regular expressions."
),
)
subparser.add_argument(
"--preview",
action="store_true",
help="Preview changes without executing commands",
)
subparser.add_argument(
"--list",
action="store_true",
help="List affected repositories (with preview or status)",
)
subparser.add_argument(
"-a",
"--args",
nargs=argparse.REMAINDER,
dest="extra_args",
help="Additional parameters to be attached.",
default=[],
)
def add_install_update_arguments(subparser: argparse.ArgumentParser) -> None:
"""
Common arguments for install/update commands.
"""
add_identifier_arguments(subparser)
subparser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Suppress warnings and info messages",
)
subparser.add_argument(
"--no-verification",
action="store_true",
default=False,
help="Disable verification via commit/gpg",
)
subparser.add_argument(
"--dependencies",
action="store_true",
help="Also pull and update dependencies",
)
subparser.add_argument(
"--clone-mode",
choices=["ssh", "https", "shallow"],
default="ssh",
help=(
"Specify the clone mode: ssh, https, or shallow "
"(HTTPS shallow clone; default: ssh)"
),
)
def create_parser(description_text: str) -> argparse.ArgumentParser:
"""
Create the top-level argument parser for pkgmgr.
"""
parser = argparse.ArgumentParser(
description=description_text,
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers(
dest="command",
help="Subcommands",
action=SortedSubParsersAction,
)
# ------------------------------------------------------------
# install / update / deinstall / delete
# ------------------------------------------------------------
install_parser = subparsers.add_parser(
"install",
help="Setup repository/repositories alias links to executables",
)
add_install_update_arguments(install_parser)
update_parser = subparsers.add_parser(
"update",
help="Update (pull + install) repository/repositories",
)
add_install_update_arguments(update_parser)
update_parser.add_argument(
"--system",
action="store_true",
help="Include system update commands",
)
deinstall_parser = subparsers.add_parser(
"deinstall",
help="Remove alias links to repository/repositories",
)
add_identifier_arguments(deinstall_parser)
delete_parser = subparsers.add_parser(
"delete",
help="Delete repository/repositories alias links to executables",
)
add_identifier_arguments(delete_parser)
# ------------------------------------------------------------
# create
# ------------------------------------------------------------
create_cmd_parser = subparsers.add_parser(
"create",
help=(
"Create new repository entries: add them to the config if not "
"already present, initialize the local repository, and push "
"remotely if --remote is set."
),
)
add_identifier_arguments(create_cmd_parser)
create_cmd_parser.add_argument(
"--remote",
action="store_true",
help="If set, add the remote and push the initial commit.",
)
# ------------------------------------------------------------
# status
# ------------------------------------------------------------
status_parser = subparsers.add_parser(
"status",
help="Show status for repository/repositories or system",
)
add_identifier_arguments(status_parser)
status_parser.add_argument(
"--system",
action="store_true",
help="Show system status",
)
# ------------------------------------------------------------
# config
# ------------------------------------------------------------
config_parser = subparsers.add_parser(
"config",
help="Manage configuration",
)
config_subparsers = config_parser.add_subparsers(
dest="subcommand",
help="Config subcommands",
required=True,
)
config_show = config_subparsers.add_parser(
"show",
help="Show configuration",
)
add_identifier_arguments(config_show)
config_subparsers.add_parser(
"add",
help="Interactively add a new repository entry",
)
config_subparsers.add_parser(
"edit",
help="Edit configuration file with nano",
)
config_subparsers.add_parser(
"init",
help="Initialize user configuration by scanning the base directory",
)
config_delete = config_subparsers.add_parser(
"delete",
help="Delete repository entry from user config",
)
add_identifier_arguments(config_delete)
config_ignore = config_subparsers.add_parser(
"ignore",
help="Set ignore flag for repository entries in user config",
)
add_identifier_arguments(config_ignore)
config_ignore.add_argument(
"--set",
choices=["true", "false"],
required=True,
help="Set ignore to true or false",
)
config_subparsers.add_parser(
"update",
help=(
"Update default config files in ~/.config/pkgmgr/ from the "
"installed pkgmgr package (does not touch config.yaml)."
),
)
# ------------------------------------------------------------
# path / explore / terminal / code / shell
# ------------------------------------------------------------
path_parser = subparsers.add_parser(
"path",
help="Print the path(s) of repository/repositories",
)
add_identifier_arguments(path_parser)
explore_parser = subparsers.add_parser(
"explore",
help="Open repository in Nautilus file manager",
)
add_identifier_arguments(explore_parser)
terminal_parser = subparsers.add_parser(
"terminal",
help="Open repository in a new GNOME Terminal tab",
)
add_identifier_arguments(terminal_parser)
code_parser = subparsers.add_parser(
"code",
help="Open repository workspace with VS Code",
)
add_identifier_arguments(code_parser)
shell_parser = subparsers.add_parser(
"shell",
help="Execute a shell command in each repository",
)
add_identifier_arguments(shell_parser)
shell_parser.add_argument(
"-c",
"--command",
nargs=argparse.REMAINDER,
dest="shell_command",
help=(
"The shell command (and its arguments) to execute in each "
"repository"
),
default=[],
)
# ------------------------------------------------------------
# branch
# ------------------------------------------------------------
branch_parser = subparsers.add_parser(
"branch",
help="Branch-related utilities (e.g. open/close feature branches)",
)
branch_subparsers = branch_parser.add_subparsers(
dest="subcommand",
help="Branch subcommands",
required=True,
)
branch_open = branch_subparsers.add_parser(
"open",
help="Create and push a new branch on top of a base branch",
)
branch_open.add_argument(
"name",
nargs="?",
help=(
"Name of the new branch (optional; will be asked interactively "
"if omitted)"
),
)
branch_open.add_argument(
"--base",
default="main",
help="Base branch to create the new branch from (default: main)",
)
branch_close = branch_subparsers.add_parser(
"close",
help="Merge a feature branch into base and delete it",
)
branch_close.add_argument(
"name",
nargs="?",
help=(
"Name of the branch to close (optional; current branch is used "
"if omitted)"
),
)
branch_close.add_argument(
"--base",
default="main",
help=(
"Base branch to merge into (default: main; falls back to master "
"internally if main does not exist)"
),
)
# ------------------------------------------------------------
# release
# ------------------------------------------------------------
release_parser = subparsers.add_parser(
"release",
help=(
"Create a release for repository/ies by incrementing version "
"and updating the changelog."
),
)
release_parser.add_argument(
"release_type",
choices=["major", "minor", "patch"],
help="Type of version increment for the release (major, minor, patch).",
)
release_parser.add_argument(
"-m",
"--message",
default=None,
help=(
"Optional release message to add to the changelog and tag."
),
)
# Generic selection / preview / list / extra_args
add_identifier_arguments(release_parser)
# Close current branch after successful release
release_parser.add_argument(
"--close",
action="store_true",
help=(
"Close the current branch after a successful release in each "
"repository, if it is not main/master."
),
)
# Force: skip preview+confirmation and run release directly
release_parser.add_argument(
"-f",
"--force",
action="store_true",
help=(
"Skip the interactive preview+confirmation step and run the "
"release directly."
),
)
# ------------------------------------------------------------
# version
# ------------------------------------------------------------
version_parser = subparsers.add_parser(
"version",
help=(
"Show version information for repository/ies "
"(git tags, pyproject.toml, flake.nix, PKGBUILD, debian, spec, "
"Ansible Galaxy)."
),
)
add_identifier_arguments(version_parser)
# ------------------------------------------------------------
# changelog
# ------------------------------------------------------------
changelog_parser = subparsers.add_parser(
"changelog",
help=(
"Show changelog derived from Git history. "
"By default, shows the changes between the last two SemVer tags."
),
)
changelog_parser.add_argument(
"range",
nargs="?",
default="",
help=(
"Optional tag or range (e.g. v1.2.3 or v1.2.0..v1.2.3). "
"If omitted, the changelog between the last two SemVer "
"tags is shown."
),
)
add_identifier_arguments(changelog_parser)
# ------------------------------------------------------------
# list
# ------------------------------------------------------------
list_parser = subparsers.add_parser(
"list",
help="List all repositories with details and status",
)
# dieselbe Selektionslogik wie bei install/update/etc.:
add_identifier_arguments(list_parser)
list_parser.add_argument(
"--status",
type=str,
default="",
help=(
"Filter repositories by status (case insensitive). "
"Use /regex/ for regular expressions."
),
)
list_parser.add_argument(
"--description",
action="store_true",
help=(
"Show an additional detailed section per repository "
"(description, homepage, tags, categories, paths)."
),
)
# ------------------------------------------------------------
# make
# ------------------------------------------------------------
make_parser = subparsers.add_parser(
"make",
help="Executes make commands",
)
add_identifier_arguments(make_parser)
make_subparsers = make_parser.add_subparsers(
dest="subcommand",
help="Make subcommands",
required=True,
)
make_install = make_subparsers.add_parser(
"install",
help="Executes the make install command",
)
add_identifier_arguments(make_install)
make_deinstall = make_subparsers.add_parser(
"deinstall",
help="Executes the make deinstall command",
)
add_identifier_arguments(make_deinstall)
# ------------------------------------------------------------
# Proxy commands (git, docker, docker compose, ...)
# ------------------------------------------------------------
register_proxy_commands(subparsers)
return parser

View File

@@ -1,294 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Repository installation pipeline for pkgmgr.
This module orchestrates the installation of repositories by:
1. Ensuring the repository directory exists (cloning if necessary).
2. Verifying the repository according to the configured policies.
3. Creating executable links using create_ink(), after resolving the
appropriate command via resolve_command_for_repo().
4. Running a sequence of modular installer components that handle
specific technologies or manifests (PKGBUILD, Nix flakes, Python
via pyproject.toml, Makefile, OS-specific package metadata).
The goal is to keep this file thin and delegate most logic to small,
focused installer classes.
"""
import os
from typing import List, Dict, Any
from pkgmgr.get_repo_identifier import get_repo_identifier
from pkgmgr.get_repo_dir import get_repo_dir
from pkgmgr.create_ink import create_ink
from pkgmgr.verify import verify_repository
from pkgmgr.clone_repos import clone_repos
from pkgmgr.context import RepoContext
from pkgmgr.resolve_command import resolve_command_for_repo
# Installer implementations
from pkgmgr.installers.os_packages import (
ArchPkgbuildInstaller,
DebianControlInstaller,
RpmSpecInstaller,
)
from pkgmgr.installers.nix_flake import NixFlakeInstaller
from pkgmgr.installers.python import PythonInstaller
from pkgmgr.installers.makefile import MakefileInstaller
# Layering:
# 1) OS packages: PKGBUILD / debian/control / RPM spec → os-deps.*
# 2) Nix flakes (flake.nix) → e.g. python-runtime, make-install
# 3) Python (pyproject.toml) → e.g. python-runtime, make-install
# 4) Makefile fallback → e.g. make-install
INSTALLERS = [
ArchPkgbuildInstaller(), # Arch
DebianControlInstaller(), # Debian/Ubuntu
RpmSpecInstaller(), # Fedora/RHEL/CentOS
NixFlakeInstaller(), # flake.nix (Nix layer)
PythonInstaller(), # pyproject.toml
MakefileInstaller(), # generic 'make install'
]
def _ensure_repo_dir(
repo: Dict[str, Any],
repositories_base_dir: str,
all_repos: List[Dict[str, Any]],
preview: bool,
no_verification: bool,
clone_mode: str,
identifier: str,
) -> str:
"""
Ensure the repository directory exists. If not, attempt to clone it.
Returns the repository directory path or an empty string if cloning failed.
"""
repo_dir = get_repo_dir(repositories_base_dir, repo)
if not os.path.exists(repo_dir):
print(f"Repository directory '{repo_dir}' does not exist. Cloning it now...")
clone_repos(
[repo],
repositories_base_dir,
all_repos,
preview,
no_verification,
clone_mode,
)
if not os.path.exists(repo_dir):
print(f"Cloning failed for repository {identifier}. Skipping installation.")
return ""
return repo_dir
def _verify_repo(
repo: Dict[str, Any],
repo_dir: str,
no_verification: bool,
identifier: str,
) -> bool:
"""
Verify the repository using verify_repository().
Returns True if installation should proceed, False if it should be skipped.
"""
verified_info = repo.get("verified")
verified_ok, errors, commit_hash, signing_key = verify_repository(
repo,
repo_dir,
mode="local",
no_verification=no_verification,
)
if not no_verification and verified_info and not verified_ok:
print(f"Warning: Verification failed for {identifier}:")
for err in errors:
print(f" - {err}")
choice = input("Proceed with installation? (y/N): ").strip().lower()
if choice != "y":
print(f"Skipping installation for {identifier}.")
return False
return True
def _create_context(
repo: Dict[str, Any],
identifier: str,
repo_dir: str,
repositories_base_dir: str,
bin_dir: str,
all_repos: List[Dict[str, Any]],
no_verification: bool,
preview: bool,
quiet: bool,
clone_mode: str,
update_dependencies: bool,
) -> RepoContext:
"""
Build a RepoContext for the given repository and parameters.
"""
return RepoContext(
repo=repo,
identifier=identifier,
repo_dir=repo_dir,
repositories_base_dir=repositories_base_dir,
bin_dir=bin_dir,
all_repos=all_repos,
no_verification=no_verification,
preview=preview,
quiet=quiet,
clone_mode=clone_mode,
update_dependencies=update_dependencies,
)
def install_repos(
selected_repos: List[Dict[str, Any]],
repositories_base_dir: str,
bin_dir: str,
all_repos: List[Dict[str, Any]],
no_verification: bool,
preview: bool,
quiet: bool,
clone_mode: str,
update_dependencies: bool,
) -> None:
"""
Install repositories by creating symbolic links and processing standard
manifest files (PKGBUILD, flake.nix, Python manifests, Makefile, etc.)
via dedicated installer components.
Any installer failure (SystemExit) is treated as fatal and will abort
the current installation.
"""
for repo in selected_repos:
identifier = get_repo_identifier(repo, all_repos)
repo_dir = _ensure_repo_dir(
repo=repo,
repositories_base_dir=repositories_base_dir,
all_repos=all_repos,
preview=preview,
no_verification=no_verification,
clone_mode=clone_mode,
identifier=identifier,
)
if not repo_dir:
continue
if not _verify_repo(
repo=repo,
repo_dir=repo_dir,
no_verification=no_verification,
identifier=identifier,
):
continue
ctx = _create_context(
repo=repo,
identifier=identifier,
repo_dir=repo_dir,
repositories_base_dir=repositories_base_dir,
bin_dir=bin_dir,
all_repos=all_repos,
no_verification=no_verification,
preview=preview,
quiet=quiet,
clone_mode=clone_mode,
update_dependencies=update_dependencies,
)
# ------------------------------------------------------------
# Resolve the command for this repository before creating the link.
# If no command is resolved, no link will be created.
# ------------------------------------------------------------
resolved_command = resolve_command_for_repo(
repo=repo,
repo_identifier=identifier,
repo_dir=repo_dir,
)
if resolved_command:
repo["command"] = resolved_command
else:
repo.pop("command", None)
# ------------------------------------------------------------
# Create the symlink using create_ink (if a command is set).
# ------------------------------------------------------------
create_ink(
repo,
repositories_base_dir,
bin_dir,
all_repos,
quiet=quiet,
preview=preview,
)
# Track which logical capabilities have already been provided by
# earlier installers for this repository. This allows us to skip
# installers that would only duplicate work (e.g. Python runtime
# already provided by Nix flake → skip pyproject/Makefile).
provided_capabilities: set[str] = set()
# Run all installers that support this repository, but only if they
# provide at least one capability that is not yet satisfied.
for installer in INSTALLERS:
if not installer.supports(ctx):
continue
caps = installer.discover_capabilities(ctx)
# If the installer declares capabilities and *all* of them are
# already provided, we can safely skip it.
if caps and caps.issubset(provided_capabilities):
if not quiet:
print(
f"Skipping installer {installer.__class__.__name__} "
f"for {identifier} capabilities {caps} already provided."
)
continue
# ------------------------------------------------------------
# Debug output + clear error if an installer fails
# ------------------------------------------------------------
if not quiet:
print(
f"[pkgmgr] Running installer {installer.__class__.__name__} "
f"for {identifier} in '{repo_dir}' "
f"(new capabilities: {caps or ''})..."
)
try:
installer.run(ctx)
except SystemExit as exc:
exit_code = exc.code if isinstance(exc.code, int) else str(exc.code)
print(
f"[ERROR] Installer {installer.__class__.__name__} failed "
f"for repository {identifier} (dir: {repo_dir}) "
f"with exit code {exit_code}."
)
print(
"[ERROR] This usually means an underlying command failed "
"(e.g. 'make install', 'nix build', 'pip install', ...)."
)
print(
"[ERROR] Check the log above for the exact command output. "
"You can also run this repository in isolation via:\n"
f" pkgmgr install {identifier} --clone-mode shallow --no-verification"
)
# Re-raise so that CLI/tests fail clearly,
# but now with much more context.
raise
# Only merge capabilities if the installer succeeded
provided_capabilities.update(caps)

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Installer package for pkgmgr.
This exposes all installer classes so users can import them directly from
pkgmgr.installers.
"""
from pkgmgr.installers.base import BaseInstaller # noqa: F401
from pkgmgr.installers.nix_flake import NixFlakeInstaller # noqa: F401
from pkgmgr.installers.python import PythonInstaller # noqa: F401
from pkgmgr.installers.makefile import MakefileInstaller # noqa: F401
# OS-specific installers
from pkgmgr.installers.os_packages.arch_pkgbuild import ArchPkgbuildInstaller # noqa: F401
from pkgmgr.installers.os_packages.debian_control import DebianControlInstaller # noqa: F401
from pkgmgr.installers.os_packages.rpm_spec import RpmSpecInstaller # noqa: F401

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Installer that triggers `make install` if a Makefile is present and
the Makefile actually defines an 'install' target.
This is useful for repositories that expose a standard Makefile-based
installation step.
"""
import os
import re
from pkgmgr.context import RepoContext
from pkgmgr.installers.base import BaseInstaller
from pkgmgr.run_command import run_command
class MakefileInstaller(BaseInstaller):
"""Run `make install` if a Makefile with an 'install' target exists."""
# Logical layer name, used by capability matchers.
layer = "makefile"
MAKEFILE_NAME = "Makefile"
def supports(self, ctx: RepoContext) -> bool:
"""Return True if a Makefile exists in the repository directory."""
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
return os.path.exists(makefile_path)
def _has_install_target(self, makefile_path: str) -> bool:
"""
Check whether the Makefile defines an 'install' target.
We treat the presence of a real install target as either:
- a line starting with 'install:' (optionally preceded by whitespace), or
- a .PHONY line that lists 'install' as one of the targets.
"""
try:
with open(makefile_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except OSError:
# If we cannot read the Makefile for some reason, assume no target.
return False
# install: ...
if re.search(r"^\s*install\s*:", content, flags=re.MULTILINE):
return True
# .PHONY: ... install ...
if re.search(r"^\s*\.PHONY\s*:\s*.*\binstall\b", content, flags=re.MULTILINE):
return True
return False
def run(self, ctx: RepoContext) -> None:
"""
Execute `make install` in the repository directory, but only if an
'install' target is actually defined in the Makefile.
Any failure in `make install` is treated as a fatal error and will
propagate as SystemExit from run_command().
"""
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
if not os.path.exists(makefile_path):
# Should normally not happen if supports() was checked before,
# but keep this guard for robustness.
if not ctx.quiet:
print(
f"[pkgmgr] Makefile '{makefile_path}' not found, "
"skipping make install."
)
return
if not self._has_install_target(makefile_path):
if not ctx.quiet:
print(
"[pkgmgr] Skipping Makefile install: no 'install' target "
f"found in {makefile_path}."
)
return
if not ctx.quiet:
print(
f"[pkgmgr] Running 'make install' in {ctx.repo_dir} "
"(install target detected in Makefile)."
)
cmd = "make install"
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)

View File

@@ -1,106 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Installer for Nix flakes.
If a repository contains flake.nix and the 'nix' command is available, this
installer will try to install profile outputs from the flake.
Behavior:
- If flake.nix is present and `nix` exists on PATH:
* First remove any existing `package-manager` profile entry (best-effort).
* Then install the flake outputs (`pkgmgr`, `default`) via `nix profile install`.
- Failure installing `pkgmgr` is treated as fatal.
- Failure installing `default` is logged as an error/warning but does not abort.
"""
import os
import shutil
from typing import TYPE_CHECKING
from pkgmgr.installers.base import BaseInstaller
from pkgmgr.run_command import run_command
if TYPE_CHECKING:
from pkgmgr.context import RepoContext
from pkgmgr.install_repos import InstallContext
class NixFlakeInstaller(BaseInstaller):
"""Install Nix flake profiles for repositories that define flake.nix."""
# Logical layer name, used by capability matchers.
layer = "nix"
FLAKE_FILE = "flake.nix"
PROFILE_NAME = "package-manager"
def supports(self, ctx: "RepoContext") -> bool:
"""
Only support repositories that:
- Have a flake.nix
- And have the `nix` command available.
"""
if shutil.which("nix") is None:
return False
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
return os.path.exists(flake_path)
def _ensure_old_profile_removed(self, ctx: "RepoContext") -> None:
"""
Best-effort removal of an existing profile entry.
This handles the "already provides the following file" conflict by
removing previous `package-manager` installations before we install
the new one.
Any error in `nix profile remove` is intentionally ignored, because
a missing profile entry is not a fatal condition.
"""
if shutil.which("nix") is None:
return
cmd = f"nix profile remove {self.PROFILE_NAME} || true"
try:
# NOTE: no allow_failure here → matches the existing unit tests
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
except SystemExit:
# Unit tests explicitly assert this is swallowed
pass
def run(self, ctx: "InstallContext") -> None:
"""
Install Nix flake profile outputs (pkgmgr, default).
Any failure installing `pkgmgr` is treated as fatal (SystemExit).
A failure installing `default` is logged but does not abort.
"""
# Reuse supports() to keep logic in one place
if not self.supports(ctx): # type: ignore[arg-type]
return
print("Nix flake detected, attempting to install profile outputs...")
# Handle the "already installed" case up-front:
self._ensure_old_profile_removed(ctx) # type: ignore[arg-type]
for output in ("pkgmgr", "default"):
cmd = f"nix profile install {ctx.repo_dir}#{output}"
try:
# For 'default' we don't want the process to exit on error
allow_failure = output == "default"
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview, allow_failure=allow_failure)
print(f"Nix flake output '{output}' successfully installed.")
except SystemExit as e:
print(f"[Error] Failed to install Nix flake output '{output}': {e}")
if output == "pkgmgr":
# Broken main CLI install → fatal
raise
# For 'default' we log and continue
print(
"[Warning] Continuing despite failure to install 'default' "
"because 'pkgmgr' is already installed."
)
break

View File

@@ -1,160 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Installer for RPM-based packages defined in *.spec files.
This installer:
1. Installs build dependencies via dnf/yum builddep (where available)
2. Uses rpmbuild to build RPMs from the provided .spec file
3. Installs the resulting RPMs via `rpm -i`
It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.).
"""
import glob
import os
import shutil
from typing import List, Optional
from pkgmgr.context import RepoContext
from pkgmgr.installers.base import BaseInstaller
from pkgmgr.run_command import run_command
class RpmSpecInstaller(BaseInstaller):
"""
Build and install RPM-based packages from *.spec files.
This installer is responsible for the full build + install of the
application on RPM-like systems.
"""
# Logical layer name, used by capability matchers.
layer = "os-packages"
def _is_rpm_like(self) -> bool:
"""
Basic RPM-like detection:
- rpmbuild must be available
- at least one of dnf / yum / yum-builddep must be present
"""
if shutil.which("rpmbuild") is None:
return False
has_dnf = shutil.which("dnf") is not None
has_yum = shutil.which("yum") is not None
has_yum_builddep = shutil.which("yum-builddep") is not None
return has_dnf or has_yum or has_yum_builddep
def _spec_path(self, ctx: RepoContext) -> Optional[str]:
"""Return the first *.spec file in the repository root, if any."""
pattern = os.path.join(ctx.repo_dir, "*.spec")
matches = sorted(glob.glob(pattern))
if not matches:
return None
return matches[0]
def supports(self, ctx: RepoContext) -> bool:
"""
This installer is supported if:
- we are on an RPM-based system (rpmbuild + dnf/yum/yum-builddep available), and
- a *.spec file exists in the repository root.
"""
if not self._is_rpm_like():
return False
return self._spec_path(ctx) is not None
def _find_built_rpms(self) -> List[str]:
"""
Find RPMs built by rpmbuild.
By default, rpmbuild outputs RPMs into:
~/rpmbuild/RPMS/*/*.rpm
"""
home = os.path.expanduser("~")
pattern = os.path.join(home, "rpmbuild", "RPMS", "**", "*.rpm")
return sorted(glob.glob(pattern, recursive=True))
def _install_build_dependencies(self, ctx: RepoContext, spec_path: str) -> None:
"""
Install build dependencies for the given .spec file.
Strategy (best-effort):
1. If dnf is available:
sudo dnf builddep -y <spec>
2. Else if yum-builddep is available:
sudo yum-builddep -y <spec>
3. Else if yum is available:
sudo yum-builddep -y <spec> # Some systems provide it via yum plugin
4. Otherwise: print a warning and skip automatic builddep install.
Any failure in builddep installation is treated as fatal (SystemExit),
consistent with other installer steps.
"""
spec_basename = os.path.basename(spec_path)
if shutil.which("dnf") is not None:
cmd = f"sudo dnf builddep -y {spec_basename}"
elif shutil.which("yum-builddep") is not None:
cmd = f"sudo yum-builddep -y {spec_basename}"
elif shutil.which("yum") is not None:
# Some distributions ship yum-builddep as a plugin.
cmd = f"sudo yum-builddep -y {spec_basename}"
else:
print(
"[Warning] No suitable RPM builddep tool (dnf/yum-builddep/yum) found. "
"Skipping automatic build dependency installation for RPM."
)
return
# Run builddep in the repository directory so relative spec paths work.
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
def run(self, ctx: RepoContext) -> None:
"""
Build and install RPM-based packages.
Steps:
1. dnf/yum builddep <spec> (automatic build dependency installation)
2. rpmbuild -ba path/to/spec
3. sudo rpm -i ~/rpmbuild/RPMS/*/*.rpm
"""
spec_path = self._spec_path(ctx)
if not spec_path:
return
# 1) Install build dependencies
self._install_build_dependencies(ctx, spec_path)
# 2) Build RPMs
# Use the full spec path, but run in the repo directory.
spec_basename = os.path.basename(spec_path)
build_cmd = f"rpmbuild -ba {spec_basename}"
run_command(build_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
# 3) Find built RPMs
rpms = self._find_built_rpms()
if not rpms:
print(
"[Warning] No RPM files found after rpmbuild. "
"Skipping RPM package installation."
)
return
# 4) Install RPMs
if shutil.which("rpm") is None:
print(
"[Warning] rpm binary not found on PATH. "
"Cannot install built RPMs."
)
return
install_cmd = "sudo rpm -i " + " ".join(rpms)
run_command(install_cmd, cwd=ctx.repo_dir, preview=ctx.preview)

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Installer for Python projects based on pyproject.toml.
Strategy:
- Determine a pip command in this order:
1. $PKGMGR_PIP (explicit override, e.g. ~/.venvs/pkgmgr/bin/pip)
2. sys.executable -m pip (current interpreter)
3. "pip" from PATH as last resort
- If pyproject.toml exists: pip install .
All installation failures are treated as fatal errors (SystemExit).
"""
import os
import sys
from pkgmgr.installers.base import BaseInstaller
from pkgmgr.run_command import run_command
class PythonInstaller(BaseInstaller):
"""Install Python projects and dependencies via pip."""
# Logical layer name, used by capability matchers.
layer = "python"
def supports(self, ctx) -> bool:
"""
Return True if this installer should handle the given repository.
Only pyproject.toml is supported as the single source of truth
for Python dependencies and packaging metadata.
"""
repo_dir = ctx.repo_dir
return os.path.exists(os.path.join(repo_dir, "pyproject.toml"))
def _pip_cmd(self) -> str:
"""
Resolve the pip command to use.
"""
explicit = os.environ.get("PKGMGR_PIP", "").strip()
if explicit:
return explicit
if sys.executable:
return f"{sys.executable} -m pip"
return "pip"
def run(self, ctx) -> None:
"""
Install Python project defined via pyproject.toml.
Any pip failure is propagated as SystemExit.
"""
pip_cmd = self._pip_cmd()
pyproject = os.path.join(ctx.repo_dir, "pyproject.toml")
if os.path.exists(pyproject):
print(
f"pyproject.toml found in {ctx.identifier}, "
f"installing Python project..."
)
cmd = f"{pip_cmd} install ."
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)

View File

@@ -1,48 +0,0 @@
import os
import subprocess
import sys
from pkgmgr.get_repo_identifier import get_repo_identifier
from pkgmgr.get_repo_dir import get_repo_dir
from pkgmgr.verify import verify_repository
def pull_with_verification(
selected_repos,
repositories_base_dir,
all_repos,
extra_args,
no_verification,
preview:bool):
"""
Executes "git pull" for each repository with verification.
Uses the verify_repository function in "pull" mode.
If verification fails (and verification info is set) and --no-verification is not enabled,
the user is prompted to confirm the pull.
"""
for repo in selected_repos:
repo_identifier = get_repo_identifier(repo, all_repos)
repo_dir = get_repo_dir(repositories_base_dir, repo)
if not os.path.exists(repo_dir):
print(f"Repository directory '{repo_dir}' not found for {repo_identifier}.")
continue
verified_info = repo.get("verified")
verified_ok, errors, commit_hash, signing_key = verify_repository(repo, repo_dir, mode="pull", no_verification=no_verification)
if not no_verification and verified_info and not verified_ok:
print(f"Warning: Verification failed for {repo_identifier}:")
for err in errors:
print(f" - {err}")
choice = input("Proceed with 'git pull'? (y/N): ").strip().lower()
if choice != "y":
continue
full_cmd = f"git pull {' '.join(extra_args)}"
if preview:
print(f"[Preview] In '{repo_dir}': {full_cmd}")
else:
print(f"Running in '{repo_dir}': {full_cmd}")
result = subprocess.run(full_cmd, cwd=repo_dir, shell=True)
if result.returncode != 0:
print(f"'git pull' for {repo_identifier} failed with exit code {result.returncode}.")
sys.exit(result.returncode)

View File

@@ -1,113 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Command resolver for repositories.
This module determines the correct command to expose via symlink.
It implements the following priority:
1. Explicit command in repo config → command
2. System package manager binary (/usr/...) → NO LINK (respect OS)
3. Nix profile binary (~/.nix-profile/bin/<id>) → command
4. Python / non-system console script on PATH → command
5. Fallback: repository's main.sh or main.py → command
6. If nothing is available → raise error
The actual symlink creation is handled by create_ink(). This resolver
only decides *what* should be used as the entrypoint, or whether no
link should be created at all.
"""
import os
import shutil
from typing import Optional
def resolve_command_for_repo(repo, repo_identifier: str, repo_dir: str) -> Optional[str]:
"""
Determine the command for this repository.
Returns:
str → path to the command (a symlink should be created)
None → do NOT create a link (e.g. system package already provides it)
On total failure (no suitable command found at any layer), this function
raises SystemExit with a descriptive error message.
"""
# ------------------------------------------------------------
# 1. Explicit command defined by repository config
# ------------------------------------------------------------
explicit = repo.get("command")
if explicit:
return explicit
home = os.path.expanduser("~")
def is_executable(path: str) -> bool:
return os.path.exists(path) and os.access(path, os.X_OK)
# ------------------------------------------------------------
# 2. System package manager binary via PATH
#
# If the binary lives under /usr/, we treat it as a system-managed
# package (e.g. installed via pacman/apt/yum). In that case, pkgmgr
# does NOT create a link at all and defers entirely to the OS.
# ------------------------------------------------------------
path_candidate = shutil.which(repo_identifier)
system_binary: Optional[str] = None
non_system_binary: Optional[str] = None
if path_candidate:
if path_candidate.startswith("/usr/"):
system_binary = path_candidate
else:
non_system_binary = path_candidate
if system_binary:
# Respect system package manager: do not create a link.
if repo.get("debug", False):
print(
f"[pkgmgr] System binary for '{repo_identifier}' found at "
f"{system_binary}; no symlink will be created."
)
return None
# ------------------------------------------------------------
# 3. Nix profile binary (~/.nix-profile/bin/<identifier>)
# ------------------------------------------------------------
nix_candidate = os.path.join(home, ".nix-profile", "bin", repo_identifier)
if is_executable(nix_candidate):
return nix_candidate
# ------------------------------------------------------------
# 4. Python / non-system console script on PATH
#
# Here we reuse the non-system PATH candidate (e.g. from a venv or
# a user-local install like ~/.local/bin). This is treated as a
# valid command target.
# ------------------------------------------------------------
if non_system_binary and is_executable(non_system_binary):
return non_system_binary
# ------------------------------------------------------------
# 5. Fallback: main.sh / main.py inside the repository
# ------------------------------------------------------------
main_sh = os.path.join(repo_dir, "main.sh")
main_py = os.path.join(repo_dir, "main.py")
if is_executable(main_sh):
return main_sh
if is_executable(main_py) or os.path.exists(main_py):
return main_py
# ------------------------------------------------------------
# 6. Nothing found → treat as a hard error
# ------------------------------------------------------------
raise SystemExit(
f"No executable command could be resolved for repository '{repo_identifier}'. "
"No explicit 'command' configured, no system-managed binary under /usr/, "
"no Nix profile binary, no non-system console script on PATH, and no "
"main.sh/main.py found in the repository."
)

View File

@@ -7,10 +7,10 @@ build-backend = "setuptools.build_meta"
[project]
name = "package-manager"
version = "0.5.0"
version = "1.3.0"
description = "Kevin's package-manager tool (pkgmgr)"
readme = "README.md"
requires-python = ">=3.11"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
@@ -39,13 +39,13 @@ pkgmgr = "pkgmgr.cli:main"
# -----------------------------
# setuptools configuration
# -----------------------------
# We use find_packages(), not a fixed list,
# and explicitly include pkgmgr* and config*
# Source layout: all packages live under "src/"
[tool.setuptools]
package-dir = { "" = "src", "config" = "config" }
[tool.setuptools.packages.find]
where = ["."]
where = ["src", "."]
include = ["pkgmgr*", "config*"]
# Ensure defaults.yaml is shipped inside wheels & nix builds
[tool.setuptools.package-data]
"config" = ["defaults.yaml"]

18
scripts/build/base.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
resolve_base_image() {
local distro="$1"
case "$distro" in
arch) echo "$BASE_IMAGE_ARCH" ;;
debian) echo "$BASE_IMAGE_DEBIAN" ;;
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
fedora) echo "$BASE_IMAGE_FEDORA" ;;
centos) echo "$BASE_IMAGE_CENTOS" ;;
*)
echo "ERROR: Unknown distro '$distro'" >&2
exit 1
;;
esac
}

120
scripts/build/image.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/usr/bin/env bash
set -euo pipefail
# Unified docker image builder for all distros.
#
# Supports:
# --missing Build only if image does not exist
# --no-cache Disable docker layer cache
# --target Dockerfile target (e.g. virgin|full)
# --tag Override image tag (default: pkgmgr-$distro[-$target])
#
# Requires:
# - env var: distro (arch|debian|ubuntu|fedora|centos)
# - base.sh in same dir
#
# Examples:
# distro=arch bash scripts/build/image.sh
# distro=arch bash scripts/build/image.sh --no-cache
# distro=arch bash scripts/build/image.sh --missing
# distro=arch bash scripts/build/image.sh --target virgin
# distro=arch bash scripts/build/image.sh --target virgin --missing
# distro=arch bash scripts/build/image.sh --tag myimg:arch
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/base.sh"
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}"
NO_CACHE=0
MISSING_ONLY=0
TARGET=""
IMAGE_TAG="" # derive later unless --tag is provided
usage() {
local default_tag="pkgmgr-${distro}"
if [[ -n "${TARGET:-}" ]]; then
default_tag="${default_tag}-${TARGET}"
fi
cat <<EOF
Usage: distro=<distro> $0 [--missing] [--no-cache] [--target <name>] [--tag <image>]
Options:
--missing Build only if the image does not already exist
--no-cache Build with --no-cache
--target <name> Build a specific Dockerfile target (e.g. virgin|full)
--tag <image> Override the output image tag (default: ${default_tag})
-h, --help Show help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--no-cache) NO_CACHE=1; shift ;;
--missing) MISSING_ONLY=1; shift ;;
--target)
TARGET="${2:-}"
if [[ -z "${TARGET}" ]]; then
echo "ERROR: --target requires a value (e.g. virgin|full)" >&2
exit 2
fi
shift 2
;;
--tag)
IMAGE_TAG="${2:-}"
if [[ -z "${IMAGE_TAG}" ]]; then
echo "ERROR: --tag requires a value" >&2
exit 2
fi
shift 2
;;
-h|--help) usage; exit 0 ;;
*)
echo "ERROR: Unknown argument: $1" >&2
usage
exit 2
;;
esac
done
# Auto-tag: if --tag not provided, derive from distro (+ target suffix)
if [[ -z "${IMAGE_TAG}" ]]; then
IMAGE_TAG="pkgmgr-${distro}"
if [[ -n "${TARGET}" ]]; then
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
fi
fi
BASE_IMAGE="$(resolve_base_image "$distro")"
if [[ "${MISSING_ONLY}" == "1" ]]; then
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
echo "[build] Image already exists: ${IMAGE_TAG} (skipping due to --missing)"
exit 0
fi
fi
echo
echo "------------------------------------------------------------"
echo "[build] Building image: ${IMAGE_TAG}"
echo "distro = ${distro}"
echo "BASE_IMAGE = ${BASE_IMAGE}"
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
echo "------------------------------------------------------------"
build_args=(--build-arg "BASE_IMAGE=${BASE_IMAGE}")
if [[ "${NO_CACHE}" == "1" ]]; then
build_args+=(--no-cache)
fi
if [[ -n "${TARGET}" ]]; then
build_args+=(--target "${TARGET}")
fi
build_args+=(-t "${IMAGE_TAG}" .)
docker build "${build_args[@]}"

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[entry] Using /src as working tree for package-manager..."
cd /src
# Optional: altes Paket entfernen
echo "[entry] Removing existing 'package-manager' Arch package (if installed)..."
pacman -Rns --noconfirm package-manager || true
# Build-Owner richtig setzen (falls /src vom Host kommt)
echo "[entry] Fixing ownership of /src for user 'builder'..."
chown -R builder:builder /src
echo "[entry] Rebuilding Arch package from /src as user 'builder'..."
su builder -c "cd /src && makepkg -s --noconfirm --clean"
echo "[entry] Installing freshly built package-manager-*.pkg.tar.*..."
pacman -U --noconfirm /src/package-manager-*.pkg.tar.*
echo "[entry] Handing off to pkgmgr with args: $*"
exec pkgmgr "$@"

39
scripts/docker/entry.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[docker] Starting package-manager container"
# ---------------------------------------------------------------------------
# Log distribution info
# ---------------------------------------------------------------------------
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
echo "[docker] Detected distro: ${ID:-unknown} (like: ${ID_LIKE:-})"
fi
# Always use /src (mounted from host) as working directory
echo "[docker] Using /src as working directory"
cd /src
# ---------------------------------------------------------------------------
# DEV mode: rebuild package-manager from the mounted /src tree
# ---------------------------------------------------------------------------
if [[ "${REINSTALL_PKGMGR:-0}" == "1" ]]; then
echo "[docker] DEV mode enabled (REINSTALL_PKGMGR=1)"
echo "[docker] Rebuilding package-manager from /src via scripts/installation/package.sh..."
bash scripts/installation/package.sh || exit 1
fi
# ---------------------------------------------------------------------------
# Hand off to pkgmgr or arbitrary command
# ---------------------------------------------------------------------------
if [[ $# -eq 0 ]]; then
echo "[docker] No arguments provided. Showing pkgmgr help..."
exec pkgmgr --help
else
echo "[docker] Executing command: $*"
exec "$@"
fi

409
scripts/init-nix.sh Normal file → Executable file
View File

@@ -1,44 +1,385 @@
#!/usr/bin/env bash
set -euo pipefail
echo ">>> Initializing Nix environment for package-manager..."
echo "[init-nix] Starting Nix initialization..."
# 1. /nix store
if [ ! -d /nix ]; then
echo ">>> Creating /nix store directory"
mkdir -m 0755 /nix
chown root:root /nix
fi
NIX_INSTALL_URL="${NIX_INSTALL_URL:-https://nixos.org/nix/install}"
NIX_DOWNLOAD_MAX_TIME="${NIX_DOWNLOAD_MAX_TIME:-300}"
NIX_DOWNLOAD_SLEEP_INTERVAL="${NIX_DOWNLOAD_SLEEP_INTERVAL:-20}"
# 2. Enable nix-daemon if available
if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q nix-daemon.service; then
echo ">>> Enabling nix-daemon.service"
systemctl enable --now nix-daemon.service 2>/dev/null || true
else
echo ">>> Warning: nix-daemon.service not found or systemctl not available."
fi
# ---------------------------------------------------------------------------
# Detect whether we are inside a container (Docker/Podman/etc.)
# ---------------------------------------------------------------------------
is_container() {
[[ -f /.dockerenv || -f /run/.containerenv ]] && return 0
grep -qiE 'docker|container|podman|lxc' /proc/1/cgroup 2>/dev/null && return 0
[[ -n "${container:-}" ]] && return 0
return 1
}
# 3. Ensure nix-users group
if ! getent group nix-users >/dev/null 2>&1; then
echo ">>> Creating nix-users group"
groupadd -r nix-users 2>/dev/null || true
fi
# ---------------------------------------------------------------------------
# Ensure Nix binaries are on PATH (additive, never destructive)
# ---------------------------------------------------------------------------
ensure_nix_on_path() {
if [[ -x /nix/var/nix/profiles/default/bin/nix ]]; then
PATH="/nix/var/nix/profiles/default/bin:$PATH"
fi
if [[ -x "$HOME/.nix-profile/bin/nix" ]]; then
PATH="$HOME/.nix-profile/bin:$PATH"
fi
if [[ -x /home/nix/.nix-profile/bin/nix ]]; then
PATH="/home/nix/.nix-profile/bin:$PATH"
fi
if [[ -d "$HOME/.local/bin" ]]; then
PATH="$HOME/.local/bin:$PATH"
fi
export PATH
}
# 4. Add users to nix-users (best-effort)
if command -v loginctl >/dev/null 2>&1; then
for user in $(loginctl list-users | awk 'NR>1 {print $2}'); do
if id "$user" >/dev/null 2>&1; then
echo ">>> Adding user '$user' to nix-users"
usermod -aG nix-users "$user" 2>/dev/null || true
# ---------------------------------------------------------------------------
# Resolve a path to a real executable (follows symlinks)
# ---------------------------------------------------------------------------
real_exe() {
local p="${1:-}"
[[ -z "$p" ]] && return 1
local r
r="$(readlink -f "$p" 2>/dev/null || echo "$p")"
[[ -x "$r" ]] && { echo "$r"; return 0; }
return 1
}
# ---------------------------------------------------------------------------
# Resolve nix binary path robustly (works across distros + Arch /usr/sbin)
# ---------------------------------------------------------------------------
resolve_nix_bin() {
local nix_cmd=""
nix_cmd="$(command -v nix 2>/dev/null || true)"
[[ -n "$nix_cmd" ]] && real_exe "$nix_cmd" && return 0
# IMPORTANT: prefer system locations before /usr/local to avoid self-symlink traps
[[ -x /usr/sbin/nix ]] && { echo "/usr/sbin/nix"; return 0; } # Arch package can land here
[[ -x /usr/bin/nix ]] && { echo "/usr/bin/nix"; return 0; }
[[ -x /bin/nix ]] && { echo "/bin/nix"; return 0; }
# /usr/local last, and only if it resolves to a real executable
[[ -e /usr/local/bin/nix ]] && real_exe "/usr/local/bin/nix" && return 0
[[ -x /nix/var/nix/profiles/default/bin/nix ]] && {
echo "/nix/var/nix/profiles/default/bin/nix"; return 0;
}
[[ -x "$HOME/.nix-profile/bin/nix" ]] && {
echo "$HOME/.nix-profile/bin/nix"; return 0;
}
[[ -x "$HOME/.local/bin/nix" ]] && {
echo "$HOME/.local/bin/nix"; return 0;
}
[[ -x /home/nix/.nix-profile/bin/nix ]] && {
echo "/home/nix/.nix-profile/bin/nix"; return 0;
}
return 1
}
# ---------------------------------------------------------------------------
# Ensure globally reachable nix symlink(s) (CI / non-login shells) - root only
#
# Key rule:
# - Never overwrite distro-managed nix locations (Arch may ship nix in /usr/sbin).
# - But for sudo secure_path (CentOS), /usr/local/bin is often NOT included.
# Therefore: also create /usr/bin/nix (and /usr/sbin/nix) ONLY if they do not exist.
# ---------------------------------------------------------------------------
ensure_global_nix_symlinks() {
local nix_bin="${1:-}"
[[ -z "$nix_bin" ]] && nix_bin="$(resolve_nix_bin 2>/dev/null || true)"
if [[ -z "$nix_bin" || ! -x "$nix_bin" ]]; then
echo "[init-nix] WARNING: nix binary not found, cannot create global symlink(s)."
return 0
fi
# Always link to the real executable to avoid /usr/local/bin/nix -> /usr/local/bin/nix
nix_bin="$(real_exe "$nix_bin" 2>/dev/null || echo "$nix_bin")"
local targets=()
# Always provide /usr/local/bin/nix for CI shells
mkdir -p /usr/local/bin 2>/dev/null || true
targets+=("/usr/local/bin/nix")
# Provide sudo-friendly locations only if they are NOT present (do not override distro paths)
if [[ ! -e /usr/bin/nix ]]; then
targets+=("/usr/bin/nix")
fi
if [[ ! -e /usr/sbin/nix ]]; then
targets+=("/usr/sbin/nix")
fi
local target current_real
for target in "${targets[@]}"; do
current_real=""
if [[ -e "$target" ]]; then
current_real="$(real_exe "$target" 2>/dev/null || true)"
fi
if [[ -n "$current_real" && "$current_real" == "$nix_bin" ]]; then
echo "[init-nix] $target already points to: $nix_bin"
continue
fi
# If something exists but is not the same (and we promised not to override), skip.
if [[ -e "$target" && "$target" != "/usr/local/bin/nix" ]]; then
echo "[init-nix] WARNING: $target exists; not overwriting."
continue
fi
if ln -sf "$nix_bin" "$target" 2>/dev/null; then
echo "[init-nix] Ensured $target -> $nix_bin"
else
echo "[init-nix] WARNING: Failed to ensure $target symlink."
fi
done
elif command -v logname >/dev/null 2>&1; then
USERNAME="$(logname 2>/dev/null || true)"
if [ -n "$USERNAME" ] && id "$USERNAME" >/dev/null 2>&1; then
echo ">>> Adding user '$USERNAME' to nix-users"
usermod -aG nix-users "$USERNAME" 2>/dev/null || true
fi
fi
}
echo ">>> Nix initialization complete."
echo ">>> You may need to log out and log back in to activate group membership."
# ---------------------------------------------------------------------------
# Ensure user-level nix symlink (works without root; CI-safe)
# ---------------------------------------------------------------------------
ensure_user_nix_symlink() {
local nix_bin="${1:-}"
[[ -z "$nix_bin" ]] && nix_bin="$(resolve_nix_bin 2>/dev/null || true)"
if [[ -z "$nix_bin" || ! -x "$nix_bin" ]]; then
echo "[init-nix] WARNING: nix binary not found, cannot create user symlink."
return 0
fi
nix_bin="$(real_exe "$nix_bin" 2>/dev/null || echo "$nix_bin")"
mkdir -p "$HOME/.local/bin" 2>/dev/null || true
ln -sf "$nix_bin" "$HOME/.local/bin/nix"
echo "[init-nix] Ensured $HOME/.local/bin/nix -> $nix_bin"
PATH="$HOME/.local/bin:$PATH"
export PATH
if [[ -w "$HOME/.profile" ]] && ! grep -q 'init-nix.sh' "$HOME/.profile" 2>/dev/null; then
cat >>"$HOME/.profile" <<'EOF'
# PATH for nix (added by package-manager init-nix.sh)
if [ -d "$HOME/.local/bin" ]; then
PATH="$HOME/.local/bin:$PATH"
fi
EOF
fi
}
# ---------------------------------------------------------------------------
# Ensure Nix build group and users exist (build-users-group = nixbld) - root only
# ---------------------------------------------------------------------------
ensure_nix_build_group() {
if ! getent group nixbld >/dev/null 2>&1; then
echo "[init-nix] Creating group 'nixbld'..."
groupadd -r nixbld
fi
for i in $(seq 1 10); do
if ! id "nixbld$i" >/dev/null 2>&1; then
echo "[init-nix] Creating build user nixbld$i..."
useradd -r -g nixbld -G nixbld -s /usr/sbin/nologin "nixbld$i"
fi
done
}
# ---------------------------------------------------------------------------
# Download and run Nix installer with retry
# Usage: install_nix_with_retry daemon|no-daemon [run_as_user]
# ---------------------------------------------------------------------------
install_nix_with_retry() {
local mode="$1"
local run_as="${2:-}"
local installer elapsed=0 mode_flag
case "$mode" in
daemon) mode_flag="--daemon" ;;
no-daemon) mode_flag="--no-daemon" ;;
*)
echo "[init-nix] ERROR: Invalid mode '$mode' (expected 'daemon' or 'no-daemon')."
exit 1
;;
esac
installer="$(mktemp -t nix-installer.XXXXXX)"
chmod 0644 "$installer"
echo "[init-nix] Downloading Nix installer from $NIX_INSTALL_URL (max ${NIX_DOWNLOAD_MAX_TIME}s)..."
while true; do
if curl -fL "$NIX_INSTALL_URL" -o "$installer"; then
echo "[init-nix] Successfully downloaded installer to $installer"
break
fi
elapsed=$((elapsed + NIX_DOWNLOAD_SLEEP_INTERVAL))
echo "[init-nix] WARNING: Download failed. Retrying in ${NIX_DOWNLOAD_SLEEP_INTERVAL}s (elapsed ${elapsed}s)..."
if (( elapsed >= NIX_DOWNLOAD_MAX_TIME )); then
echo "[init-nix] ERROR: Giving up after ${elapsed}s trying to download Nix installer."
rm -f "$installer"
exit 1
fi
sleep "$NIX_DOWNLOAD_SLEEP_INTERVAL"
done
if [[ -n "$run_as" ]]; then
chown "$run_as:$run_as" "$installer" 2>/dev/null || true
echo "[init-nix] Running installer as user '$run_as' ($mode_flag)..."
if command -v sudo >/dev/null 2>&1; then
sudo -u "$run_as" bash -lc "sh '$installer' $mode_flag"
else
su - "$run_as" -c "sh '$installer' $mode_flag"
fi
else
echo "[init-nix] Running installer as current user ($mode_flag)..."
sh "$installer" "$mode_flag"
fi
rm -f "$installer"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
# Fast path: already available
if command -v nix >/dev/null 2>&1; then
echo "[init-nix] Nix already available on PATH: $(command -v nix)"
ensure_nix_on_path
if [[ "${EUID:-0}" -eq 0 ]]; then
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
else
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
fi
return 0
fi
ensure_nix_on_path
if command -v nix >/dev/null 2>&1; then
echo "[init-nix] Nix found after PATH adjustment: $(command -v nix)"
if [[ "${EUID:-0}" -eq 0 ]]; then
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
else
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
fi
return 0
fi
local IN_CONTAINER=0
if is_container; then
IN_CONTAINER=1
echo "[init-nix] Detected container environment."
else
echo "[init-nix] No container detected."
fi
# -------------------------------------------------------------------------
# Container + root: dedicated "nix" user, single-user install
# -------------------------------------------------------------------------
if [[ "$IN_CONTAINER" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
echo "[init-nix] Container + root: installing as 'nix' user (single-user)."
ensure_nix_build_group
if ! id nix >/dev/null 2>&1; then
echo "[init-nix] Creating user 'nix'..."
local BASH_SHELL
BASH_SHELL="$(command -v bash || true)"
[[ -z "$BASH_SHELL" ]] && BASH_SHELL="/bin/sh"
useradd -m -r -g nixbld -s "$BASH_SHELL" nix
fi
if [[ ! -d /nix ]]; then
echo "[init-nix] Creating /nix with owner nix:nixbld..."
mkdir -m 0755 /nix
chown nix:nixbld /nix
else
local current_owner current_group
current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')"
current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')"
if [[ "$current_owner" != "nix" || "$current_group" != "nixbld" ]]; then
echo "[init-nix] Fixing /nix ownership from $current_owner:$current_group to nix:nixbld..."
chown -R nix:nixbld /nix
fi
fi
install_nix_with_retry "no-daemon" "nix"
ensure_nix_on_path
# Ensure stable global symlink(s) (sudo secure_path friendly)
ensure_global_nix_symlinks "/home/nix/.nix-profile/bin/nix"
# Ensure non-root users can traverse and execute nix user profile
if [[ -d /home/nix ]]; then
chmod o+rx /home/nix 2>/dev/null || true
fi
if [[ -d /home/nix/.nix-profile ]]; then
chmod -R o+rx /home/nix/.nix-profile 2>/dev/null || true
fi
# -------------------------------------------------------------------------
# Host (no container)
# -------------------------------------------------------------------------
else
if command -v systemctl >/dev/null 2>&1; then
echo "[init-nix] Host with systemd: using multi-user install (--daemon)."
if [[ "${EUID:-0}" -eq 0 ]]; then
ensure_nix_build_group
fi
install_nix_with_retry "daemon"
else
echo "[init-nix] No systemd detected: using single-user install (--no-daemon)."
if [[ "${EUID:-0}" -eq 0 ]]; then
ensure_nix_build_group
fi
install_nix_with_retry "no-daemon"
fi
fi
# -------------------------------------------------------------------------
# After install: PATH + symlink(s)
# -------------------------------------------------------------------------
ensure_nix_on_path
local nix_bin_post
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"
if [[ "${EUID:-0}" -eq 0 ]]; then
ensure_global_nix_symlinks "$nix_bin_post"
else
ensure_user_nix_symlink "$nix_bin_post"
fi
# Final verification (must succeed for CI)
if ! command -v nix >/dev/null 2>&1; then
echo "[init-nix] ERROR: nix not found after installation."
echo "[init-nix] DEBUG: resolved nix path = ${nix_bin_post:-<empty>}"
echo "[init-nix] DEBUG: PATH = $PATH"
exit 1
fi
echo "[init-nix] Nix successfully available at: $(command -v nix)"
echo "[init-nix] Nix initialization complete."
}
main "$@"

View File

@@ -0,0 +1,88 @@
#!/usr/bin/env bash
set -euo pipefail
# ------------------------------------------------------------
# aur-builder-setup.sh
#
# Setup helper for an 'aur_builder' user and yay on Arch-based
# systems. Intended for host usage and can also be used in
# containers if desired.
# ------------------------------------------------------------
echo "[aur-builder-setup] Checking for pacman..."
if ! command -v pacman >/dev/null 2>&1; then
echo "[aur-builder-setup] pacman not found this is not an Arch-based system. Skipping."
exit 0
fi
if [[ "${EUID:-0}" -ne 0 ]]; then
ROOT_CMD="sudo"
else
ROOT_CMD=""
fi
echo "[aur-builder-setup] Installing base-devel, git, sudo..."
${ROOT_CMD} pacman -Syu --noconfirm
${ROOT_CMD} pacman -S --needed --noconfirm base-devel git sudo
echo "[aur-builder-setup] Ensuring aur_builder group/user..."
if ! getent group aur_builder >/dev/null 2>&1; then
${ROOT_CMD} groupadd -r aur_builder
fi
if ! id -u aur_builder >/dev/null 2>&1; then
${ROOT_CMD} useradd -m -r -g aur_builder -s /bin/bash aur_builder
fi
echo "[aur-builder-setup] Configuring sudoers for aur_builder..."
${ROOT_CMD} bash -c "echo '%aur_builder ALL=(ALL) NOPASSWD: /usr/bin/pacman' > /etc/sudoers.d/aur_builder"
${ROOT_CMD} chmod 0440 /etc/sudoers.d/aur_builder
if command -v sudo >/dev/null 2>&1; then
RUN_AS_AUR=(sudo -u aur_builder bash -lc)
else
RUN_AS_AUR=(su - aur_builder -c)
fi
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
if ! "${RUN_AS_AUR[@]}" 'command -v yay >/dev/null 2>&1'; then
echo "[aur-builder-setup] yay not found starting retry sequence for download..."
MAX_TIME=300
SLEEP_INTERVAL=20
ELAPSED=0
while true; do
if "${RUN_AS_AUR[@]}" '
set -euo pipefail
cd ~
rm -rf yay || true
git clone https://aur.archlinux.org/yay.git yay
'; then
echo "[aur-builder-setup] yay repository cloned successfully."
break
fi
echo "[aur-builder-setup] git clone failed (likely 504). Retrying in ${SLEEP_INTERVAL}s..."
sleep "${SLEEP_INTERVAL}"
ELAPSED=$((ELAPSED + SLEEP_INTERVAL))
if (( ELAPSED >= MAX_TIME )); then
echo "[aur-builder-setup] ERROR: Aborted after 5 minutes of retry attempts."
exit 1
fi
done
# Now build yay after successful clone
"${RUN_AS_AUR[@]}" '
set -euo pipefail
cd ~/yay
makepkg -si --noconfirm
'
else
echo "[aur-builder-setup] yay already installed."
fi
echo "[aur-builder-setup] Done."

View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[arch/dependencies] Installing Arch build dependencies..."
pacman -Syu --noconfirm
pacman -S --noconfirm --needed \
base-devel \
git \
rsync \
curl \
ca-certificates \
python \
xz
pacman -Scc --noconfirm
# Always run AUR builder setup for Arch
AUR_SETUP="${SCRIPT_DIR}/aur-builder-setup.sh"
if [[ ! -x "${AUR_SETUP}" ]]; then
echo "[arch/dependencies] ERROR: AUR builder setup script not found or not executable: ${AUR_SETUP}"
exit 1
fi
echo "[arch/dependencies] Running AUR builder setup..."
bash "${AUR_SETUP}"
echo "[arch/dependencies] Done."

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[arch/package] Building Arch package (makepkg --nodeps) in an isolated build dir..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
# We must not build inside /src (mounted repo). Build in /tmp to avoid permission issues.
BUILD_ROOT="/tmp/package-manager-arch-build"
PKG_SRC_DIR="${PROJECT_ROOT}/packaging/arch"
PKG_BUILD_DIR="${BUILD_ROOT}/packaging/arch"
if [[ ! -f "${PKG_SRC_DIR}/PKGBUILD" ]]; then
echo "[arch/package] ERROR: PKGBUILD not found in ${PKG_SRC_DIR}"
exit 1
fi
echo "[arch/package] Preparing build directory: ${BUILD_ROOT}"
rm -rf "${BUILD_ROOT}"
mkdir -p "${BUILD_ROOT}"
echo "[arch/package] Syncing project sources to ${BUILD_ROOT}..."
# Keep it simple: copy everything; adjust excludes if needed later.
rsync -a --delete \
--exclude '.git' \
--exclude '.venv' \
--exclude '.venvs' \
--exclude '__pycache__' \
--exclude '*.pyc' \
"${PROJECT_ROOT}/" "${BUILD_ROOT}/"
if [[ ! -d "${PKG_BUILD_DIR}" ]]; then
echo "[arch/package] ERROR: Build PKG dir missing: ${PKG_BUILD_DIR}"
exit 1
fi
# ------------------------------------------------------------
# Unprivileged user for Arch package build (makepkg)
# ------------------------------------------------------------
if ! id aur_builder >/dev/null 2>&1; then
echo "[arch/package] ERROR: user 'aur_builder' not found. Run scripts/installation/arch/aur-builder-setup.sh first."
exit 1
fi
echo "[arch/package] Using 'aur_builder' user for makepkg..."
chown -R aur_builder:aur_builder "${BUILD_ROOT}"
echo "[arch/package] Running makepkg in: ${PKG_BUILD_DIR}"
su aur_builder -c "cd '${PKG_BUILD_DIR}' && rm -f package-manager-*.pkg.tar.* && makepkg --noconfirm --clean --nodeps"
echo "[arch/package] Installing generated Arch package..."
pkg_path="$(find "${PKG_BUILD_DIR}" -maxdepth 1 -type f -name 'package-manager-*.pkg.tar.*' | head -n1)"
if [[ -z "${pkg_path}" ]]; then
echo "[arch/package] ERROR: Built package not found in ${PKG_BUILD_DIR}"
exit 1
fi
pacman -U --noconfirm "${pkg_path}"
echo "[arch/package] Cleanup build directory..."
rm -rf "${BUILD_ROOT}"
echo "[arch/package] Done."

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[centos/dependencies] Installing CentOS build dependencies..."
dnf -y update
dnf -y install \
git \
rsync \
rpm-build \
make \
gcc \
bash \
curl-minimal \
ca-certificates \
python3 \
sudo \
xz
dnf clean all
# -----------------------------------------------------------------------------
# Persist CA bundle configuration system-wide (virgin-compatible)
# -----------------------------------------------------------------------------
detect_ca_bundle() {
local candidates=(
/etc/pki/tls/certs/ca-bundle.crt
/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem
/etc/ssl/certs/ca-certificates.crt
/etc/ssl/cert.pem
/etc/ssl/ca-bundle.pem
)
for path in "${candidates[@]}"; do
if [[ -f "$path" ]]; then
echo "$path"
return 0
fi
done
return 1
}
CA_BUNDLE="$(detect_ca_bundle || true)"
if [[ -n "${CA_BUNDLE}" ]]; then
echo "[centos/dependencies] Persisting CA bundle: ${CA_BUNDLE}"
# 1) Make it available for login shells
cat >/etc/profile.d/pkgmgr-ca.sh <<EOF
# Generated by package-manager
export NIX_SSL_CERT_FILE="${CA_BUNDLE}"
export SSL_CERT_FILE="${CA_BUNDLE}"
export REQUESTS_CA_BUNDLE="${CA_BUNDLE}"
export GIT_SSL_CAINFO="${CA_BUNDLE}"
EOF
chmod 0644 /etc/profile.d/pkgmgr-ca.sh
# 2) Ensure Nix uses it even without environment variables
mkdir -p /etc/nix
if [[ -f /etc/nix/nix.conf ]]; then
# Replace existing ssl-cert-file or append it
if grep -qE '^\s*ssl-cert-file\s*=' /etc/nix/nix.conf; then
sed -i "s|^\s*ssl-cert-file\s*=.*|ssl-cert-file = ${CA_BUNDLE}|" /etc/nix/nix.conf
else
echo "ssl-cert-file = ${CA_BUNDLE}" >>/etc/nix/nix.conf
fi
else
echo "ssl-cert-file = ${CA_BUNDLE}" >/etc/nix/nix.conf
fi
else
echo "[centos/dependencies] WARNING: No CA bundle found after installing ca-certificates."
fi
echo "[centos/dependencies] Done."

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[centos/package] Setting up rpmbuild directories..."
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
SPEC_PATH="${PROJECT_ROOT}/packaging/fedora/package-manager.spec"
if [[ ! -f "${SPEC_PATH}" ]]; then
echo "[centos/package] ERROR: SPEC file not found: ${SPEC_PATH}"
exit 1
fi
echo "[centos/package] Extracting version from package-manager.spec..."
version="$(grep -E '^Version:' "${SPEC_PATH}" | awk '{print $2}')"
if [[ -z "${version}" ]]; then
echo "ERROR: Version missing!"
exit 1
fi
srcdir="package-manager-${version}"
echo "[centos/package] Preparing source tree: ${srcdir}"
rm -rf "/tmp/${srcdir}"
mkdir -p "/tmp/${srcdir}"
cp -a "${PROJECT_ROOT}/." "/tmp/${srcdir}/"
echo "[centos/package] Creating source tarball..."
tar czf "/root/rpmbuild/SOURCES/${srcdir}.tar.gz" -C /tmp "${srcdir}"
echo "[centos/package] Copying SPEC..."
cp "${SPEC_PATH}" /root/rpmbuild/SPECS/
echo "[centos/package] Running rpmbuild..."
cd /root/rpmbuild/SPECS
rpmbuild -bb package-manager.spec
echo "[centos/package] Installing generated RPM (local, offline, forced reinstall)..."
rpm_path="$(find /root/rpmbuild/RPMS -name 'package-manager-*.rpm' | head -n1)"
if [[ -z "${rpm_path}" ]]; then
echo "ERROR: RPM not found!"
exit 1
fi
# ------------------------------------------------------------
# Forced reinstall, always overwrite old version
# ------------------------------------------------------------
echo "[centos/package] Forcing reinstall via rpm -Uvh --replacepkgs --force"
rpm -Uvh --replacepkgs --force "${rpm_path}"
# Keep structure: remove temp directory afterwards
rm -rf "/tmp/${srcdir}"
echo "[centos/package] Done."

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[debian/dependencies] Installing Debian build dependencies..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
debhelper \
dpkg-dev \
git \
rsync \
bash \
curl \
ca-certificates \
python3 \
python3-venv \
xz-utils
rm -rf /var/lib/apt/lists/*
echo "[debian/dependencies] Done."

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[debian/package] Building Debian package..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
BUILD_ROOT="/tmp/package-manager-debian-build"
rm -rf "${BUILD_ROOT}"
mkdir -p "${BUILD_ROOT}"
echo "[debian/package] Syncing project sources to ${BUILD_ROOT}..."
rsync -a \
--exclude 'packaging/debian' \
"${PROJECT_ROOT}/" "${BUILD_ROOT}/"
echo "[debian/package] Overlaying debian/ metadata from packaging/debian..."
mkdir -p "${BUILD_ROOT}/debian"
cp -a "${PROJECT_ROOT}/packaging/debian/." "${BUILD_ROOT}/debian/"
cd "${BUILD_ROOT}"
echo "[debian/package] Running dpkg-buildpackage..."
dpkg-buildpackage -us -uc -b
echo "[debian/package] Installing generated DEB package..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y ./../package-manager_*.deb
rm -rf /var/lib/apt/lists/*
echo "[debian/package] Done."

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib.sh"
OS_ID="$(detect_os_id)"
echo "[run-dependencies] Detected OS: ${OS_ID}"
case "${OS_ID}" in
arch|debian|ubuntu|fedora|centos)
DEP_SCRIPT="${SCRIPT_DIR}/${OS_ID}/dependencies.sh"
;;
*)
echo "[run-dependencies] Unsupported OS: ${OS_ID}"
exit 1
;;
esac
if [[ ! -f "${DEP_SCRIPT}" ]]; then
echo "[run-dependencies] Dependency script not found: ${DEP_SCRIPT}"
exit 1
fi
echo "[run-dependencies] Executing: ${DEP_SCRIPT}"
exec bash "${DEP_SCRIPT}"

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[fedora/dependencies] Installing Fedora build dependencies..."
dnf -y update
dnf -y install \
git \
rsync \
rpm-build \
make \
gcc \
bash \
curl \
ca-certificates \
python3 \
xz
dnf clean all
echo "[fedora/dependencies] Done."

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[fedora/package] Setting up rpmbuild directories..."
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
SPEC_PATH="${PROJECT_ROOT}/packaging/fedora/package-manager.spec"
if [[ ! -f "${SPEC_PATH}" ]]; then
echo "[fedora/package] ERROR: SPEC file not found: ${SPEC_PATH}"
exit 1
fi
echo "[fedora/package] Extracting version from package-manager.spec..."
version="$(grep -E '^Version:' "${SPEC_PATH}" | awk '{print $2}')"
if [[ -z "${version}" ]]; then
echo "ERROR: Version missing!"
exit 1
fi
srcdir="package-manager-${version}"
echo "[fedora/package] Preparing source tree: ${srcdir}"
rm -rf "/tmp/${srcdir}"
mkdir -p "/tmp/${srcdir}"
cp -a "${PROJECT_ROOT}/." "/tmp/${srcdir}/"
echo "[fedora/package] Creating source tarball..."
tar czf "/root/rpmbuild/SOURCES/${srcdir}.tar.gz" -C /tmp "${srcdir}"
echo "[fedora/package] Copying SPEC..."
cp "${SPEC_PATH}" /root/rpmbuild/SPECS/
echo "[fedora/package] Running rpmbuild..."
cd /root/rpmbuild/SPECS
rpmbuild -bb package-manager.spec
echo "[fedora/package] Installing generated RPM (local, offline, forced reinstall)..."
rpm_path="$(find /root/rpmbuild/RPMS -name 'package-manager-*.rpm' | head -n1)"
if [[ -z "${rpm_path}" ]]; then
echo "ERROR: RPM not found!"
exit 1
fi
# Always force (re)install the freshly built RPM, even if the same
# version is already installed. This is what we want in dev/test containers.
rpm -Uvh --replacepkgs --force "${rpm_path}"
rm -rf "/tmp/${srcdir}"
echo "[fedora/package] Done."

12
scripts/installation/lib.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
detect_os_id() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
echo "${ID:-unknown}"
else
echo "unknown"
fi
}

15
scripts/installation/main.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "[installation/install] Warning: Installation is just possible via root."
exit 0
fi
echo "[installation] Running as root (EUID=0)."
echo "[installation] Install Package Dependencies..."
bash scripts/installation/dependencies.sh
echo "[installation] Install Distribution Package..."
bash scripts/installation/package.sh
echo "[installation] Root/system setup complete."
exit 0

35
scripts/installation/package.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/lib.sh"
OS_ID="$(detect_os_id)"
# Map Manjaro to Arch
if [[ "${OS_ID}" == "manjaro" ]]; then
echo "[package] Mapping OS 'manjaro' → 'arch'"
OS_ID="arch"
fi
echo "[package] Detected OS: ${OS_ID}"
case "${OS_ID}" in
arch|debian|ubuntu|fedora|centos)
PKG_SCRIPT="${SCRIPT_DIR}/${OS_ID}/package.sh"
;;
*)
echo "[package] Unsupported OS: ${OS_ID}"
exit 1
;;
esac
if [[ ! -f "${PKG_SCRIPT}" ]]; then
echo "[package] Package script not found: ${PKG_SCRIPT}"
exit 1
fi
echo "[package] Executing: ${PKG_SCRIPT}"
exec bash "${PKG_SCRIPT}"

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[ubuntu/dependencies] Installing Ubuntu build dependencies..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
debhelper \
dpkg-dev \
git \
tzdata \
lsb-release \
rsync \
bash \
curl \
make \
python3 \
python3-venv \
ca-certificates \
xz-utils
rm -rf /var/lib/apt/lists/*
echo "[ubuntu/dependencies] Done."

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[ubuntu/package] Building Ubuntu (Debian-style) package..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
BUILD_ROOT="/tmp/package-manager-ubuntu-build"
rm -rf "${BUILD_ROOT}"
mkdir -p "${BUILD_ROOT}"
echo "[ubuntu/package] Syncing project sources to ${BUILD_ROOT}..."
rsync -a \
--exclude 'packaging/debian' \
"${PROJECT_ROOT}/" "${BUILD_ROOT}/"
echo "[ubuntu/package] Overlaying debian/ metadata from packaging/debian..."
mkdir -p "${BUILD_ROOT}/debian"
cp -a "${PROJECT_ROOT}/packaging/debian/." "${BUILD_ROOT}/debian/"
cd "${BUILD_ROOT}"
echo "[ubuntu/package] Running dpkg-buildpackage..."
dpkg-buildpackage -us -uc -b
echo "[ubuntu/package] Installing generated DEB package..."
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y ./../package-manager_*.deb
rm -rf /var/lib/apt/lists/*
echo "[ubuntu/package] Done."

40
scripts/pkgmgr-wrapper.sh Normal file → Executable file
View File

@@ -1,10 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
# Ensure NIX_CONFIG has our defaults if not already set
if [[ -z "${NIX_CONFIG:-}" ]]; then
export NIX_CONFIG="experimental-features = nix-command flakes"
fi
FLAKE_DIR="/usr/lib/package-manager"
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
# ---------------------------------------------------------------------------
# Try to ensure that "nix" is on PATH (common locations + container user)
# ---------------------------------------------------------------------------
if ! command -v nix >/dev/null 2>&1; then
CANDIDATES=(
"/nix/var/nix/profiles/default/bin/nix"
"${HOME:-/root}/.nix-profile/bin/nix"
"/home/nix/.nix-profile/bin/nix"
)
for candidate in "${CANDIDATES[@]}"; do
if [[ -x "$candidate" ]]; then
PATH="$(dirname "$candidate"):${PATH}"
export PATH
break
fi
done
fi
# ---------------------------------------------------------------------------
# If nix is still missing, try to run init-nix.sh once
# ---------------------------------------------------------------------------
if ! command -v nix >/dev/null 2>&1; then
if [[ -x "${FLAKE_DIR}/init-nix.sh" ]]; then
"${FLAKE_DIR}/init-nix.sh" || true
fi
fi
# ---------------------------------------------------------------------------
# Primary path: use Nix flake if available
# ---------------------------------------------------------------------------
if command -v nix >/dev/null 2>&1; then
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
fi
echo "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH after init."
echo "[pkgmgr-wrapper] Nix is required to run pkgmgr (no Python fallback)."
exit 1

9
scripts/setup/nix.sh Executable file
View File

@@ -0,0 +1,9 @@
# ------------------------------------------------------------
# Nix shell mode: do not touch venv, only run main.py install
# ------------------------------------------------------------
echo "[setup] Nix mode enabled (NIX_ENABLED=1)."
echo "[setup] Skipping virtualenv creation and dependency installation."
echo "[setup] Running main.py install via system python3..."
python3 main.py install
echo "[setup] Setup finished (Nix mode)."

98
scripts/setup/venv.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[setup] Starting setup..."
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${PROJECT_ROOT}"
VENV_DIR="${HOME}/.venvs/pkgmgr"
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'
# ------------------------------------------------------------
# Normal user mode: dev setup with venv
# ------------------------------------------------------------
echo "[setup] Running in normal user mode (developer setup)."
echo "[setup] Ensuring main.py is executable..."
chmod +x main.py || true
echo "[setup] Ensuring global virtualenv root: ${HOME}/.venvs"
mkdir -p "${HOME}/.venvs"
echo "[setup] Creating/updating virtualenv via helper..."
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${PROJECT_ROOT}"
PIP_EDITABLE="${PKGMGR_PIP_EDITABLE:-1}"
PIP_EXTRAS="${PKGMGR_PIP_EXTRAS:-}"
PREFER_NIX="${PKGMGR_PREFER_NIX:-0}"
echo "[venv] Using VENV_DIR=${VENV_DIR}"
if [[ "${PREFER_NIX}" == "1" ]]; then
echo "[venv] PKGMGR_PREFER_NIX=1 set."
echo "[venv] Hint: Use Nix instead of a venv for reproducible installs:"
echo "[venv] nix develop"
echo "[venv] nix run .#pkgmgr -- --help"
exit 2
fi
echo "[venv] Ensuring virtualenv parent directory exists..."
mkdir -p "$(dirname "${VENV_DIR}")"
if [[ ! -d "${VENV_DIR}" ]]; then
echo "[venv] Creating virtual environment at: ${VENV_DIR}"
python3 -m venv "${VENV_DIR}"
else
echo "[venv] Virtual environment already exists at: ${VENV_DIR}"
fi
echo "[venv] Installing Python tooling into venv..."
"${VENV_DIR}/bin/python" -m ensurepip --upgrade
"${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel
# ---------------------------------------------------------------------------
# Install dependencies
# ---------------------------------------------------------------------------
if [[ -f "pyproject.toml" ]]; then
echo "[venv] Detected pyproject.toml. Installing project via pip..."
target="."
if [[ -n "${PIP_EXTRAS}" ]]; then
target=".[${PIP_EXTRAS}]"
fi
if [[ "${PIP_EDITABLE}" == "1" ]]; then
echo "[venv] pip install -e ${target}"
"${VENV_DIR}/bin/pip" install -e "${target}"
else
echo "[venv] pip install ${target}"
"${VENV_DIR}/bin/pip" install "${target}"
fi
else
echo "[venv] No pyproject.toml found. Skipping dependency installation."
fi
echo "[venv] Done."
echo "[setup] Ensuring ~/.bashrc and ~/.zshrc exist..."
touch "${HOME}/.bashrc" "${HOME}/.zshrc"
echo "[setup] Ensuring venv auto-activation is present in shell rc files..."
for rc in "${HOME}/.bashrc" "${HOME}/.zshrc"; do
if ! grep -qxF "${RC_LINE}" "$rc"; then
echo "${RC_LINE}" >> "$rc"
echo "[setup] Appended auto-activation to $rc"
else
echo "[setup] Auto-activation already present in $rc"
fi
done
echo "[setup] Running main.py install via venv Python..."
"${VENV_DIR}/bin/python" main.py install
echo
echo "[setup] Developer setup complete."
echo "Restart your shell (or run 'exec bash' or 'exec zsh') to activate the environment."

12
scripts/test/test-common.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -euo pipefail
detect_container_distro() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
echo "${ID:-unknown}"
else
echo "unknown"
fi
}

60
scripts/test/test-e2e.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
set -euo pipefail
echo "============================================================"
echo ">>> Running E2E tests: $distro"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_store_${distro}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
--workdir /src \
"pkgmgr-${distro}" \
bash -lc '
set -euo pipefail
# Load distro info
if [ -f /etc/os-release ]; then
. /etc/os-release
fi
echo "Running tests inside distro: ${ID:-unknown}"
# Load Nix environment if available
if [ -f "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" ]; then
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
fi
if [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
PATH="/nix/var/nix/profiles/default/bin:$HOME/.nix-profile/bin:$PATH"
command -v nix >/dev/null || {
echo "ERROR: nix not found."
exit 1
}
# Mark the mounted repository as safe to avoid Git ownership errors.
# Newer Git (e.g. on Ubuntu) complains about the gitdir (/src/.git),
# older versions about the worktree (/src). Nix turns "." into the
# flake input "git+file:///src", which then uses Git under the hood.
if command -v git >/dev/null 2>&1; then
# Worktree path
git config --global --add safe.directory /src || true
# Gitdir path shown in the "dubious ownership" error
git config --global --add safe.directory /src/.git || true
# Ephemeral CI containers: allow all paths as a last resort
git config --global --add safe.directory '*' || true
fi
# Run the E2E tests inside the Nix development shell
nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \
-s /src/tests/e2e \
-p "$TEST_PATTERN"
'

48
scripts/test/test-env-nix.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
IMAGE="pkgmgr-${distro}"
echo "============================================================"
echo ">>> Running Nix flake-only test in ${distro} container"
echo ">>> Image: ${IMAGE}"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_store_${distro}:/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
--workdir /src \
-e REINSTALL_PKGMGR=1 \
"${IMAGE}" \
bash -lc '
set -euo pipefail
if command -v git >/dev/null 2>&1; then
git config --global --add safe.directory /src || true
git config --global --add safe.directory /src/.git || true
git config --global --add safe.directory "*" || true
fi
echo ">>> preflight: nix must exist in image"
if ! command -v nix >/dev/null 2>&1; then
echo "NO_NIX"
echo "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (distro='"${distro}"')"
echo "HINT: Ensure Nix is installed during image build for this distro."
exit 1
fi
echo ">>> nix version"
nix --version
echo ">>> nix flake show"
nix flake show . --no-write-lock-file >/dev/null
echo ">>> nix build .#default"
nix build .#default --no-link --no-write-lock-file
echo ">>> nix run .#pkgmgr -- --help"
nix run .#pkgmgr -- --help --no-write-lock-file
echo ">>> OK: Nix flake-only test succeeded."
'

View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
IMAGE="pkgmgr-$distro"
echo
echo "------------------------------------------------------------"
echo ">>> Testing VENV: $IMAGE"
echo "------------------------------------------------------------"
echo "[test-env-virtual] Inspect image metadata:"
docker image inspect "$IMAGE" | sed -n '1,40p'
echo "[test-env-virtual] Running: docker run --rm --entrypoint pkgmgr $IMAGE --help"
echo
# Run the command and capture the output
if OUTPUT=$(docker run --rm \
-e REINSTALL_PKGMGR=1 \
-v pkgmgr_nix_store_${distro}:/nix \
-v "$(pwd):/src" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
"$IMAGE" 2>&1); then
echo "$OUTPUT"
echo
echo "[test-env-virtual] SUCCESS: $IMAGE responded to 'pkgmgr --help'"
else
echo "$OUTPUT"
echo
echo "[test-env-virtual] ERROR: $IMAGE failed to run 'pkgmgr --help'"
exit 1
fi

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
echo "============================================================"
echo ">>> Running INTEGRATION tests in ${distro} container"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v pkgmgr_nix_store_${distro}:/nix \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
--workdir /src \
-e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${distro}" \
bash -lc '
set -e;
git config --global --add safe.directory /src || true;
nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \
-s tests/integration \
-t /src \
-p "$TEST_PATTERN";
'

24
scripts/test/test-unit.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euo pipefail
echo "============================================================"
echo ">>> Running UNIT tests in ${distro} container"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v pkgmgr_nix_store_${distro}:/nix \
--workdir /src \
-e REINSTALL_PKGMGR=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
"pkgmgr-${distro}" \
bash -lc '
set -e;
git config --global --add safe.directory /src || true;
nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \
-s tests/unit \
-t /src \
-p "$TEST_PATTERN";
'

34
scripts/uninstall.sh Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
set -euo pipefail
echo "[uninstall] Starting pkgmgr uninstall..."
VENV_DIR="${HOME}/.venvs/pkgmgr"
# ------------------------------------------------------------
# Remove virtual environment
# ------------------------------------------------------------
echo "[uninstall] Removing global user virtual environment if it exists..."
if [[ -d "$VENV_DIR" ]]; then
rm -rf "$VENV_DIR"
echo "[uninstall] Removed: $VENV_DIR"
else
echo "[uninstall] No venv found at: $VENV_DIR"
fi
# ------------------------------------------------------------
# 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'
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
if [[ -f "$rc" ]]; then
sed -i "/$RC_PATTERN/d" "$rc"
echo "[uninstall] Cleaned $rc"
else
echo "[uninstall] File not found: $rc (skipped)"
fi
done
echo "[uninstall] Done. Restart your shell (or run 'exec bash' or 'exec zsh') to apply changes."

36
src/pkgmgr/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Top-level pkgmgr package.
We deliberately avoid importing heavy submodules (like the CLI)
on import to keep unit tests fast and to not require optional
dependencies (like PyYAML) unless they are actually used.
Accessing ``pkgmgr.cli`` will load the CLI module lazily via
``__getattr__``. This keeps patterns like
from pkgmgr import cli
working as expected in tests and entry points.
"""
from __future__ import annotations
from importlib import import_module
from typing import Any
__all__ = ["cli"]
def __getattr__(name: str) -> Any:
"""
Lazily expose ``pkgmgr.cli`` as attribute on the top-level package.
This keeps ``import pkgmgr`` lightweight while still allowing
``from pkgmgr import cli`` in tests and entry points.
"""
if name == "cli":
return import_module("pkgmgr.cli")
raise AttributeError(f"module 'pkgmgr' has no attribute {name!r}")

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
"""
Public API for branch actions.
"""
from .open_branch import open_branch
from .close_branch import close_branch
from .drop_branch import drop_branch
__all__ = [
"open_branch",
"close_branch",
"drop_branch",
]

View File

@@ -0,0 +1,99 @@
from __future__ import annotations
from typing import Optional
from pkgmgr.core.git import run_git, GitError, get_current_branch
from .utils import _resolve_base_branch
def close_branch(
name: Optional[str],
base_branch: str = "main",
fallback_base: str = "master",
cwd: str = ".",
force: bool = False,
) -> None:
"""
Merge a feature branch into the base branch and delete it afterwards.
"""
# Determine branch name
if not name:
try:
name = get_current_branch(cwd=cwd)
except GitError as exc:
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
if not name:
raise RuntimeError("Branch name must not be empty.")
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
if name == target_base:
raise RuntimeError(
f"Refusing to close base branch {target_base!r}. "
"Please specify a feature branch."
)
# Confirmation
if not force:
answer = input(
f"Merge branch '{name}' into '{target_base}' and delete it afterwards? (y/N): "
).strip().lower()
if answer != "y":
print("Aborted closing branch.")
return
# Fetch
try:
run_git(["fetch", "origin"], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to fetch from origin before closing branch {name!r}: {exc}"
) from exc
# Checkout base
try:
run_git(["checkout", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {target_base!r}: {exc}"
) from exc
# Pull latest
try:
run_git(["pull", "origin", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {target_base!r}: {exc}"
) from exc
# Merge
try:
run_git(["merge", "--no-ff", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to merge branch {name!r} into {target_base!r}: {exc}"
) from exc
# Push result
try:
run_git(["push", "origin", target_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to push base branch {target_base!r} after merge: {exc}"
) from exc
# Delete local
try:
run_git(["branch", "-d", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to delete local branch {name!r}: {exc}"
) from exc
# Delete remote
try:
run_git(["push", "origin", "--delete", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Branch {name!r} deleted locally, but remote deletion failed: {exc}"
) from exc

View File

@@ -0,0 +1,55 @@
from __future__ import annotations
from typing import Optional
from pkgmgr.core.git import run_git, GitError, get_current_branch
from .utils import _resolve_base_branch
def drop_branch(
name: Optional[str],
base_branch: str = "main",
fallback_base: str = "master",
cwd: str = ".",
force: bool = False,
) -> None:
"""
Delete a branch locally and remotely without merging.
"""
if not name:
try:
name = get_current_branch(cwd=cwd)
except GitError as exc:
raise RuntimeError(f"Failed to detect current branch: {exc}") from exc
if not name:
raise RuntimeError("Branch name must not be empty.")
target_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
if name == target_base:
raise RuntimeError(
f"Refusing to drop base branch {target_base!r}. It cannot be deleted."
)
# Confirmation
if not force:
answer = input(
f"Delete branch '{name}' locally and on origin? This is destructive! (y/N): "
).strip().lower()
if answer != "y":
print("Aborted dropping branch.")
return
# Local delete
try:
run_git(["branch", "-d", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(f"Failed to delete local branch {name!r}: {exc}") from exc
# Remote delete
try:
run_git(["push", "origin", "--delete", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Branch {name!r} was deleted locally, but remote deletion failed: {exc}"
) from exc

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
from typing import Optional
from pkgmgr.core.git import run_git, GitError
from .utils import _resolve_base_branch
def open_branch(
name: Optional[str],
base_branch: str = "main",
fallback_base: str = "master",
cwd: str = ".",
) -> None:
"""
Create and push a new feature branch on top of a base branch.
"""
# Request name interactively if not provided
if not name:
name = input("Enter new branch name: ").strip()
if not name:
raise RuntimeError("Branch name must not be empty.")
resolved_base = _resolve_base_branch(base_branch, fallback_base, cwd=cwd)
# 1) Fetch from origin
try:
run_git(["fetch", "origin"], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to fetch from origin before creating branch {name!r}: {exc}"
) from exc
# 2) Checkout base branch
try:
run_git(["checkout", resolved_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to checkout base branch {resolved_base!r}: {exc}"
) from exc
# 3) Pull latest changes
try:
run_git(["pull", "origin", resolved_base], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to pull latest changes for base branch {resolved_base!r}: {exc}"
) from exc
# 4) Create new branch
try:
run_git(["checkout", "-b", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to create new branch {name!r} from base {resolved_base!r}: {exc}"
) from exc
# 5) Push new branch
try:
run_git(["push", "-u", "origin", name], cwd=cwd)
except GitError as exc:
raise RuntimeError(
f"Failed to push new branch {name!r} to origin: {exc}"
) from exc

View File

@@ -0,0 +1,27 @@
from __future__ import annotations
from pkgmgr.core.git import run_git, GitError
def _resolve_base_branch(
preferred: str,
fallback: str,
cwd: str,
) -> str:
"""
Resolve the base branch to use.
Try `preferred` first (default: main),
fall back to `fallback` (default: master).
Raise RuntimeError if neither exists.
"""
for candidate in (preferred, fallback):
try:
run_git(["rev-parse", "--verify", candidate], cwd=cwd)
return candidate
except GitError:
continue
raise RuntimeError(
f"Neither {preferred!r} nor {fallback!r} exist in this repository."
)

View File

@@ -13,7 +13,7 @@ from __future__ import annotations
from typing import Optional
from pkgmgr.git_utils import run_git, GitError
from pkgmgr.core.git import run_git, GitError
def generate_changelog(

View File

@@ -26,8 +26,8 @@ import os
import subprocess
from typing import Any, Dict
from pkgmgr.generate_alias import generate_alias
from pkgmgr.save_user_config import save_user_config
from pkgmgr.core.command.alias import generate_alias
from pkgmgr.core.config.save import save_user_config
def config_init(

View File

@@ -1,5 +1,5 @@
import yaml
from .load_config import load_config
from pkgmgr.core.config.load import load_config
def show_config(selected_repos, user_config_path, full_config=False):
"""Display configuration for one or more repositories, or the entire merged config."""

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
High-level entry point for repository installation.
Responsibilities:
- Ensure the repository directory exists (clone if necessary).
- Verify the repository (GPG / commit checks).
- Build a RepoContext object.
- Delegate the actual installation decision logic to InstallationPipeline.
"""
from __future__ import annotations
import os
from typing import Any, Dict, List, Optional
from pkgmgr.core.repository.identifier import get_repo_identifier
from pkgmgr.core.repository.dir import get_repo_dir
from pkgmgr.core.repository.verify import verify_repository
from pkgmgr.actions.repository.clone import clone_repos
from pkgmgr.actions.install.context import RepoContext
from pkgmgr.actions.install.installers.os_packages import (
ArchPkgbuildInstaller,
DebianControlInstaller,
RpmSpecInstaller,
)
from pkgmgr.actions.install.installers.nix_flake import (
NixFlakeInstaller,
)
from pkgmgr.actions.install.installers.python import PythonInstaller
from pkgmgr.actions.install.installers.makefile import (
MakefileInstaller,
)
from pkgmgr.actions.install.pipeline import InstallationPipeline
Repository = Dict[str, Any]
# All available installers, in the order they should be considered.
INSTALLERS = [
ArchPkgbuildInstaller(),
DebianControlInstaller(),
RpmSpecInstaller(),
NixFlakeInstaller(),
PythonInstaller(),
MakefileInstaller(),
]
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _ensure_repo_dir(
repo: Repository,
repositories_base_dir: str,
all_repos: List[Repository],
preview: bool,
no_verification: bool,
clone_mode: str,
identifier: str,
) -> Optional[str]:
"""
Compute and, if necessary, clone the repository directory.
Returns the absolute repository path or None if cloning ultimately failed.
"""
repo_dir = get_repo_dir(repositories_base_dir, repo)
if not os.path.exists(repo_dir):
print(
f"Repository directory '{repo_dir}' does not exist. "
f"Cloning it now..."
)
clone_repos(
[repo],
repositories_base_dir,
all_repos,
preview,
no_verification,
clone_mode,
)
if not os.path.exists(repo_dir):
print(
f"Cloning failed for repository {identifier}. "
f"Skipping installation."
)
return None
return repo_dir
def _verify_repo(
repo: Repository,
repo_dir: str,
no_verification: bool,
identifier: str,
) -> bool:
"""
Verify a repository using the configured verification data.
Returns True if verification is considered okay and installation may continue.
"""
verified_info = repo.get("verified")
verified_ok, errors, _commit_hash, _signing_key = verify_repository(
repo,
repo_dir,
mode="local",
no_verification=no_verification,
)
if not no_verification and verified_info and not verified_ok:
print(f"Warning: Verification failed for {identifier}:")
for err in errors:
print(f" - {err}")
choice = input("Continue anyway? [y/N]: ").strip().lower()
if choice != "y":
print(f"Skipping installation for {identifier}.")
return False
return True
def _create_context(
repo: Repository,
identifier: str,
repo_dir: str,
repositories_base_dir: str,
bin_dir: str,
all_repos: List[Repository],
no_verification: bool,
preview: bool,
quiet: bool,
clone_mode: str,
update_dependencies: bool,
) -> RepoContext:
"""
Build a RepoContext instance for the given repository.
"""
return RepoContext(
repo=repo,
identifier=identifier,
repo_dir=repo_dir,
repositories_base_dir=repositories_base_dir,
bin_dir=bin_dir,
all_repos=all_repos,
no_verification=no_verification,
preview=preview,
quiet=quiet,
clone_mode=clone_mode,
update_dependencies=update_dependencies,
)
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
def install_repos(
selected_repos: List[Repository],
repositories_base_dir: str,
bin_dir: str,
all_repos: List[Repository],
no_verification: bool,
preview: bool,
quiet: bool,
clone_mode: str,
update_dependencies: bool,
) -> None:
"""
Install one or more repositories according to the configured installers
and the CLI layer precedence rules.
"""
pipeline = InstallationPipeline(INSTALLERS)
for repo in selected_repos:
identifier = get_repo_identifier(repo, all_repos)
repo_dir = _ensure_repo_dir(
repo=repo,
repositories_base_dir=repositories_base_dir,
all_repos=all_repos,
preview=preview,
no_verification=no_verification,
clone_mode=clone_mode,
identifier=identifier,
)
if not repo_dir:
continue
if not _verify_repo(
repo=repo,
repo_dir=repo_dir,
no_verification=no_verification,
identifier=identifier,
):
continue
ctx = _create_context(
repo=repo,
identifier=identifier,
repo_dir=repo_dir,
repositories_base_dir=repositories_base_dir,
bin_dir=bin_dir,
all_repos=all_repos,
no_verification=no_verification,
preview=preview,
quiet=quiet,
clone_mode=clone_mode,
update_dependencies=update_dependencies,
)
pipeline.run(ctx)

View File

@@ -35,10 +35,10 @@ from __future__ import annotations
import glob
import os
from abc import ABC, abstractmethod
from typing import Iterable, TYPE_CHECKING
from typing import Iterable, TYPE_CHECKING, Optional
if TYPE_CHECKING:
from pkgmgr.context import RepoContext
from pkgmgr.actions.install.context import RepoContext
# ---------------------------------------------------------------------------
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
# ---------------------------------------------------------------------------
def _read_text_if_exists(path: str) -> str | None:
def _read_text_if_exists(path: str) -> Optional[str]:
"""Read a file as UTF-8 text, returning None if it does not exist or fails."""
if not os.path.exists(path):
return None
@@ -75,7 +75,7 @@ def _scan_files_for_patterns(files: Iterable[str], patterns: Iterable[str]) -> b
return False
def _first_spec_file(repo_dir: str) -> str | None:
def _first_spec_file(repo_dir: str) -> Optional[str]:
"""Return the first *.spec file in repo_dir, if any."""
matches = glob.glob(os.path.join(repo_dir, "*.spec"))
if not matches:
@@ -360,7 +360,7 @@ def detect_capabilities(
def resolve_effective_capabilities(
ctx: "RepoContext",
layers: Iterable[str] | None = None,
layers: Optional[Iterable[str]] = None,
) -> dict[str, set[str]]:
"""
Resolve *effective* capabilities for each layer using a bottom-up strategy.

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Installer package for pkgmgr.
This exposes all installer classes so users can import them directly from
pkgmgr.actions.install.installers.
"""
from pkgmgr.actions.install.installers.base import BaseInstaller # noqa: F401
from pkgmgr.actions.install.installers.nix_flake import NixFlakeInstaller # noqa: F401
from pkgmgr.actions.install.installers.python import PythonInstaller # noqa: F401
from pkgmgr.actions.install.installers.makefile import MakefileInstaller # noqa: F401
# OS-specific installers
from pkgmgr.actions.install.installers.os_packages.arch_pkgbuild import ArchPkgbuildInstaller # noqa: F401
from pkgmgr.actions.install.installers.os_packages.debian_control import DebianControlInstaller # noqa: F401
from pkgmgr.actions.install.installers.os_packages.rpm_spec import RpmSpecInstaller # noqa: F401

View File

@@ -6,10 +6,10 @@ Base interface for all installer components in the pkgmgr installation pipeline.
"""
from abc import ABC, abstractmethod
from typing import Set
from typing import Set, Optional
from pkgmgr.context import RepoContext
from pkgmgr.capabilities import CAPABILITY_MATCHERS
from pkgmgr.actions.install.context import RepoContext
from pkgmgr.actions.install.capabilities import CAPABILITY_MATCHERS
class BaseInstaller(ABC):
@@ -24,7 +24,7 @@ class BaseInstaller(ABC):
# Examples: "nix", "python", "makefile".
# This is used by capability matchers to decide which patterns to
# search for in the repository.
layer: str | None = None
layer: Optional[str] = None
def discover_capabilities(self, ctx: RepoContext) -> Set[str]:
"""

View File

@@ -0,0 +1,97 @@
from __future__ import annotations
import os
import re
from pkgmgr.actions.install.context import RepoContext
from pkgmgr.actions.install.installers.base import BaseInstaller
from pkgmgr.core.command.run import run_command
class MakefileInstaller(BaseInstaller):
"""
Generic installer that runs `make install` if a Makefile with an
install target is present.
Safety rules:
- If PKGMGR_DISABLE_MAKEFILE_INSTALLER=1 is set, this installer
is globally disabled.
- The higher-level InstallationPipeline ensures that Makefile
installation does not run if a stronger CLI layer already owns
the command (e.g. Nix or OS packages).
"""
layer = "makefile"
MAKEFILE_NAME = "Makefile"
def supports(self, ctx: RepoContext) -> bool:
"""
Return True if this repository has a Makefile and the installer
is not globally disabled.
"""
# Optional global kill switch.
if os.environ.get("PKGMGR_DISABLE_MAKEFILE_INSTALLER") == "1":
if not ctx.quiet:
print(
"[INFO] MakefileInstaller is disabled via "
"PKGMGR_DISABLE_MAKEFILE_INSTALLER."
)
return False
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
return os.path.exists(makefile_path)
def _has_install_target(self, makefile_path: str) -> bool:
"""
Heuristically check whether the Makefile defines an install target.
We look for:
- a plain 'install:' target, or
- any 'install-*:' style target.
"""
try:
with open(makefile_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except OSError:
return False
# Simple heuristics: look for "install:" or targets starting with "install-"
if re.search(r"^install\s*:", content, flags=re.MULTILINE):
return True
if re.search(r"^install-[a-zA-Z0-9_-]*\s*:", content, flags=re.MULTILINE):
return True
return False
def run(self, ctx: RepoContext) -> None:
"""
Execute `make install` in the repository directory if an install
target exists.
"""
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
if not os.path.exists(makefile_path):
if not ctx.quiet:
print(
f"[pkgmgr] Makefile '{makefile_path}' not found, "
"skipping MakefileInstaller."
)
return
if not self._has_install_target(makefile_path):
if not ctx.quiet:
print(
f"[pkgmgr] No 'install' target found in {makefile_path}."
)
return
if not ctx.quiet:
print(
f"[pkgmgr] Running 'make install' in {ctx.repo_dir} "
f"(MakefileInstaller)"
)
cmd = "make install"
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)

Some files were not shown because too many files have changed in this diff Show More