Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55a0ae4337 | ||
|
|
bcf284c5d6 | ||
|
|
db23b1a445 | ||
|
|
506f69d8a7 | ||
|
|
097e64408f | ||
|
|
a3913d9489 | ||
|
|
c92fd44dd3 | ||
|
|
2c3efa7a27 | ||
|
|
f388bc51bc | ||
|
|
4e28eba883 | ||
|
|
b8acd634f8 | ||
|
|
fb68b325d6 | ||
|
|
650a22d425 | ||
|
|
6a590d8780 | ||
|
|
5601ea442a | ||
|
|
5ff15013d7 | ||
|
|
6ccc1c1490 | ||
|
|
8ead3472dd | ||
|
|
422ac8b837 | ||
|
|
ea84c1b14e | ||
|
|
71a4e7e725 | ||
|
|
fb737ef290 | ||
|
|
2963a43754 | ||
|
|
103f49c8f6 | ||
|
|
f5d428950e | ||
|
|
b40787ffc5 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -27,3 +27,9 @@ jobs:
|
||||
|
||||
test-virgin-root:
|
||||
uses: ./.github/workflows/test-virgin-root.yml
|
||||
|
||||
codesniffer-shellcheck:
|
||||
uses: ./.github/workflows/codesniffer-shellcheck.yml
|
||||
|
||||
codesniffer-ruff:
|
||||
uses: ./.github/workflows/codesniffer-ruff.yml
|
||||
|
||||
23
.github/workflows/codesniffer-ruff.yml
vendored
Normal file
23
.github/workflows/codesniffer-ruff.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Ruff (Python code sniffer)
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
codesniffer-ruff:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install ruff
|
||||
run: pip install ruff
|
||||
|
||||
- name: Run ruff
|
||||
run: |
|
||||
ruff check src tests
|
||||
14
.github/workflows/codesniffer-shellcheck.yml
vendored
Normal file
14
.github/workflows/codesniffer-shellcheck.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: ShellCheck
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
codesniffer-shellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install ShellCheck
|
||||
run: sudo apt-get update && sudo apt-get install -y shellcheck
|
||||
- name: Run ShellCheck
|
||||
run: shellcheck -x $(find scripts -type f -name '*.sh' -print)
|
||||
8
.github/workflows/mark-stable.yml
vendored
8
.github/workflows/mark-stable.yml
vendored
@@ -29,8 +29,16 @@ jobs:
|
||||
test-virgin-root:
|
||||
uses: ./.github/workflows/test-virgin-root.yml
|
||||
|
||||
codesniffer-shellcheck:
|
||||
uses: ./.github/workflows/codesniffer-shellcheck.yml
|
||||
|
||||
codesniffer-ruff:
|
||||
uses: ./.github/workflows/codesniffer-ruff.yml
|
||||
|
||||
mark-stable:
|
||||
needs:
|
||||
- codesniffer-shellcheck
|
||||
- codesniffer-ruff
|
||||
- test-unit
|
||||
- test-integration
|
||||
- test-env-nix
|
||||
|
||||
20
.github/workflows/publish-containers.yml
vendored
20
.github/workflows/publish-containers.yml
vendored
@@ -1,12 +1,13 @@
|
||||
name: Publish container images (GHCR)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_run:
|
||||
workflows: ["Mark stable commit"]
|
||||
types: [completed]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
@@ -20,13 +21,22 @@ jobs:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Checkout workflow_run commit and refresh tags
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git checkout -f "${{ github.event.workflow_run.head_sha }}"
|
||||
git fetch --tags --force
|
||||
git tag --list 'stable' 'v*' --sort=version:refname | tail -n 20
|
||||
|
||||
- name: Compute version and stable flag
|
||||
id: info
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
SHA="$(git rev-parse HEAD)"
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
|
||||
V_TAG="$(git tag --points-at "${SHA}" --list 'v*' | sort -V | tail -n1)"
|
||||
[[ -n "$V_TAG" ]] || { echo "No version tag found"; exit 1; }
|
||||
VERSION="${V_TAG#v}"
|
||||
|
||||
STABLE_SHA="$(git rev-parse -q --verify refs/tags/stable^{commit} 2>/dev/null || true)"
|
||||
IS_STABLE=false
|
||||
|
||||
2
.github/workflows/test-e2e.yml
vendored
2
.github/workflows/test-e2e.yml
vendored
@@ -22,4 +22,4 @@ jobs:
|
||||
- name: Run E2E tests via make (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
distro="${{ matrix.distro }}" make test-e2e
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make test-e2e
|
||||
|
||||
2
.github/workflows/test-env-nix.yml
vendored
2
.github/workflows/test-env-nix.yml
vendored
@@ -23,4 +23,4 @@ jobs:
|
||||
- name: Nix flake-only test (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
distro="${{ matrix.distro }}" make test-env-nix
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-nix
|
||||
|
||||
2
.github/workflows/test-env-virtual.yml
vendored
2
.github/workflows/test-env-virtual.yml
vendored
@@ -25,4 +25,4 @@ jobs:
|
||||
- name: Run container tests (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
distro="${{ matrix.distro }}" make test-env-virtual
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-virtual
|
||||
|
||||
2
.github/workflows/test-integration.yml
vendored
2
.github/workflows/test-integration.yml
vendored
@@ -16,4 +16,4 @@ jobs:
|
||||
run: docker version
|
||||
|
||||
- name: Run integration tests via make (Arch container)
|
||||
run: make test-integration distro="arch"
|
||||
run: make test-integration PKGMGR_DISTRO="arch"
|
||||
|
||||
2
.github/workflows/test-unit.yml
vendored
2
.github/workflows/test-unit.yml
vendored
@@ -16,4 +16,4 @@ jobs:
|
||||
run: docker version
|
||||
|
||||
- name: Run unit tests via make (Arch container)
|
||||
run: make test-unit distro="arch"
|
||||
run: make test-unit PKGMGR_DISTRO="arch"
|
||||
|
||||
2
.github/workflows/test-virgin-root.yml
vendored
2
.github/workflows/test-virgin-root.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Build virgin container (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
distro="${{ matrix.distro }}" make build-missing-virgin
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
|
||||
|
||||
# 🔹 RUN test inside virgin image
|
||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
|
||||
|
||||
2
.github/workflows/test-virgin-user.yml
vendored
2
.github/workflows/test-virgin-user.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
- name: Build virgin container (${{ matrix.distro }})
|
||||
run: |
|
||||
set -euo pipefail
|
||||
distro="${{ matrix.distro }}" make build-missing-virgin
|
||||
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
|
||||
|
||||
# 🔹 RUN test inside virgin image as non-root
|
||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
|
||||
|
||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -1,6 +1,24 @@
|
||||
## [1.5.0] - 2025-12-13
|
||||
|
||||
* - Commands now show live output while running, making long operations easier to follow
|
||||
- Error messages include full command output, making failures easier to understand and debug
|
||||
- Deinstallation is more complete and predictable, removing CLI links and properly cleaning up repositories
|
||||
- Preview mode is more trustworthy, clearly showing what would happen without making changes
|
||||
- Repository configuration problems are detected earlier with clear, user-friendly explanations
|
||||
- More consistent behavior across different Linux distributions
|
||||
- More reliable execution in Docker containers and CI environments
|
||||
- Nix-based execution works more smoothly, especially when running as root or inside containers
|
||||
- Existing commands, scripts, and workflows continue to work without any breaking changes
|
||||
|
||||
|
||||
## [1.4.1] - 2025-12-12
|
||||
|
||||
* Fixed stable release container publishing
|
||||
|
||||
|
||||
## [1.4.0] - 2025-12-12
|
||||
|
||||
* **Docker Container Building**
|
||||
**Docker Container Building**
|
||||
|
||||
* New official container images are automatically published on each release.
|
||||
* Images are available per distribution and as a default Arch-based image.
|
||||
@@ -14,7 +32,7 @@
|
||||
|
||||
## [1.3.0] - 2025-12-12
|
||||
|
||||
* **Minor release – Stability & CI hardening**
|
||||
**Stability & CI hardening**
|
||||
|
||||
* Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu
|
||||
* Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths
|
||||
@@ -26,7 +44,7 @@
|
||||
|
||||
## [1.2.1] - 2025-12-12
|
||||
|
||||
* **Changed**
|
||||
**Changed**
|
||||
|
||||
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
|
||||
|
||||
@@ -43,7 +61,7 @@
|
||||
|
||||
## [1.2.0] - 2025-12-12
|
||||
|
||||
* **Release workflow overhaul**
|
||||
**Release workflow overhaul**
|
||||
|
||||
* Introduced a fully structured release workflow with clear phases and safeguards
|
||||
* Added preview-first releases with explicit confirmation before execution
|
||||
@@ -60,7 +78,8 @@
|
||||
|
||||
## [1.0.0] - 2025-12-11
|
||||
|
||||
* **1.0.0 – Official Stable Release 🎉**
|
||||
**Official Stable Release 🎉**
|
||||
|
||||
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
||||
|
||||
---
|
||||
@@ -153,7 +172,7 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
||||
|
||||
## [0.9.1] - 2025-12-10
|
||||
|
||||
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||
* Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
||||
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
|
||||
* Removed deprecated files and updated `.gitignore`.
|
||||
@@ -248,47 +267,45 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
||||
|
||||
## [0.7.1] - 2025-12-09
|
||||
|
||||
* Fix floating 'latest' tag logic: dereference annotated target (vX.Y.Z^{}), add tag message to avoid Git errors, ensure best-effort update without blocking releases, and update unit tests (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff).
|
||||
|
||||
* Fix floating 'latest' tag logic
|
||||
* dereference annotated target (vX.Y.Z^{})
|
||||
* add tag message to avoid Git errors
|
||||
* ensure best-effort update without blocking releases
|
||||
|
||||
## [0.7.0] - 2025-12-09
|
||||
|
||||
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow, ensure main/master are updated from origin before tagging, and extend unit/e2e tests including 'pkgmgr release --help' coverage (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff)
|
||||
|
||||
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow
|
||||
* ensure main/master are updated from origin before tagging
|
||||
|
||||
## [0.6.0] - 2025-12-09
|
||||
|
||||
* Expose DISTROS and BASE_IMAGE_* variables as exported Makefile environment variables so all build and test commands can consume them dynamically. By exporting these values, every Make target (e.g., build, build-no-cache, build-missing, test-container, test-unit, test-e2e) and every delegated script in scripts/build/ and scripts/test/ now receives a consistent view of the supported distributions and their base container images. This change removes duplicated definitions across scripts, ensures reproducible builds, and allows build tooling to react automatically when new distros or base images are added to the Makefile.
|
||||
|
||||
* Consistent view of the supported distributions and their base container images.
|
||||
|
||||
## [0.5.1] - 2025-12-09
|
||||
|
||||
* Refine pkgmgr release CLI close wiring and integration tests for --close flag (ChatGPT: https://chatgpt.com/share/69376b4e-8440-800f-9d06-535ec1d7a40e)
|
||||
* Refine pkgmgr release CLI close wiring and integration tests for --close flag
|
||||
|
||||
|
||||
## [0.5.0] - 2025-12-09
|
||||
|
||||
* Add pkgmgr branch close subcommand, extend CLI parser wiring, and add unit tests for branch handling and version version-selection logic (see ChatGPT conversation: https://chatgpt.com/share/693762a3-9ea8-800f-a640-bc78170953d1)
|
||||
|
||||
* Add pkgmgr branch close subcommand, extend CLI parser wiring
|
||||
|
||||
## [0.4.3] - 2025-12-09
|
||||
|
||||
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject), and add comprehensive unit/e2e tests for release and branch commands (see ChatGPT conversation: https://chatgpt.com/share/69375cfe-9e00-800f-bd65-1bd5937e1696)
|
||||
|
||||
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject)
|
||||
|
||||
## [0.4.2] - 2025-12-09
|
||||
|
||||
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
||||
* Wire pkgmgr release CLI to new helpe
|
||||
|
||||
|
||||
## [0.4.1] - 2025-12-08
|
||||
|
||||
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
||||
|
||||
* Add branch close subcommand and integrate release close/editor flow
|
||||
|
||||
## [0.4.0] - 2025-12-08
|
||||
|
||||
* Add branch closing helper and --close flag to release command, including CLI wiring and tests (see https://chatgpt.com/share/69374aec-74ec-800f-bde3-5d91dfdb9b91)
|
||||
* Add branch closing helper and --close flag to release command
|
||||
|
||||
## [0.3.0] - 2025-12-08
|
||||
|
||||
@@ -299,13 +316,10 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
||||
- 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
|
||||
|
||||
## [0.2.0] - 2025-12-08
|
||||
|
||||
* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
|
||||
|
||||
* Add preview-first release workflow and extended packaging support
|
||||
|
||||
## [0.1.0] - 2025-12-08
|
||||
|
||||
@@ -314,5 +328,4 @@ Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
|
||||
|
||||
## [0.1.0] - 2025-12-08
|
||||
|
||||
* Implement unified release helper with preview mode, multi-packaging version bumps, and new integration/unit tests (see ChatGPT conversation 2025-12-08: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
|
||||
|
||||
* Implement unified release helper with preview mode, multi-packaging version bumps
|
||||
11
Makefile
11
Makefile
@@ -7,8 +7,8 @@
|
||||
# Distro
|
||||
# Options: arch debian ubuntu fedora centos
|
||||
DISTROS ?= arch debian ubuntu fedora centos
|
||||
distro ?= arch
|
||||
export distro
|
||||
PKGMGR_DISTRO ?= arch
|
||||
export PKGMGR_DISTRO
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Base images
|
||||
@@ -30,6 +30,7 @@ export BASE_IMAGE_CENTOS
|
||||
# PYthon Unittest Pattern
|
||||
TEST_PATTERN := test_*.py
|
||||
export TEST_PATTERN
|
||||
export PYTHONPATH := src
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# System install
|
||||
@@ -45,7 +46,7 @@ install:
|
||||
# Default: keep current auto-detection behavior
|
||||
setup: setup-nix setup-venv
|
||||
|
||||
# Explicit: developer setup (Python venv + shell RC + main.py install)
|
||||
# Explicit: developer setup (Python venv + shell RC + install)
|
||||
setup-venv: setup-nix
|
||||
@bash scripts/setup/venv.sh
|
||||
|
||||
@@ -74,7 +75,7 @@ build-no-cache-all:
|
||||
@set -e; \
|
||||
for d in $(DISTROS); do \
|
||||
echo "=== build-no-cache: $$d ==="; \
|
||||
distro="$$d" $(MAKE) build-no-cache; \
|
||||
PKGMGR_DISTRO="$$d" $(MAKE) build-no-cache; \
|
||||
done
|
||||
|
||||
# ------------------------------------------------------------
|
||||
@@ -100,7 +101,7 @@ test-env-nix: build-missing
|
||||
test: test-env-virtual test-unit test-integration test-e2e
|
||||
|
||||
delete-volumes:
|
||||
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true
|
||||
@docker volume rm "pkgmgr_nix_store_${PKGMGR_DISTRO}" "pkgmgr_nix_cache_${PKGMGR_DISTRO}" || echo "No volumes to delete."
|
||||
|
||||
purge: delete-volumes build-no-cache
|
||||
|
||||
|
||||
167
README.md
167
README.md
@@ -25,52 +25,37 @@ together into repeatable development workflows.
|
||||
|
||||
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
||||
single operating system. PKGMGR instead focuses on **your repositories and
|
||||
development lifecycle**:
|
||||
development lifecycle**. It provides one configuration for all repositories,
|
||||
one unified CLI to interact with them, and a Nix-based foundation that keeps
|
||||
tooling reproducible across distributions.
|
||||
|
||||
* one configuration for all your repos,
|
||||
* one CLI to interact with them,
|
||||
* one Nix-based layer to keep tooling reproducible across distros.
|
||||
Native package managers are still used where they make sense. PKGMGR coordinates
|
||||
the surrounding development, build and release workflows in a consistent way.
|
||||
|
||||
You keep using your native package manager where it makes sense – PKGMGR
|
||||
coordinates the *development and release flow* around it.
|
||||
In addition, PKGMGR provides Docker images that can serve as a **reproducible
|
||||
system baseline**. These images bundle the complete PKGMGR toolchain and are
|
||||
designed to be reused as a stable execution environment across machines,
|
||||
pipelines and teams. This approach is specifically used within
|
||||
[**Infinito.Nexus**](https://s.infinito.nexus/code) to make complex systems
|
||||
distribution-independent while remaining fully reproducible.
|
||||
|
||||
---
|
||||
|
||||
## Features 🚀
|
||||
|
||||
### Multi-distro development & packaging
|
||||
PKGMGR enables multi-distro development and packaging by managing multiple
|
||||
repositories from a single configuration file. It drives complete release
|
||||
pipelines across Linux distributions using Nix flakes, Python build metadata,
|
||||
native OS packages such as Arch, Debian and RPM formats, and additional ecosystem
|
||||
integrations like Ansible.
|
||||
|
||||
* Manage **many repositories at once** from a single `config/config.yaml`.
|
||||
* Drive full **release pipelines** across Linux distributions using:
|
||||
All functionality is exposed through a unified `pkgmgr` command-line interface
|
||||
that works identically on every supported distribution. It combines repository
|
||||
management, Git operations, Docker and Compose orchestration, as well as
|
||||
versioning, release and changelog workflows. Many commands support a preview
|
||||
mode, allowing you to inspect the underlying actions before they are executed.
|
||||
|
||||
* Nix flakes (`flake.nix`)
|
||||
* PyPI style builds (`pyproject.toml`)
|
||||
* OS packages (PKGBUILD, Debian control/changelog, RPM spec)
|
||||
* Ansible Galaxy metadata and more.
|
||||
|
||||
### Rich CLI for daily work
|
||||
|
||||
All commands are exposed via the `pkgmgr` CLI and are available on every distro:
|
||||
|
||||
* **Repository management**
|
||||
|
||||
* `clone`, `update`, `install`, `delete`, `deinstall`, `path`, `list`, `config`
|
||||
* **Git proxies**
|
||||
|
||||
* `pull`, `push`, `status`, `diff`, `add`, `show`, `checkout`,
|
||||
`reset`, `revert`, `rebase`, `commit`, `branch`
|
||||
* **Docker & Compose orchestration**
|
||||
|
||||
* `build`, `up`, `down`, `exec`, `ps`, `start`, `stop`, `restart`
|
||||
* **Release toolchain**
|
||||
|
||||
* `version`, `release`, `changelog`, `make`
|
||||
* **Mirror & workflow helpers**
|
||||
|
||||
* `mirror` (list/diff/merge/setup), `shell`, `terminal`, `code`, `explore`
|
||||
|
||||
Many of these commands support `--preview` mode so you can inspect the
|
||||
underlying Git or Docker calls without executing them.
|
||||
---
|
||||
|
||||
### Full development workflows
|
||||
|
||||
@@ -83,10 +68,6 @@ versioning features it can drive **end-to-end workflows**:
|
||||
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 🗺️
|
||||
@@ -99,25 +80,26 @@ The following diagram gives a full overview of:
|
||||
|
||||

|
||||
|
||||
|
||||
**Diagram status:** 12 December 2025
|
||||
|
||||
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
||||
|
||||
---
|
||||
|
||||
Perfekt, dann hier die **noch kompaktere und korrekt differenzierte Version**, die **nur** zwischen
|
||||
**`make setup`** und **`make setup-venv`** unterscheidet und exakt deinem Verhalten entspricht.
|
||||
|
||||
README-ready, ohne Over-Engineering.
|
||||
|
||||
---
|
||||
|
||||
## Installation ⚙️
|
||||
|
||||
PKGMGR can be installed using `make`.
|
||||
The setup mode defines **which runtime layers are prepared**.
|
||||
|
||||
---
|
||||
|
||||
### Download
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||
cd package-manager
|
||||
```
|
||||
|
||||
### Dependency installation (optional)
|
||||
|
||||
System dependencies required **before running any *make* commands** are installed via:
|
||||
@@ -128,8 +110,13 @@ scripts/installation/dependencies.sh
|
||||
|
||||
The script detects and normalizes the OS and installs the required **system-level dependencies** accordingly.
|
||||
|
||||
### Install
|
||||
|
||||
---
|
||||
```bash
|
||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||
cd package-manager
|
||||
make install
|
||||
```
|
||||
|
||||
### Setup modes
|
||||
|
||||
@@ -138,17 +125,8 @@ The script detects and normalizes the OS and installs the required **system-leve
|
||||
| **make setup** | Python venv **and** Nix | Full development & CI |
|
||||
| **make setup-venv** | Python venv only | Local user setup |
|
||||
|
||||
---
|
||||
|
||||
### Install & setup
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||
cd package-manager
|
||||
make install
|
||||
```
|
||||
|
||||
#### Full setup (venv + Nix)
|
||||
##### Full setup (venv + Nix)
|
||||
|
||||
```bash
|
||||
make setup
|
||||
@@ -156,7 +134,7 @@ make setup
|
||||
|
||||
Use this for CI, servers, containers and full development workflows.
|
||||
|
||||
#### Venv-only setup
|
||||
##### Venv-only setup
|
||||
|
||||
```bash
|
||||
make setup-venv
|
||||
@@ -167,38 +145,77 @@ Use this if you want PKGMGR isolated without Nix integration.
|
||||
|
||||
---
|
||||
|
||||
## Run without installation (Nix)
|
||||
Alles klar 🙂
|
||||
Hier ist der **RUN-Abschnitt ohne Gedankenstriche**, klar nach **Nix, Docker und venv** getrennt:
|
||||
|
||||
Run PKGMGR directly via Nix Flakes.
|
||||
---
|
||||
|
||||
## Run PKGMGR 🧰
|
||||
|
||||
PKGMGR can be executed in different environments.
|
||||
All modes expose the same CLI and commands.
|
||||
|
||||
---
|
||||
|
||||
### Run via Nix (no installation)
|
||||
|
||||
```bash
|
||||
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
|
||||
```
|
||||
|
||||
Example:
|
||||
---
|
||||
|
||||
```bash
|
||||
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- version pkgmgr
|
||||
```
|
||||
### Run via Docker 🐳
|
||||
|
||||
Notes:
|
||||
PKGMGR can be executed **inside Docker containers** for CI, testing and isolated
|
||||
workflows.
|
||||
---
|
||||
|
||||
* full flake URL required
|
||||
* `--` separates Nix and PKGMGR arguments
|
||||
* can be used alongside any setup mode
|
||||
#### Container types
|
||||
|
||||
Two container types are available.
|
||||
|
||||
|
||||
| Image type | Contains | Typical use |
|
||||
| ---------- | ----------------------------- | ----------------------- |
|
||||
| **Virgin** | Base OS + system dependencies | Clean test environments |
|
||||
| **Stable** | PKGMGR + Nix (flakes enabled) | Ready-to-use workflows |
|
||||
|
||||
Example images:
|
||||
|
||||
* Virgin: `pkgmgr-arch-virgin`
|
||||
* Stable: `ghcr.io/kevinveenbirkenbach/pkgmgr:stable`
|
||||
|
||||
|
||||
Use **virgin images** for isolated test runs,
|
||||
use the **stable image** for fast, reproducible execution.
|
||||
|
||||
---
|
||||
|
||||
## Usage 🧰
|
||||
#### Run examples
|
||||
|
||||
After installation, the main entry point is:
|
||||
```bash
|
||||
docker run --rm -it \
|
||||
-v "$PWD":/src \
|
||||
-w /src \
|
||||
ghcr.io/kevinveenbirkenbach/pkgmgr:stable \
|
||||
pkgmgr --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Run via virtual environment (venv)
|
||||
|
||||
After activating the venv:
|
||||
|
||||
```bash
|
||||
pkgmgr --help
|
||||
```
|
||||
|
||||
This prints a list of all available subcommands.
|
||||
The help for each command is available via:
|
||||
---
|
||||
|
||||
This allows you to choose between zero install execution using Nix, fully prebuilt
|
||||
Docker environments or local isolated venv setups with identical command behavior.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -26,17 +26,13 @@
|
||||
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 = "1.4.0";
|
||||
version = "1.5.0";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
14
main.py
14
main.py
@@ -1,14 +0,0 @@
|
||||
#!/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()
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "package-manager"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
@@ -8,13 +8,13 @@ set -euo pipefail
|
||||
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
|
||||
|
||||
resolve_base_image() {
|
||||
local distro="$1"
|
||||
case "$distro" in
|
||||
local PKGMGR_DISTRO="$1"
|
||||
case "$PKGMGR_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 ;;
|
||||
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# shellcheck source=./scripts/build/base.sh
|
||||
source "${SCRIPT_DIR}/base.sh"
|
||||
|
||||
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}"
|
||||
: "${PKGMGR_DISTRO:?Environment variable 'PKGMGR_DISTRO' must be set (arch|debian|ubuntu|fedora|centos)}"
|
||||
|
||||
NO_CACHE=0
|
||||
MISSING_ONLY=0
|
||||
@@ -20,13 +22,13 @@ IS_STABLE="false" # "true" -> publish stable tags
|
||||
DEFAULT_DISTRO="arch"
|
||||
|
||||
usage() {
|
||||
local default_tag="pkgmgr-${distro}"
|
||||
local default_tag="pkgmgr-${PKGMGR_DISTRO}"
|
||||
if [[ -n "${TARGET:-}" ]]; then
|
||||
default_tag="${default_tag}-${TARGET}"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
Usage: distro=<distro> $0 [options]
|
||||
Usage: PKGMGR_DISTRO=<distro> $0 [options]
|
||||
|
||||
Build options:
|
||||
--missing Build only if the image does not already exist (local build only)
|
||||
@@ -101,13 +103,13 @@ done
|
||||
|
||||
# Derive default local tag if not provided
|
||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||
IMAGE_TAG="${REPO_PREFIX}-${distro}"
|
||||
IMAGE_TAG="${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
|
||||
fi
|
||||
fi
|
||||
|
||||
BASE_IMAGE="$(resolve_base_image "$distro")"
|
||||
BASE_IMAGE="$(resolve_base_image "$PKGMGR_DISTRO")"
|
||||
|
||||
# Local-only "missing" shortcut
|
||||
if [[ "${MISSING_ONLY}" == "1" ]]; then
|
||||
@@ -139,7 +141,7 @@ fi
|
||||
echo
|
||||
echo "------------------------------------------------------------"
|
||||
echo "[build] Building image"
|
||||
echo "distro = ${distro}"
|
||||
echo "distro = ${PKGMGR_DISTRO}"
|
||||
echo "BASE_IMAGE = ${BASE_IMAGE}"
|
||||
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
|
||||
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
|
||||
@@ -165,14 +167,14 @@ if [[ -n "${TARGET}" ]]; then
|
||||
fi
|
||||
|
||||
compute_publish_tags() {
|
||||
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${distro}"
|
||||
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||
local alias_tag_base=""
|
||||
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
distro_tag_base="${distro_tag_base}-${TARGET}"
|
||||
fi
|
||||
|
||||
if [[ "${distro}" == "${DEFAULT_DISTRO}" ]]; then
|
||||
if [[ "${PKGMGR_DISTRO}" == "${DEFAULT_DISTRO}" ]]; then
|
||||
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
|
||||
if [[ -n "${TARGET}" ]]; then
|
||||
alias_tag_base="${alias_tag_base}-${TARGET}"
|
||||
|
||||
@@ -30,11 +30,11 @@ echo "[publish] DISTROS=${DISTROS}"
|
||||
for d in ${DISTROS}; do
|
||||
echo
|
||||
echo "============================================================"
|
||||
echo "[publish] distro=${d}"
|
||||
echo "[publish] PKGMGR_DISTRO=${d}"
|
||||
echo "============================================================"
|
||||
|
||||
# virgin
|
||||
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
--publish \
|
||||
--registry "${REGISTRY}" \
|
||||
--owner "${OWNER}" \
|
||||
@@ -43,7 +43,7 @@ for d in ${DISTROS}; do
|
||||
--target virgin
|
||||
|
||||
# full (default target)
|
||||
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||
--publish \
|
||||
--registry "${REGISTRY}" \
|
||||
--owner "${OWNER}" \
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
echo "[docker] Starting package-manager container"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -22,7 +22,7 @@ It is invoked during package installation (Arch/Debian/Fedora scriptlets) and ca
|
||||
|
||||
The entry point sources small, focused modules from *scripts/nix/lib/*:
|
||||
|
||||
- *config.sh* — configuration defaults (installer URL, retry timing)
|
||||
- *bootstrap_config.sh* — configuration defaults (installer URL, retry timing)
|
||||
- *detect.sh* — container detection helpers
|
||||
- *path.sh* — PATH adjustments and `nix` binary resolution helpers
|
||||
- *symlinks.sh* — user/global symlink helpers for stable `nix` discovery
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=lib/config.sh
|
||||
# shellcheck source=lib/detect.sh
|
||||
# shellcheck source=lib/path.sh
|
||||
# shellcheck source=lib/symlinks.sh
|
||||
# shellcheck source=lib/users.sh
|
||||
# shellcheck source=lib/install.sh
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "${SCRIPT_DIR}/lib/config.sh"
|
||||
# shellcheck source=./scripts/nix/lib/bootstrap_config.sh
|
||||
source "${SCRIPT_DIR}/lib/bootstrap_config.sh"
|
||||
|
||||
# shellcheck source=./scripts/nix/lib/detect.sh
|
||||
source "${SCRIPT_DIR}/lib/detect.sh"
|
||||
|
||||
# shellcheck source=./scripts/nix/lib/path.sh
|
||||
source "${SCRIPT_DIR}/lib/path.sh"
|
||||
|
||||
# shellcheck source=./scripts/nix/lib/symlinks.sh
|
||||
source "${SCRIPT_DIR}/lib/symlinks.sh"
|
||||
|
||||
# shellcheck source=./scripts/nix/lib/users.sh
|
||||
source "${SCRIPT_DIR}/lib/users.sh"
|
||||
|
||||
# shellcheck source=./scripts/nix/lib/install.sh
|
||||
source "${SCRIPT_DIR}/lib/install.sh"
|
||||
|
||||
# shellcheck source=./scripts/nix/lib/nix_conf_file.sh
|
||||
source "${SCRIPT_DIR}/lib/nix_conf_file.sh"
|
||||
|
||||
echo "[init-nix] Starting Nix initialization..."
|
||||
|
||||
main() {
|
||||
@@ -26,6 +33,7 @@ main() {
|
||||
ensure_nix_on_path
|
||||
|
||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||
nixconf_ensure_experimental_features
|
||||
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
|
||||
else
|
||||
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
|
||||
@@ -106,6 +114,10 @@ main() {
|
||||
# -------------------------------------------------------------------------
|
||||
ensure_nix_on_path
|
||||
|
||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||
nixconf_ensure_experimental_features
|
||||
fi
|
||||
|
||||
local nix_bin_post
|
||||
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"
|
||||
|
||||
|
||||
55
scripts/nix/lib/nix_conf_file.sh
Normal file
55
scripts/nix/lib/nix_conf_file.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Prevent double-sourcing
|
||||
if [[ -n "${PKGMGR_NIX_CONF_FILE_SH:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
PKGMGR_NIX_CONF_FILE_SH=1
|
||||
|
||||
nixconf_file_path() {
|
||||
echo "/etc/nix/nix.conf"
|
||||
}
|
||||
|
||||
nixconf_ensure_experimental_features() {
|
||||
local nix_conf want
|
||||
nix_conf="$(nixconf_file_path)"
|
||||
want="experimental-features = nix-command flakes"
|
||||
|
||||
mkdir -p /etc/nix
|
||||
|
||||
if [[ ! -f "${nix_conf}" ]]; then
|
||||
echo "[nix-conf] Creating ${nix_conf} with: ${want}"
|
||||
printf "%s\n" "${want}" >"${nix_conf}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -qE '^\s*experimental-features\s*=' "${nix_conf}"; then
|
||||
if grep -qE '^\s*experimental-features\s*=.*\bnix-command\b' "${nix_conf}" \
|
||||
&& grep -qE '^\s*experimental-features\s*=.*\bflakes\b' "${nix_conf}"; then
|
||||
echo "[nix-conf] experimental-features already correct"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[nix-conf] Extending experimental-features in ${nix_conf}"
|
||||
|
||||
local current
|
||||
current="$(grep -E '^\s*experimental-features\s*=' "${nix_conf}" | head -n1 | cut -d= -f2-)"
|
||||
current="$(echo "${current}" | xargs)" # trim
|
||||
|
||||
# Build a merged feature string without duplicates (simple token set)
|
||||
local merged="nix-command flakes"
|
||||
local token
|
||||
for token in ${current}; do
|
||||
if [[ " ${merged} " != *" ${token} "* ]]; then
|
||||
merged="${merged} ${token}"
|
||||
fi
|
||||
done
|
||||
|
||||
sed -i "s|^\s*experimental-features\s*=.*|experimental-features = ${merged}|" "${nix_conf}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[nix-conf] Appending to ${nix_conf}: ${want}"
|
||||
printf "\n%s\n" "${want}" >>"${nix_conf}"
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Nix shell mode: do not touch venv, only run main.py install
|
||||
# Nix shell mode: do not touch venv, only run 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] Running install via system python3..."
|
||||
python3 -m pkgmgr install
|
||||
echo "[setup] Setup finished (Nix mode)."
|
||||
|
||||
@@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
VENV_DIR="${HOME}/.venvs/pkgmgr"
|
||||
# shellcheck disable=SC2016
|
||||
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
|
||||
|
||||
# ------------------------------------------------------------
|
||||
@@ -15,9 +16,6 @@ RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/ac
|
||||
|
||||
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"
|
||||
|
||||
@@ -90,8 +88,8 @@ for rc in "${HOME}/.bashrc" "${HOME}/.zshrc"; do
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[setup] Running main.py install via venv Python..."
|
||||
"${VENV_DIR}/bin/python" main.py install
|
||||
echo "[setup] Running install via venv Python..."
|
||||
"${VENV_DIR}/bin/python" -m pkgmgr install
|
||||
|
||||
echo
|
||||
echo "[setup] Developer setup complete."
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================================"
|
||||
echo ">>> Running E2E tests: $distro"
|
||||
echo ">>> Running E2E tests: $PKGMGR_DISTRO"
|
||||
echo "============================================================"
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
-v "pkgmgr_nix_store_${distro}:/nix" \
|
||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
||||
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||
-e REINSTALL_PKGMGR=1 \
|
||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||
--workdir /src \
|
||||
"pkgmgr-${distro}" \
|
||||
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="pkgmgr-${distro}"
|
||||
IMAGE="pkgmgr-${PKGMGR_DISTRO}"
|
||||
|
||||
echo "============================================================"
|
||||
echo ">>> Running Nix flake-only test in ${distro} container"
|
||||
echo ">>> Running Nix flake-only test in ${PKGMGR_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" \
|
||||
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||
--workdir /src \
|
||||
-e REINSTALL_PKGMGR=1 \
|
||||
"${IMAGE}" \
|
||||
@@ -27,7 +27,7 @@ docker run --rm \
|
||||
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 "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (PKGMGR_DISTRO='"${PKGMGR_DISTRO}"')"
|
||||
echo "HINT: Ensure Nix is installed during image build for this distro."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="pkgmgr-$distro"
|
||||
IMAGE="pkgmgr-$PKGMGR_DISTRO"
|
||||
|
||||
echo
|
||||
echo "------------------------------------------------------------"
|
||||
@@ -16,9 +16,9 @@ echo
|
||||
# Run the command and capture the output
|
||||
if OUTPUT=$(docker run --rm \
|
||||
-e REINSTALL_PKGMGR=1 \
|
||||
-v pkgmgr_nix_store_${distro}:/nix \
|
||||
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||
-v "$(pwd):/src" \
|
||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
||||
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||
"$IMAGE" 2>&1); then
|
||||
echo "$OUTPUT"
|
||||
echo
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================================"
|
||||
echo ">>> Running INTEGRATION tests in ${distro} container"
|
||||
echo ">>> Running INTEGRATION tests in ${PKGMGR_DISTRO} container"
|
||||
echo "============================================================"
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
-v pkgmgr_nix_store_${distro}:/nix \
|
||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
||||
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||
--workdir /src \
|
||||
-e REINSTALL_PKGMGR=1 \
|
||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||
"pkgmgr-${distro}" \
|
||||
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||
bash -lc '
|
||||
set -e;
|
||||
git config --global --add safe.directory /src || true;
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================================"
|
||||
echo ">>> Running UNIT tests in ${distro} container"
|
||||
echo ">>> Running UNIT tests in ${PKGMGR_DISTRO} container"
|
||||
echo "============================================================"
|
||||
|
||||
docker run --rm \
|
||||
-v "$(pwd):/src" \
|
||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
||||
-v pkgmgr_nix_store_${distro}:/nix \
|
||||
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||
--workdir /src \
|
||||
-e REINSTALL_PKGMGR=1 \
|
||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||
"pkgmgr-${distro}" \
|
||||
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||
bash -lc '
|
||||
set -e;
|
||||
git config --global --add safe.directory /src || true;
|
||||
|
||||
@@ -19,12 +19,20 @@ 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'
|
||||
# Matches:
|
||||
# ~/.venvs/pkgmgr/bin/activate
|
||||
# ./.venvs/pkgmgr/bin/activate
|
||||
RC_PATTERN='(\./)?\.venvs/pkgmgr/bin/activate'
|
||||
|
||||
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"
|
||||
# Remove activation lines (functional)
|
||||
sed -E -i "/$RC_PATTERN/d" "$rc"
|
||||
|
||||
# Remove leftover echo / cosmetic lines referencing pkgmgr venv
|
||||
sed -i '/\.venvs\/pkgmgr/d' "$rc"
|
||||
|
||||
echo "[uninstall] Cleaned $rc"
|
||||
else
|
||||
echo "[uninstall] File not found: $rc (skipped)"
|
||||
|
||||
5
src/pkgmgr/__main__.py
Executable file
5
src/pkgmgr/__main__.py
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
from pkgmgr.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,6 @@
|
||||
import yaml
|
||||
import os
|
||||
from pkgmgr.core.config.save import save_user_config
|
||||
|
||||
def interactive_add(config,USER_CONFIG_PATH:str):
|
||||
"""Interactively prompt the user to add a new repository entry to the user config."""
|
||||
|
||||
@@ -45,7 +45,7 @@ def config_init(
|
||||
# Announce where we will write the result
|
||||
# ------------------------------------------------------------
|
||||
print("============================================================")
|
||||
print(f"[INIT] Writing user configuration to:")
|
||||
print("[INIT] Writing user configuration to:")
|
||||
print(f" {user_config_path}")
|
||||
print("============================================================")
|
||||
|
||||
@@ -53,7 +53,7 @@ def config_init(
|
||||
defaults_config["directories"]["repositories"]
|
||||
)
|
||||
|
||||
print(f"[INIT] Scanning repository base directory:")
|
||||
print("[INIT] Scanning repository base directory:")
|
||||
print(f" {repositories_base_dir}")
|
||||
print("")
|
||||
|
||||
@@ -173,7 +173,7 @@ def config_init(
|
||||
if new_entries:
|
||||
user_config.setdefault("repositories", []).extend(new_entries)
|
||||
save_user_config(user_config, user_config_path)
|
||||
print(f"[SAVE] Wrote user configuration to:")
|
||||
print("[SAVE] Wrote user configuration to:")
|
||||
print(f" {user_config_path}")
|
||||
else:
|
||||
print("[INFO] No new repositories were added.")
|
||||
|
||||
@@ -74,7 +74,7 @@ def _ensure_repo_dir(
|
||||
if not os.path.exists(repo_dir):
|
||||
print(
|
||||
f"Repository directory '{repo_dir}' does not exist. "
|
||||
f"Cloning it now..."
|
||||
"Cloning it now..."
|
||||
)
|
||||
clone_repos(
|
||||
[repo],
|
||||
@@ -87,7 +87,7 @@ def _ensure_repo_dir(
|
||||
if not os.path.exists(repo_dir):
|
||||
print(
|
||||
f"Cloning failed for repository {identifier}. "
|
||||
f"Skipping installation."
|
||||
"Skipping installation."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ class MakefileInstaller(BaseInstaller):
|
||||
if not ctx.quiet:
|
||||
print(
|
||||
f"[pkgmgr] Running 'make install' in {ctx.repo_dir} "
|
||||
f"(MakefileInstaller)"
|
||||
"(MakefileInstaller)"
|
||||
)
|
||||
|
||||
cmd = "make install"
|
||||
|
||||
@@ -160,7 +160,7 @@ class InstallationPipeline:
|
||||
# so we skip this installer entirely.
|
||||
if not quiet:
|
||||
print(
|
||||
f"[pkgmgr] Skipping installer "
|
||||
"[pkgmgr] Skipping installer "
|
||||
f"{installer.__class__.__name__} for {identifier} – "
|
||||
f"CLI already provided by layer {state.layer.value!r}."
|
||||
)
|
||||
@@ -171,7 +171,7 @@ class InstallationPipeline:
|
||||
# need to run another installer on top of it.
|
||||
if not quiet:
|
||||
print(
|
||||
f"[pkgmgr] Skipping installer "
|
||||
"[pkgmgr] Skipping installer "
|
||||
f"{installer.__class__.__name__} for {identifier} – "
|
||||
f"layer {installer_layer.value!r} is already loaded."
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""
|
||||
High-level mirror actions.
|
||||
|
||||
@@ -10,6 +8,7 @@ Public API:
|
||||
- setup_mirrors
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from .types import Repository, MirrorMap
|
||||
from .list_cmd import list_mirrors
|
||||
from .diff_cmd import diff_mirrors
|
||||
|
||||
@@ -150,7 +150,7 @@ def ensure_origin_remote(
|
||||
current = current_origin_url(repo_dir)
|
||||
if current == url or not url:
|
||||
print(
|
||||
f"[INFO] 'origin' already points to "
|
||||
"[INFO] 'origin' already points to "
|
||||
f"{current or '<unknown>'} (no change needed)."
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
from typing import List, Mapping
|
||||
from typing import Mapping
|
||||
|
||||
from .types import MirrorMap, Repository
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ def update_spec_version(
|
||||
|
||||
if preview:
|
||||
print(
|
||||
f"[PREVIEW] Would update spec file "
|
||||
"[PREVIEW] Would update spec file "
|
||||
f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..."
|
||||
)
|
||||
return
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
from pkgmgr.actions.branch import close_branch
|
||||
from pkgmgr.core.git import get_current_branch, GitError
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import yaml
|
||||
from pkgmgr.core.command.alias import generate_alias
|
||||
from pkgmgr.core.config.save import save_user_config
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import os
|
||||
import sys
|
||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
|
||||
def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, preview=False):
|
||||
from pkgmgr.core.command.run import run_command
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
|
||||
|
||||
def deinstall_repos(
|
||||
selected_repos,
|
||||
repositories_base_dir,
|
||||
bin_dir,
|
||||
all_repos,
|
||||
preview: bool = False,
|
||||
) -> None:
|
||||
for repo in selected_repos:
|
||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||
alias_path = os.path.join(bin_dir, repo_identifier)
|
||||
|
||||
# Resolve repository directory
|
||||
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||
|
||||
# Prefer alias if available; fall back to identifier
|
||||
alias_name = str(repo.get("alias") or repo_identifier)
|
||||
alias_path = os.path.join(os.path.expanduser(bin_dir), alias_name)
|
||||
|
||||
# Remove alias link/file (interactive)
|
||||
if os.path.exists(alias_path):
|
||||
confirm = input(f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: ").strip().lower()
|
||||
confirm = input(
|
||||
f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: "
|
||||
).strip().lower()
|
||||
if confirm == "y":
|
||||
if preview:
|
||||
print(f"[Preview] Would remove link '{alias_path}'.")
|
||||
@@ -19,10 +36,13 @@ def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, p
|
||||
else:
|
||||
print(f"No link found for {repo_identifier} in {bin_dir}.")
|
||||
|
||||
# Run make deinstall if repository exists and has a Makefile
|
||||
makefile_path = os.path.join(repo_dir, "Makefile")
|
||||
if os.path.exists(makefile_path):
|
||||
print(f"Makefile found in {repo_identifier}, running 'make deinstall'...")
|
||||
try:
|
||||
run_command("make deinstall", cwd=repo_dir, preview=preview)
|
||||
except SystemExit as e:
|
||||
print(f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}")
|
||||
print(
|
||||
f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}"
|
||||
)
|
||||
|
||||
@@ -272,7 +272,7 @@ def list_repositories(
|
||||
f"{'STATUS'.ljust(status_width)} "
|
||||
f"{'CATEGORIES'.ljust(cat_width)} "
|
||||
f"{'TAGS'.ljust(tag_width)} "
|
||||
f"DIR"
|
||||
"DIR"
|
||||
f"{RESET}"
|
||||
)
|
||||
print(header)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from pkgmgr.actions.proxy import exec_proxy_command
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
from pkgmgr.actions.repository.pull import pull_with_verification
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -9,7 +8,7 @@ from pkgmgr.cli.context import CLIContext
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||
from pkgmgr.core.git import get_tags
|
||||
from pkgmgr.core.version.semver import SemVer, extract_semver_from_tags
|
||||
from pkgmgr.core.version.semver import extract_semver_from_tags
|
||||
from pkgmgr.actions.changelog import generate_changelog
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ from pkgmgr.actions.repository.status import status_repos
|
||||
from pkgmgr.actions.repository.list import list_repositories
|
||||
from pkgmgr.core.command.run import run_command
|
||||
from pkgmgr.actions.repository.create import create_repo
|
||||
from pkgmgr.core.repository.selected import get_selected_repos
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
|
||||
Repository = Dict[str, Any]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -28,6 +28,7 @@ PROXY_COMMANDS: Dict[str, List[str]] = {
|
||||
"reset",
|
||||
"revert",
|
||||
"rebase",
|
||||
"status",
|
||||
"commit",
|
||||
],
|
||||
"docker": [
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from typing import Optional
|
||||
import os
|
||||
import shutil
|
||||
from typing import Optional, List, Dict, Any
|
||||
@@ -201,8 +200,8 @@ def resolve_command_for_repo(
|
||||
print(
|
||||
f"[INFO] Repository '{repo_identifier}' appears to be a Python "
|
||||
f"package at '{python_package_root}' but no CLI entry point was "
|
||||
f"found (PATH, Nix, main.sh/main.py). Treating it as a "
|
||||
f"library-only repository with no command."
|
||||
"found (PATH, Nix, main.sh/main.py). Treating it as a "
|
||||
"library-only repository with no command."
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from typing import Optional
|
||||
# pkgmgr/run_command.py
|
||||
from __future__ import annotations
|
||||
|
||||
import selectors
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import List, Optional, Union
|
||||
|
||||
|
||||
CommandType = Union[str, List[str]]
|
||||
|
||||
|
||||
@@ -15,32 +15,97 @@ def run_command(
|
||||
allow_failure: bool = False,
|
||||
) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Run a command and optionally exit on error.
|
||||
Run a command with live output while capturing stdout/stderr.
|
||||
|
||||
- If `cmd` is a string, it is executed with `shell=True`.
|
||||
- If `cmd` is a list of strings, it is executed without a shell.
|
||||
- Output is streamed live to the terminal.
|
||||
- Output is captured in memory.
|
||||
- On failure, captured stdout/stderr are printed again so errors are never lost.
|
||||
- Command is executed exactly once.
|
||||
"""
|
||||
if isinstance(cmd, str):
|
||||
display = cmd
|
||||
else:
|
||||
display = " ".join(cmd)
|
||||
|
||||
display = cmd if isinstance(cmd, str) else " ".join(cmd)
|
||||
where = cwd or "."
|
||||
|
||||
if preview:
|
||||
print(f"[Preview] In '{where}': {display}")
|
||||
# Fake a successful result; most callers ignore the return value anyway
|
||||
return subprocess.CompletedProcess(cmd, 0) # type: ignore[arg-type]
|
||||
|
||||
print(f"Running in '{where}': {display}")
|
||||
|
||||
if isinstance(cmd, str):
|
||||
result = subprocess.run(cmd, cwd=cwd, shell=True)
|
||||
else:
|
||||
result = subprocess.run(cmd, cwd=cwd)
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
shell=isinstance(cmd, str),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
)
|
||||
|
||||
if result.returncode != 0 and not allow_failure:
|
||||
print(f"Command failed with exit code {result.returncode}. Exiting.")
|
||||
sys.exit(result.returncode)
|
||||
assert process.stdout is not None
|
||||
assert process.stderr is not None
|
||||
|
||||
return result
|
||||
sel = selectors.DefaultSelector()
|
||||
sel.register(process.stdout, selectors.EVENT_READ, data="stdout")
|
||||
sel.register(process.stderr, selectors.EVENT_READ, data="stderr")
|
||||
|
||||
stdout_lines: List[str] = []
|
||||
stderr_lines: List[str] = []
|
||||
|
||||
try:
|
||||
while sel.get_map():
|
||||
for key, _ in sel.select():
|
||||
stream = key.fileobj
|
||||
which = key.data
|
||||
|
||||
line = stream.readline()
|
||||
if line == "":
|
||||
# EOF: stop watching this stream
|
||||
try:
|
||||
sel.unregister(stream)
|
||||
except Exception:
|
||||
pass
|
||||
continue
|
||||
|
||||
if which == "stdout":
|
||||
stdout_lines.append(line)
|
||||
print(line, end="")
|
||||
else:
|
||||
stderr_lines.append(line)
|
||||
print(line, end="", file=sys.stderr)
|
||||
finally:
|
||||
# Ensure we don't leak FDs
|
||||
try:
|
||||
sel.close()
|
||||
finally:
|
||||
try:
|
||||
process.stdout.close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
process.stderr.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
returncode = process.wait()
|
||||
|
||||
if returncode != 0 and not allow_failure:
|
||||
print("\n[pkgmgr] Command failed, captured diagnostics:", file=sys.stderr)
|
||||
print(f"[pkgmgr] Failed command: {display}", file=sys.stderr)
|
||||
|
||||
if stdout_lines:
|
||||
print("----- stdout -----")
|
||||
print("".join(stdout_lines), end="")
|
||||
|
||||
if stderr_lines:
|
||||
print("----- stderr -----", file=sys.stderr)
|
||||
print("".join(stderr_lines), end="", file=sys.stderr)
|
||||
|
||||
print(f"Command failed with exit code {returncode}. Exiting.")
|
||||
sys.exit(returncode)
|
||||
|
||||
return subprocess.CompletedProcess(
|
||||
cmd,
|
||||
returncode,
|
||||
stdout="".join(stdout_lines),
|
||||
stderr="".join(stderr_lines),
|
||||
)
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
import sys
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict
|
||||
|
||||
def get_repo_dir(repositories_base_dir:str,repo:{})->str:
|
||||
try:
|
||||
return os.path.join(repositories_base_dir, repo.get("provider"), repo.get("account"), repo.get("repository"))
|
||||
except TypeError as e:
|
||||
if repositories_base_dir:
|
||||
print(f"Error: {e} \nThe repository {repo} seems not correct configured.\nPlease configure it correct.")
|
||||
for key in ["provider","account","repository"]:
|
||||
if not repo.get(key,False):
|
||||
print(f"Key '{key}' is missing.")
|
||||
else:
|
||||
print(f"Error: {e} \nThe base {base} seems not correct configured.\nPlease configure it correct.")
|
||||
sys.exit(3)
|
||||
|
||||
def get_repo_dir(repositories_base_dir: str, repo: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Build the local repository directory path from:
|
||||
repositories_base_dir/provider/account/repository
|
||||
|
||||
Exits with code 3 and prints diagnostics if the input config is invalid.
|
||||
"""
|
||||
# Base dir must be set and non-empty
|
||||
if not repositories_base_dir:
|
||||
print(
|
||||
"Error: repositories_base_dir is missing.\n"
|
||||
"The base directory for repositories seems not correctly configured.\n"
|
||||
"Please configure it correctly."
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
# Repo must be a dict-like object
|
||||
if not isinstance(repo, dict):
|
||||
print(
|
||||
f"Error: invalid repo object '{repo}'.\n"
|
||||
"The repository entry seems not correctly configured.\n"
|
||||
"Please configure it correctly."
|
||||
)
|
||||
sys.exit(3)
|
||||
|
||||
base_dir = os.path.expanduser(str(repositories_base_dir))
|
||||
|
||||
provider = repo.get("provider")
|
||||
account = repo.get("account")
|
||||
repository = repo.get("repository")
|
||||
|
||||
missing = [k for k, v in [("provider", provider), ("account", account), ("repository", repository)] if not v]
|
||||
if missing:
|
||||
print(
|
||||
"Error: repository entry is missing required keys.\n"
|
||||
f"Repository: {repo}\n"
|
||||
"Please configure it correctly."
|
||||
)
|
||||
for k in missing:
|
||||
print(f"Key '{k}' is missing.")
|
||||
sys.exit(3)
|
||||
|
||||
return os.path.join(base_dir, str(provider), str(account), str(repository))
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
|
||||
def resolve_repos(identifiers:[], all_repos:[]):
|
||||
"""
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestIntegrationBranchCommands(unittest.TestCase):
|
||||
try:
|
||||
# argv[0] is the program name; the rest are CLI arguments.
|
||||
sys.argv = ["pkgmgr"] + list(extra_args)
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ def _run_pkgmgr_help(argv_tail: list[str]) -> str:
|
||||
|
||||
try:
|
||||
with redirect_stdout(buffer), redirect_stderr(buffer):
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else None
|
||||
if code not in (0, None):
|
||||
|
||||
@@ -53,7 +53,7 @@ class TestIntegrationChangelogCommands(unittest.TestCase):
|
||||
sys.argv = ["pkgmgr", "changelog"] + list(extra_args)
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -47,7 +47,7 @@ class TestIntegrationCloneAllHttps(unittest.TestCase):
|
||||
try:
|
||||
# Execute main.py as if it was called from CLI.
|
||||
# This will run the full clone pipeline inside the container.
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
# Determine the exit code (int or string)
|
||||
exit_code = exc.code
|
||||
|
||||
@@ -34,7 +34,7 @@ def _run_pkgmgr_config(extra_args: list[str]) -> None:
|
||||
sys.argv = ["pkgmgr"] + extra_args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -139,7 +139,7 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
||||
]
|
||||
|
||||
# Execute installation via main.py
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
|
||||
# Debug: interactive shell test
|
||||
pkgmgr_help_debug()
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestIntegrationListCommands(unittest.TestCase):
|
||||
sys.argv = ["pkgmgr"] + args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -44,7 +44,7 @@ class TestIntegrationMakeCommands(unittest.TestCase):
|
||||
sys.argv = ["pkgmgr"] + extra_args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -50,7 +50,7 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
|
||||
|
||||
try:
|
||||
with redirect_stdout(buffer), redirect_stderr(buffer):
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else None
|
||||
if code not in (0, None):
|
||||
|
||||
@@ -50,7 +50,7 @@ class TestPathCommandsE2E(unittest.TestCase):
|
||||
try:
|
||||
# Capture stdout while running the CLI entry point.
|
||||
with redirect_stdout(buffer):
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
# Determine the exit code (int or string)
|
||||
exit_code = exc.code
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestIntegrationProxyCommands(unittest.TestCase):
|
||||
sys.argv = ["pkgmgr"] + args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -44,7 +44,7 @@ class TestIntegrationReleaseCommand(unittest.TestCase):
|
||||
try:
|
||||
# argv[0] is the program name; the rest are CLI arguments.
|
||||
sys.argv = ["pkgmgr"] + list(extra_args)
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
@@ -152,7 +152,7 @@ class TestIntegrationReleaseCommand(unittest.TestCase):
|
||||
# argparse will call sys.exit(), so we expect a SystemExit here.
|
||||
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
finally:
|
||||
sys.argv = original_argv
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class TestIntegrationToolsCommands(unittest.TestCase):
|
||||
sys.argv = ["pkgmgr"] + extra_args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -12,20 +12,11 @@ which we treat as success and suppress in the helper.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import runpy
|
||||
import sys
|
||||
import unittest
|
||||
from typing import List
|
||||
|
||||
|
||||
# Resolve project root (the repo where main.py lives, e.g. /src)
|
||||
PROJECT_ROOT = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), "..", "..")
|
||||
)
|
||||
MAIN_PATH = os.path.join(PROJECT_ROOT, "main.py")
|
||||
|
||||
|
||||
def _run_main(argv: List[str]) -> None:
|
||||
"""
|
||||
Helper to run main.py with the given argv.
|
||||
@@ -40,7 +31,7 @@ def _run_main(argv: List[str]) -> None:
|
||||
try:
|
||||
sys.argv = ["pkgmgr"] + argv
|
||||
try:
|
||||
runpy.run_path(MAIN_PATH, run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc: # argparse uses this for --help
|
||||
# SystemExit.code can be int, str or None; for our purposes:
|
||||
code = exc.code
|
||||
|
||||
@@ -130,7 +130,7 @@ class TestIntegrationVersionCommands(unittest.TestCase):
|
||||
sys.argv = ["pkgmgr", "version"] + extra_args
|
||||
|
||||
try:
|
||||
runpy.run_module("main", run_name="__main__")
|
||||
runpy.run_module("pkgmgr", run_name="__main__")
|
||||
except SystemExit as exc:
|
||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||
if code != 0:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.branch.utils import _resolve_base_branch
|
||||
from pkgmgr.core.git import GitError
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.branch.close_branch import close_branch
|
||||
from pkgmgr.core.git import GitError
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.branch.drop_branch import drop_branch
|
||||
from pkgmgr.core.git import GitError
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.branch.open_branch import open_branch
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# tests/unit/pkgmgr/test_capabilities.py
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch, mock_open
|
||||
|
||||
|
||||
0
tests/unit/pkgmgr/actions/repository/__init__.py
Normal file
0
tests/unit/pkgmgr/actions/repository/__init__.py
Normal file
79
tests/unit/pkgmgr/actions/repository/test_deinstall.py
Normal file
79
tests/unit/pkgmgr/actions/repository/test_deinstall.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.repository.deinstall import deinstall_repos
|
||||
|
||||
|
||||
class TestDeinstallRepos(unittest.TestCase):
|
||||
def test_preview_removes_nothing_but_runs_make_if_makefile_exists(self):
|
||||
repo = {"provider": "github.com", "account": "alice", "repository": "demo", "alias": "demo"}
|
||||
selected = [repo]
|
||||
|
||||
with patch("pkgmgr.actions.repository.deinstall.get_repo_identifier", return_value="demo"), \
|
||||
patch("pkgmgr.actions.repository.deinstall.get_repo_dir", return_value="/repos/github.com/alice/demo"), \
|
||||
patch("pkgmgr.actions.repository.deinstall.os.path.expanduser", return_value="/home/u/.local/bin"), \
|
||||
patch("pkgmgr.actions.repository.deinstall.os.path.exists") as mock_exists, \
|
||||
patch("pkgmgr.actions.repository.deinstall.os.remove") as mock_remove, \
|
||||
patch("pkgmgr.actions.repository.deinstall.run_command") as mock_run, \
|
||||
patch("builtins.input", return_value="y"):
|
||||
|
||||
# alias exists, Makefile exists
|
||||
def exists_side_effect(path):
|
||||
if path == "/home/u/.local/bin/demo":
|
||||
return True
|
||||
if path == "/repos/github.com/alice/demo/Makefile":
|
||||
return True
|
||||
return False
|
||||
|
||||
mock_exists.side_effect = exists_side_effect
|
||||
|
||||
deinstall_repos(
|
||||
selected_repos=selected,
|
||||
repositories_base_dir="/repos",
|
||||
bin_dir="~/.local/bin",
|
||||
all_repos=selected,
|
||||
preview=True,
|
||||
)
|
||||
|
||||
# Preview: do not remove
|
||||
mock_remove.assert_not_called()
|
||||
|
||||
# But still "would run" make deinstall via run_command (preview=True)
|
||||
mock_run.assert_called_once_with(
|
||||
"make deinstall",
|
||||
cwd="/repos/github.com/alice/demo",
|
||||
preview=True,
|
||||
)
|
||||
|
||||
def test_non_preview_removes_alias_when_confirmed(self):
|
||||
repo = {"provider": "github.com", "account": "alice", "repository": "demo", "alias": "demo"}
|
||||
selected = [repo]
|
||||
|
||||
with patch("pkgmgr.actions.repository.deinstall.get_repo_identifier", return_value="demo"), \
|
||||
patch("pkgmgr.actions.repository.deinstall.get_repo_dir", return_value="/repos/github.com/alice/demo"), \
|
||||
patch("pkgmgr.actions.repository.deinstall.os.path.expanduser", return_value="/home/u/.local/bin"), \
|
||||
patch("pkgmgr.actions.repository.deinstall.os.path.exists") as mock_exists, \
|
||||
patch("pkgmgr.actions.repository.deinstall.os.remove") as mock_remove, \
|
||||
patch("pkgmgr.actions.repository.deinstall.run_command") as mock_run, \
|
||||
patch("builtins.input", return_value="y"):
|
||||
|
||||
# alias exists, Makefile does NOT exist
|
||||
def exists_side_effect(path):
|
||||
if path == "/home/u/.local/bin/demo":
|
||||
return True
|
||||
if path == "/repos/github.com/alice/demo/Makefile":
|
||||
return False
|
||||
return False
|
||||
|
||||
mock_exists.side_effect = exists_side_effect
|
||||
|
||||
deinstall_repos(
|
||||
selected_repos=selected,
|
||||
repositories_base_dir="/repos",
|
||||
bin_dir="~/.local/bin",
|
||||
all_repos=selected,
|
||||
preview=False,
|
||||
)
|
||||
|
||||
mock_remove.assert_called_once_with("/home/u/.local/bin/demo")
|
||||
mock_run.assert_not_called()
|
||||
@@ -24,12 +24,11 @@ Goals:
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.cli.context import CLIContext
|
||||
from pkgmgr.cli.commands.repos import handle_repos_command
|
||||
|
||||
47
tests/unit/pkgmgr/core/command/test_run.py
Normal file
47
tests/unit/pkgmgr/core/command/test_run.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import pkgmgr.core.command.run as run_mod
|
||||
|
||||
|
||||
class TestRunCommand(unittest.TestCase):
|
||||
def test_preview_returns_success_without_running(self) -> None:
|
||||
with patch.object(run_mod.subprocess, "Popen") as popen_mock:
|
||||
result = run_mod.run_command("echo hi", cwd="/tmp", preview=True)
|
||||
self.assertEqual(result.returncode, 0)
|
||||
popen_mock.assert_not_called()
|
||||
|
||||
def test_success_streams_and_returns_completed_process(self) -> None:
|
||||
cmd = ["python3", "-c", "print('out'); import sys; print('err', file=sys.stderr)"]
|
||||
|
||||
with patch.object(run_mod.sys, "exit") as exit_mock:
|
||||
result = run_mod.run_command(cmd, allow_failure=False)
|
||||
|
||||
self.assertEqual(result.returncode, 0)
|
||||
self.assertIn("out", result.stdout)
|
||||
self.assertIn("err", result.stderr)
|
||||
exit_mock.assert_not_called()
|
||||
|
||||
def test_failure_exits_when_not_allowed(self) -> None:
|
||||
cmd = ["python3", "-c", "import sys; print('oops', file=sys.stderr); sys.exit(2)"]
|
||||
|
||||
with patch.object(run_mod.sys, "exit", side_effect=SystemExit(2)) as exit_mock:
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
run_mod.run_command(cmd, allow_failure=False)
|
||||
|
||||
self.assertEqual(ctx.exception.code, 2)
|
||||
exit_mock.assert_called_once_with(2)
|
||||
|
||||
def test_failure_does_not_exit_when_allowed(self) -> None:
|
||||
cmd = ["python3", "-c", "import sys; print('oops', file=sys.stderr); sys.exit(3)"]
|
||||
|
||||
with patch.object(run_mod.sys, "exit") as exit_mock:
|
||||
result = run_mod.run_command(cmd, allow_failure=True)
|
||||
|
||||
self.assertEqual(result.returncode, 3)
|
||||
self.assertIn("oops", result.stderr)
|
||||
exit_mock.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
0
tests/unit/pkgmgr/core/repository/__init__.py
Normal file
0
tests/unit/pkgmgr/core/repository/__init__.py
Normal file
26
tests/unit/pkgmgr/core/repository/test_dir.py
Normal file
26
tests/unit/pkgmgr/core/repository/test_dir.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.core.repository.dir import get_repo_dir
|
||||
|
||||
|
||||
class TestGetRepoDir(unittest.TestCase):
|
||||
def test_builds_path_with_expanded_base_dir(self):
|
||||
repo = {"provider": "github.com", "account": "alice", "repository": "demo"}
|
||||
with patch("pkgmgr.core.repository.dir.os.path.expanduser", return_value="/home/u/repos"):
|
||||
result = get_repo_dir("~/repos", repo)
|
||||
|
||||
self.assertEqual(result, "/home/u/repos/github.com/alice/demo")
|
||||
|
||||
def test_exits_with_code_3_if_base_dir_is_none(self):
|
||||
repo = {"provider": "github.com", "account": "alice", "repository": "demo"}
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
get_repo_dir(None, repo) # type: ignore[arg-type]
|
||||
|
||||
self.assertEqual(ctx.exception.code, 3)
|
||||
|
||||
def test_exits_with_code_3_if_repo_is_invalid_type(self):
|
||||
with self.assertRaises(SystemExit) as ctx:
|
||||
get_repo_dir("/repos", None) # type: ignore[arg-type]
|
||||
|
||||
self.assertEqual(ctx.exception.code, 3)
|
||||
@@ -1,166 +0,0 @@
|
||||
# tests/unit/pkgmgr/test_resolve_command.py
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import pkgmgr.core.command.resolve as resolve_command_module
|
||||
|
||||
|
||||
class TestResolveCommandForRepo(unittest.TestCase):
|
||||
def test_explicit_command_wins(self):
|
||||
repo = {"command": "/custom/cmd"}
|
||||
result = resolve_command_module.resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier="tool",
|
||||
repo_dir="/repos/tool",
|
||||
)
|
||||
self.assertEqual(result, "/custom/cmd")
|
||||
|
||||
@patch("pkgmgr.core.command.resolve.shutil.which", return_value="/usr/bin/tool")
|
||||
def test_system_binary_returns_none_and_no_error(self, mock_which):
|
||||
repo = {}
|
||||
result = resolve_command_module.resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier="tool",
|
||||
repo_dir="/repos/tool",
|
||||
)
|
||||
# System binary → no link
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("pkgmgr.core.command.resolve.os.access")
|
||||
@patch("pkgmgr.core.command.resolve.os.path.exists")
|
||||
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
|
||||
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
|
||||
def test_nix_profile_binary(
|
||||
self,
|
||||
mock_expanduser,
|
||||
mock_which,
|
||||
mock_exists,
|
||||
mock_access,
|
||||
):
|
||||
"""
|
||||
No system/PATH binary, but a Nix profile binary exists:
|
||||
→ must return the Nix binary path.
|
||||
"""
|
||||
repo = {}
|
||||
fake_home = "/fakehome"
|
||||
nix_path = f"{fake_home}/.nix-profile/bin/tool"
|
||||
|
||||
def fake_exists(path):
|
||||
# Only the Nix binary exists
|
||||
return path == nix_path
|
||||
|
||||
def fake_access(path, mode):
|
||||
# Only the Nix binary is executable
|
||||
return path == nix_path
|
||||
|
||||
mock_exists.side_effect = fake_exists
|
||||
mock_access.side_effect = fake_access
|
||||
|
||||
result = resolve_command_module.resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier="tool",
|
||||
repo_dir="/repos/tool",
|
||||
)
|
||||
self.assertEqual(result, nix_path)
|
||||
|
||||
@patch("pkgmgr.core.command.resolve.os.access")
|
||||
@patch("pkgmgr.core.command.resolve.os.path.exists")
|
||||
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/home/user")
|
||||
@patch("pkgmgr.core.command.resolve.shutil.which", return_value="/home/user/.local/bin/tool")
|
||||
def test_non_system_binary_on_path(
|
||||
self,
|
||||
mock_which,
|
||||
mock_expanduser,
|
||||
mock_exists,
|
||||
mock_access,
|
||||
):
|
||||
"""
|
||||
No system (/usr) binary and no Nix binary, but a non-system
|
||||
PATH binary exists (e.g. venv or ~/.local/bin):
|
||||
→ must return that PATH binary.
|
||||
"""
|
||||
repo = {}
|
||||
non_system_path = "/home/user/.local/bin/tool"
|
||||
nix_candidate = "/home/user/.nix-profile/bin/tool"
|
||||
|
||||
def fake_exists(path):
|
||||
# Only the non-system PATH binary "exists".
|
||||
return path == non_system_path
|
||||
|
||||
def fake_access(path, mode):
|
||||
# Only the non-system PATH binary is executable.
|
||||
return path == non_system_path
|
||||
|
||||
mock_exists.side_effect = fake_exists
|
||||
mock_access.side_effect = fake_access
|
||||
|
||||
result = resolve_command_module.resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier="tool",
|
||||
repo_dir="/repos/tool",
|
||||
)
|
||||
self.assertEqual(result, non_system_path)
|
||||
|
||||
@patch("pkgmgr.core.command.resolve.os.access")
|
||||
@patch("pkgmgr.core.command.resolve.os.path.exists")
|
||||
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
|
||||
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
|
||||
def test_fallback_to_main_py(
|
||||
self,
|
||||
mock_expanduser,
|
||||
mock_which,
|
||||
mock_exists,
|
||||
mock_access,
|
||||
):
|
||||
"""
|
||||
No system/non-system PATH binary, no Nix binary, but main.py exists:
|
||||
→ must fall back to main.py in the repo.
|
||||
"""
|
||||
repo = {}
|
||||
main_py = "/repos/tool/main.py"
|
||||
|
||||
def fake_exists(path):
|
||||
return path == main_py
|
||||
|
||||
def fake_access(path, mode):
|
||||
return path == main_py
|
||||
|
||||
mock_exists.side_effect = fake_exists
|
||||
mock_access.side_effect = fake_access
|
||||
|
||||
result = resolve_command_module.resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier="tool",
|
||||
repo_dir="/repos/tool",
|
||||
)
|
||||
self.assertEqual(result, main_py)
|
||||
|
||||
@patch("pkgmgr.core.command.resolve.os.access", return_value=False)
|
||||
@patch("pkgmgr.core.command.resolve.os.path.exists", return_value=False)
|
||||
@patch("pkgmgr.core.command.resolve.shutil.which", return_value=None)
|
||||
@patch("pkgmgr.core.command.resolve.os.path.expanduser", return_value="/fakehome")
|
||||
def test_no_command_results_in_system_exit(
|
||||
self,
|
||||
mock_expanduser,
|
||||
mock_which,
|
||||
mock_exists,
|
||||
mock_access,
|
||||
):
|
||||
"""
|
||||
Nothing available at any layer:
|
||||
→ must raise SystemExit with a descriptive error message.
|
||||
"""
|
||||
repo = {}
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
resolve_command_module.resolve_command_for_repo(
|
||||
repo=repo,
|
||||
repo_identifier="tool",
|
||||
repo_dir="/repos/tool",
|
||||
)
|
||||
msg = str(cm.exception)
|
||||
self.assertIn("No executable command could be resolved for repository 'tool'", msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
77
tests/unit/pkgmgr/core/repository/test_resolve_repos.py
Normal file
77
tests/unit/pkgmgr/core/repository/test_resolve_repos.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.core.repository.resolve import resolve_repos
|
||||
|
||||
|
||||
class TestResolveRepos(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
# Two repos share the same repository name "common" to test uniqueness logic
|
||||
self.repos = [
|
||||
{
|
||||
"provider": "github.com",
|
||||
"account": "alice",
|
||||
"repository": "demo",
|
||||
"alias": "d",
|
||||
},
|
||||
{
|
||||
"provider": "github.com",
|
||||
"account": "bob",
|
||||
"repository": "common",
|
||||
"alias": "c1",
|
||||
},
|
||||
{
|
||||
"provider": "gitlab.com",
|
||||
"account": "carol",
|
||||
"repository": "common",
|
||||
"alias": "c2",
|
||||
},
|
||||
]
|
||||
|
||||
def test_matches_full_identifier(self):
|
||||
result = resolve_repos(["github.com/alice/demo"], self.repos)
|
||||
self.assertEqual(result, [self.repos[0]])
|
||||
|
||||
def test_matches_alias(self):
|
||||
result = resolve_repos(["d"], self.repos)
|
||||
self.assertEqual(result, [self.repos[0]])
|
||||
|
||||
def test_matches_unique_repository_name_only_if_unique(self):
|
||||
# "demo" is unique -> match
|
||||
result = resolve_repos(["demo"], self.repos)
|
||||
self.assertEqual(result, [self.repos[0]])
|
||||
|
||||
# "common" is NOT unique -> should not match anything
|
||||
result2 = resolve_repos(["common"], self.repos)
|
||||
self.assertEqual(result2, [])
|
||||
|
||||
def test_multiple_identifiers_accumulate_matches_in_order(self):
|
||||
result = resolve_repos(["d", "github.com/bob/common"], self.repos)
|
||||
self.assertEqual(result, [self.repos[0], self.repos[1]])
|
||||
|
||||
def test_unknown_identifier_prints_message(self):
|
||||
with patch("builtins.print") as mock_print:
|
||||
result = resolve_repos(["does-not-exist"], self.repos)
|
||||
|
||||
self.assertEqual(result, [])
|
||||
mock_print.assert_called_with(
|
||||
"Identifier 'does-not-exist' did not match any repository in config."
|
||||
)
|
||||
|
||||
def test_duplicate_identifiers_return_duplicates(self):
|
||||
# Current behavior: duplicates are not de-duplicated
|
||||
result = resolve_repos(["d", "d"], self.repos)
|
||||
self.assertEqual(result, [self.repos[0], self.repos[0]])
|
||||
|
||||
def test_empty_identifiers_returns_empty_list(self):
|
||||
result = resolve_repos([], self.repos)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_empty_repo_list_returns_empty_list_and_prints(self):
|
||||
with patch("builtins.print") as mock_print:
|
||||
result = resolve_repos(["github.com/alice/demo"], [])
|
||||
|
||||
self.assertEqual(result, [])
|
||||
mock_print.assert_called_with(
|
||||
"Identifier 'github.com/alice/demo' did not match any repository in config."
|
||||
)
|
||||
@@ -79,7 +79,6 @@ class TestCreateInk(unittest.TestCase):
|
||||
patch("pkgmgr.core.command.ink.os.chmod") as mock_chmod, \
|
||||
patch("pkgmgr.core.command.ink.os.path.exists", return_value=False), \
|
||||
patch("pkgmgr.core.command.ink.os.path.islink", return_value=False), \
|
||||
patch("pkgmgr.core.command.ink.os.remove") as mock_remove, \
|
||||
patch("pkgmgr.core.command.ink.os.path.realpath", side_effect=lambda p: p):
|
||||
create_ink_module.create_ink(
|
||||
repo=repo,
|
||||
|
||||
Reference in New Issue
Block a user