Compare commits
33 Commits
v1.3.1
...
7f262c6557
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f262c6557 | ||
|
|
0bc7a3ecc0 | ||
|
|
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 | ||
|
|
0482a7f88d | ||
|
|
8c127cc45a | ||
|
|
2761e829cb | ||
|
|
d0c01b6955 | ||
|
|
b2421c9b84 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -27,3 +27,9 @@ jobs:
|
|||||||
|
|
||||||
test-virgin-root:
|
test-virgin-root:
|
||||||
uses: ./.github/workflows/test-virgin-root.yml
|
uses: ./.github/workflows/test-virgin-root.yml
|
||||||
|
|
||||||
|
codesniffer-shellcheck:
|
||||||
|
uses: ./.github/workflows/codesniffer-shellcheck.yml
|
||||||
|
|
||||||
|
codesniffer-ruff:
|
||||||
|
uses: ./.github/workflows/codesniffer-ruff.yml
|
||||||
|
|||||||
23
.github/workflows/codesniffer-ruff.yml
vendored
Normal file
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:
|
test-virgin-root:
|
||||||
uses: ./.github/workflows/test-virgin-root.yml
|
uses: ./.github/workflows/test-virgin-root.yml
|
||||||
|
|
||||||
|
codesniffer-shellcheck:
|
||||||
|
uses: ./.github/workflows/codesniffer-shellcheck.yml
|
||||||
|
|
||||||
|
codesniffer-ruff:
|
||||||
|
uses: ./.github/workflows/codesniffer-ruff.yml
|
||||||
|
|
||||||
mark-stable:
|
mark-stable:
|
||||||
needs:
|
needs:
|
||||||
|
- codesniffer-shellcheck
|
||||||
|
- codesniffer-ruff
|
||||||
- test-unit
|
- test-unit
|
||||||
- test-integration
|
- test-integration
|
||||||
- test-env-nix
|
- test-env-nix
|
||||||
|
|||||||
66
.github/workflows/publish-containers.yml
vendored
Normal file
66
.github/workflows/publish-containers.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: Publish container images (GHCR)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Mark stable commit"]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository (with tags)
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
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)"
|
||||||
|
|
||||||
|
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
|
||||||
|
[[ -n "${STABLE_SHA}" && "${STABLE_SHA}" == "${SHA}" ]] && IS_STABLE=true
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "is_stable=${IS_STABLE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
use: true
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish all images
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
OWNER="${{ github.repository_owner }}" \
|
||||||
|
VERSION="${{ steps.info.outputs.version }}" \
|
||||||
|
IS_STABLE="${{ steps.info.outputs.is_stable }}" \
|
||||||
|
bash scripts/build/publish.sh
|
||||||
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 }})
|
- name: Run E2E tests via make (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make test-e2e
|
PKGMGR_DISTRO="${{ matrix.distro }}" make test-e2e
|
||||||
|
|||||||
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 }})
|
- name: Nix flake-only test (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make test-env-nix
|
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-nix
|
||||||
|
|||||||
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 }})
|
- name: Run container tests (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make test-env-virtual
|
PKGMGR_DISTRO="${{ matrix.distro }}" make test-env-virtual
|
||||||
|
|||||||
2
.github/workflows/test-integration.yml
vendored
2
.github/workflows/test-integration.yml
vendored
@@ -16,4 +16,4 @@ jobs:
|
|||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run integration tests via make (Arch container)
|
- name: Run integration tests via make (Arch container)
|
||||||
run: make test-integration distro="arch"
|
run: make test-integration PKGMGR_DISTRO="arch"
|
||||||
|
|||||||
2
.github/workflows/test-unit.yml
vendored
2
.github/workflows/test-unit.yml
vendored
@@ -16,4 +16,4 @@ jobs:
|
|||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run unit tests via make (Arch container)
|
- name: Run unit tests via make (Arch container)
|
||||||
run: make test-unit distro="arch"
|
run: make test-unit PKGMGR_DISTRO="arch"
|
||||||
|
|||||||
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 }})
|
- name: Build virgin container (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make build-missing-virgin
|
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
|
||||||
|
|
||||||
# 🔹 RUN test inside virgin image
|
# 🔹 RUN test inside virgin image
|
||||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
|
- name: Virgin ${{ matrix.distro }} pkgmgr test (root)
|
||||||
|
|||||||
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 }})
|
- name: Build virgin container (${{ matrix.distro }})
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
distro="${{ matrix.distro }}" make build-missing-virgin
|
PKGMGR_DISTRO="${{ matrix.distro }}" make build-missing-virgin
|
||||||
|
|
||||||
# 🔹 RUN test inside virgin image as non-root
|
# 🔹 RUN test inside virgin image as non-root
|
||||||
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
|
- name: Virgin ${{ matrix.distro }} pkgmgr test (user)
|
||||||
|
|||||||
74
CHANGELOG.md
74
CHANGELOG.md
@@ -1,3 +1,30 @@
|
|||||||
|
## [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**
|
||||||
|
|
||||||
|
* New official container images are automatically published on each release.
|
||||||
|
* Images are available per distribution and as a default Arch-based image.
|
||||||
|
* Stable releases now provide an additional `stable` container tag.
|
||||||
|
|
||||||
|
|
||||||
## [1.3.1] - 2025-12-12
|
## [1.3.1] - 2025-12-12
|
||||||
|
|
||||||
* Updated documentation with better run and installation instructions
|
* Updated documentation with better run and installation instructions
|
||||||
@@ -5,7 +32,7 @@
|
|||||||
|
|
||||||
## [1.3.0] - 2025-12-12
|
## [1.3.0] - 2025-12-12
|
||||||
|
|
||||||
* **Minor release – Stability & CI hardening**
|
**Stability & CI hardening**
|
||||||
|
|
||||||
* Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu
|
* Stabilized Nix resolution and global symlink handling across Arch, CentOS, Debian, and Ubuntu
|
||||||
* Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths
|
* Ensured Nix works reliably in CI, sudo, login, and non-login shells without overriding distro-managed paths
|
||||||
@@ -17,7 +44,7 @@
|
|||||||
|
|
||||||
## [1.2.1] - 2025-12-12
|
## [1.2.1] - 2025-12-12
|
||||||
|
|
||||||
* **Changed**
|
**Changed**
|
||||||
|
|
||||||
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
|
* Split container tests into *virtualenv* and *Nix flake* environments to clearly separate Python and Nix responsibilities.
|
||||||
|
|
||||||
@@ -34,7 +61,7 @@
|
|||||||
|
|
||||||
## [1.2.0] - 2025-12-12
|
## [1.2.0] - 2025-12-12
|
||||||
|
|
||||||
* **Release workflow overhaul**
|
**Release workflow overhaul**
|
||||||
|
|
||||||
* Introduced a fully structured release workflow with clear phases and safeguards
|
* Introduced a fully structured release workflow with clear phases and safeguards
|
||||||
* Added preview-first releases with explicit confirmation before execution
|
* Added preview-first releases with explicit confirmation before execution
|
||||||
@@ -51,7 +78,8 @@
|
|||||||
|
|
||||||
## [1.0.0] - 2025-12-11
|
## [1.0.0] - 2025-12-11
|
||||||
|
|
||||||
* **1.0.0 – Official Stable Release 🎉**
|
**Official Stable Release 🎉**
|
||||||
|
|
||||||
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -144,7 +172,7 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
|||||||
|
|
||||||
## [0.9.1] - 2025-12-10
|
## [0.9.1] - 2025-12-10
|
||||||
|
|
||||||
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
* Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||||
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
|
||||||
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
|
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
|
||||||
* Removed deprecated files and updated `.gitignore`.
|
* Removed deprecated files and updated `.gitignore`.
|
||||||
@@ -239,47 +267,45 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
|||||||
|
|
||||||
## [0.7.1] - 2025-12-09
|
## [0.7.1] - 2025-12-09
|
||||||
|
|
||||||
* Fix floating 'latest' tag logic: dereference annotated target (vX.Y.Z^{}), add tag message to avoid Git errors, ensure best-effort update without blocking releases, and update unit tests (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff).
|
* Fix floating 'latest' tag logic
|
||||||
|
* dereference annotated target (vX.Y.Z^{})
|
||||||
|
* add tag message to avoid Git errors
|
||||||
|
* ensure best-effort update without blocking releases
|
||||||
|
|
||||||
## [0.7.0] - 2025-12-09
|
## [0.7.0] - 2025-12-09
|
||||||
|
|
||||||
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow, ensure main/master are updated from origin before tagging, and extend unit/e2e tests including 'pkgmgr release --help' coverage (see ChatGPT conversation: https://chatgpt.com/share/69383024-efa4-800f-a875-129b81fa40ff)
|
* Add Git helpers for branch sync and floating 'latest' tag in the release workflow
|
||||||
|
* ensure main/master are updated from origin before tagging
|
||||||
|
|
||||||
## [0.6.0] - 2025-12-09
|
## [0.6.0] - 2025-12-09
|
||||||
|
|
||||||
* Expose DISTROS and BASE_IMAGE_* variables as exported Makefile environment variables so all build and test commands can consume them dynamically. By exporting these values, every Make target (e.g., build, build-no-cache, build-missing, test-container, test-unit, test-e2e) and every delegated script in scripts/build/ and scripts/test/ now receives a consistent view of the supported distributions and their base container images. This change removes duplicated definitions across scripts, ensures reproducible builds, and allows build tooling to react automatically when new distros or base images are added to the Makefile.
|
* Consistent view of the supported distributions and their base container images.
|
||||||
|
|
||||||
|
|
||||||
## [0.5.1] - 2025-12-09
|
## [0.5.1] - 2025-12-09
|
||||||
|
|
||||||
* Refine pkgmgr release CLI close wiring and integration tests for --close flag (ChatGPT: https://chatgpt.com/share/69376b4e-8440-800f-9d06-535ec1d7a40e)
|
* Refine pkgmgr release CLI close wiring and integration tests for --close flag
|
||||||
|
|
||||||
|
|
||||||
## [0.5.0] - 2025-12-09
|
## [0.5.0] - 2025-12-09
|
||||||
|
|
||||||
* Add pkgmgr branch close subcommand, extend CLI parser wiring, and add unit tests for branch handling and version version-selection logic (see ChatGPT conversation: https://chatgpt.com/share/693762a3-9ea8-800f-a640-bc78170953d1)
|
* Add pkgmgr branch close subcommand, extend CLI parser wiring
|
||||||
|
|
||||||
|
|
||||||
## [0.4.3] - 2025-12-09
|
## [0.4.3] - 2025-12-09
|
||||||
|
|
||||||
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject), and add comprehensive unit/e2e tests for release and branch commands (see ChatGPT conversation: https://chatgpt.com/share/69375cfe-9e00-800f-bd65-1bd5937e1696)
|
* Implement current-directory repository selection for release and proxy commands, unify selection semantics across CLI layers, extend release workflow with --close, integrate branch closing logic, fix wiring for get_repo_identifier/get_repo_dir, update packaging files (PKGBUILD, spec, flake.nix, pyproject)
|
||||||
|
|
||||||
|
|
||||||
## [0.4.2] - 2025-12-09
|
## [0.4.2] - 2025-12-09
|
||||||
|
|
||||||
* Wire pkgmgr release CLI to new helper and add unit tests (see ChatGPT conversation: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
* Wire pkgmgr release CLI to new helpe
|
||||||
|
|
||||||
|
|
||||||
## [0.4.1] - 2025-12-08
|
## [0.4.1] - 2025-12-08
|
||||||
|
|
||||||
* Add branch close subcommand and integrate release close/editor flow (ChatGPT: https://chatgpt.com/share/69374f09-c760-800f-92e4-5b44a4510b62)
|
* Add branch close subcommand and integrate release close/editor flow
|
||||||
|
|
||||||
|
|
||||||
## [0.4.0] - 2025-12-08
|
## [0.4.0] - 2025-12-08
|
||||||
|
|
||||||
* Add branch closing helper and --close flag to release command, including CLI wiring and tests (see https://chatgpt.com/share/69374aec-74ec-800f-bde3-5d91dfdb9b91)
|
* Add branch closing helper and --close flag to release command
|
||||||
|
|
||||||
## [0.3.0] - 2025-12-08
|
## [0.3.0] - 2025-12-08
|
||||||
|
|
||||||
@@ -290,13 +316,10 @@ PKGMGR 1.0.0 unifies repository management, build tooling, release automation an
|
|||||||
- New config update logic + default YAML sync
|
- New config update logic + default YAML sync
|
||||||
- Improved proxy command handling
|
- Improved proxy command handling
|
||||||
- Full CLI routing refactor
|
- Full CLI routing refactor
|
||||||
- Expanded E2E tests for list, proxy, and selection logic
|
|
||||||
Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
|
|
||||||
|
|
||||||
## [0.2.0] - 2025-12-08
|
## [0.2.0] - 2025-12-08
|
||||||
|
|
||||||
* Add preview-first release workflow and extended packaging support (see ChatGPT conversation: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
|
* Add preview-first release workflow and extended packaging support
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0] - 2025-12-08
|
## [0.1.0] - 2025-12-08
|
||||||
|
|
||||||
@@ -305,5 +328,4 @@ Konversation: https://chatgpt.com/share/693745c3-b8d8-800f-aa29-c8481a2ffae1
|
|||||||
|
|
||||||
## [0.1.0] - 2025-12-08
|
## [0.1.0] - 2025-12-08
|
||||||
|
|
||||||
* Implement unified release helper with preview mode, multi-packaging version bumps, and new integration/unit tests (see ChatGPT conversation 2025-12-08: https://chatgpt.com/share/693722b4-af9c-800f-bccc-8a4036e99630)
|
* Implement unified release helper with preview mode, multi-packaging version bumps
|
||||||
|
|
||||||
13
Makefile
13
Makefile
@@ -7,8 +7,8 @@
|
|||||||
# Distro
|
# Distro
|
||||||
# Options: arch debian ubuntu fedora centos
|
# Options: arch debian ubuntu fedora centos
|
||||||
DISTROS ?= arch debian ubuntu fedora centos
|
DISTROS ?= arch debian ubuntu fedora centos
|
||||||
distro ?= arch
|
PKGMGR_DISTRO ?= arch
|
||||||
export distro
|
export PKGMGR_DISTRO
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Base images
|
# Base images
|
||||||
@@ -30,13 +30,14 @@ export BASE_IMAGE_CENTOS
|
|||||||
# PYthon Unittest Pattern
|
# PYthon Unittest Pattern
|
||||||
TEST_PATTERN := test_*.py
|
TEST_PATTERN := test_*.py
|
||||||
export TEST_PATTERN
|
export TEST_PATTERN
|
||||||
|
export PYTHONPATH := src
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# System install
|
# System install
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
install:
|
install:
|
||||||
@echo "Building and installing distro-native package-manager for this system..."
|
@echo "Building and installing distro-native package-manager for this system..."
|
||||||
@bash scripts/installation/main.sh
|
@bash scripts/installation/init.sh
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# PKGMGR setup
|
# PKGMGR setup
|
||||||
@@ -45,7 +46,7 @@ install:
|
|||||||
# Default: keep current auto-detection behavior
|
# Default: keep current auto-detection behavior
|
||||||
setup: setup-nix setup-venv
|
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
|
setup-venv: setup-nix
|
||||||
@bash scripts/setup/venv.sh
|
@bash scripts/setup/venv.sh
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ build-no-cache-all:
|
|||||||
@set -e; \
|
@set -e; \
|
||||||
for d in $(DISTROS); do \
|
for d in $(DISTROS); do \
|
||||||
echo "=== build-no-cache: $$d ==="; \
|
echo "=== build-no-cache: $$d ==="; \
|
||||||
distro="$$d" $(MAKE) build-no-cache; \
|
PKGMGR_DISTRO="$$d" $(MAKE) build-no-cache; \
|
||||||
done
|
done
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -100,7 +101,7 @@ test-env-nix: build-missing
|
|||||||
test: test-env-virtual test-unit test-integration test-e2e
|
test: test-env-virtual test-unit test-integration test-e2e
|
||||||
|
|
||||||
delete-volumes:
|
delete-volumes:
|
||||||
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true
|
@docker volume rm "pkgmgr_nix_store_${PKGMGR_DISTRO}" "pkgmgr_nix_cache_${PKGMGR_DISTRO}" || echo "No volumes to delete."
|
||||||
|
|
||||||
purge: delete-volumes build-no-cache
|
purge: delete-volumes build-no-cache
|
||||||
|
|
||||||
|
|||||||
178
README.md
178
README.md
@@ -25,52 +25,37 @@ together into repeatable development workflows.
|
|||||||
|
|
||||||
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
||||||
single operating system. PKGMGR instead focuses on **your repositories and
|
single operating system. PKGMGR instead focuses on **your repositories and
|
||||||
development lifecycle**:
|
development lifecycle**. It provides one configuration for all repositories,
|
||||||
|
one unified CLI to interact with them, and a Nix-based foundation that keeps
|
||||||
|
tooling reproducible across distributions.
|
||||||
|
|
||||||
* one configuration for all your repos,
|
Native package managers are still used where they make sense. PKGMGR coordinates
|
||||||
* one CLI to interact with them,
|
the surrounding development, build and release workflows in a consistent way.
|
||||||
* one Nix-based layer to keep tooling reproducible across distros.
|
|
||||||
|
|
||||||
You keep using your native package manager where it makes sense – PKGMGR
|
In addition, PKGMGR provides Docker images that can serve as a **reproducible
|
||||||
coordinates the *development and release flow* around it.
|
system baseline**. These images bundle the complete PKGMGR toolchain and are
|
||||||
|
designed to be reused as a stable execution environment across machines,
|
||||||
|
pipelines and teams. This approach is specifically used within
|
||||||
|
[**Infinito.Nexus**](https://s.infinito.nexus/code) to make complex systems
|
||||||
|
distribution-independent while remaining fully reproducible.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features 🚀
|
## Features 🚀
|
||||||
|
|
||||||
### Multi-distro development & packaging
|
PKGMGR enables multi-distro development and packaging by managing multiple
|
||||||
|
repositories from a single configuration file. It drives complete release
|
||||||
|
pipelines across Linux distributions using Nix flakes, Python build metadata,
|
||||||
|
native OS packages such as Arch, Debian and RPM formats, and additional ecosystem
|
||||||
|
integrations like Ansible.
|
||||||
|
|
||||||
* Manage **many repositories at once** from a single `config/config.yaml`.
|
All functionality is exposed through a unified `pkgmgr` command-line interface
|
||||||
* Drive full **release pipelines** across Linux distributions using:
|
that works identically on every supported distribution. It combines repository
|
||||||
|
management, Git operations, Docker and Compose orchestration, as well as
|
||||||
|
versioning, release and changelog workflows. Many commands support a preview
|
||||||
|
mode, allowing you to inspect the underlying actions before they are executed.
|
||||||
|
|
||||||
* Nix flakes (`flake.nix`)
|
---
|
||||||
* PyPI style builds (`pyproject.toml`)
|
|
||||||
* OS packages (PKGBUILD, Debian control/changelog, RPM spec)
|
|
||||||
* Ansible Galaxy metadata and more.
|
|
||||||
|
|
||||||
### Rich CLI for daily work
|
|
||||||
|
|
||||||
All commands are exposed via the `pkgmgr` CLI and are available on every distro:
|
|
||||||
|
|
||||||
* **Repository management**
|
|
||||||
|
|
||||||
* `clone`, `update`, `install`, `delete`, `deinstall`, `path`, `list`, `config`
|
|
||||||
* **Git proxies**
|
|
||||||
|
|
||||||
* `pull`, `push`, `status`, `diff`, `add`, `show`, `checkout`,
|
|
||||||
`reset`, `revert`, `rebase`, `commit`, `branch`
|
|
||||||
* **Docker & Compose orchestration**
|
|
||||||
|
|
||||||
* `build`, `up`, `down`, `exec`, `ps`, `start`, `stop`, `restart`
|
|
||||||
* **Release toolchain**
|
|
||||||
|
|
||||||
* `version`, `release`, `changelog`, `make`
|
|
||||||
* **Mirror & workflow helpers**
|
|
||||||
|
|
||||||
* `mirror` (list/diff/merge/setup), `shell`, `terminal`, `code`, `explore`
|
|
||||||
|
|
||||||
Many of these commands support `--preview` mode so you can inspect the
|
|
||||||
underlying Git or Docker calls without executing them.
|
|
||||||
|
|
||||||
### Full development workflows
|
### Full development workflows
|
||||||
|
|
||||||
@@ -83,10 +68,6 @@ versioning features it can drive **end-to-end workflows**:
|
|||||||
4. Build distro-specific packages.
|
4. Build distro-specific packages.
|
||||||
5. Keep all mirrors and working copies in sync.
|
5. Keep all mirrors and working copies in sync.
|
||||||
|
|
||||||
The extensive E2E tests (`tests/e2e/`) and GitHub Actions workflows (including
|
|
||||||
“virgin user” and “virgin root” Arch tests) validate these flows across
|
|
||||||
different Linux environments.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture & Setup Map 🗺️
|
## Architecture & Setup Map 🗺️
|
||||||
@@ -99,25 +80,44 @@ The following diagram gives a full overview of:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
**Diagram status:** 12 December 2025
|
**Diagram status:** 12 December 2025
|
||||||
|
|
||||||
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Perfekt, dann hier die **noch kompaktere und korrekt differenzierte Version**, die **nur** zwischen
|
|
||||||
**`make setup`** und **`make setup-venv`** unterscheidet und exakt deinem Verhalten entspricht.
|
|
||||||
|
|
||||||
README-ready, ohne Over-Engineering.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation ⚙️
|
## Installation ⚙️
|
||||||
|
|
||||||
PKGMGR can be installed using `make`.
|
PKGMGR can be installed using `make`.
|
||||||
The setup mode defines **which runtime layers are prepared**.
|
The setup mode defines **which runtime layers are prepared**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Download
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||||
|
cd package-manager
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency installation (optional)
|
||||||
|
|
||||||
|
System dependencies required **before running any *make* commands** are installed via:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
### Setup modes
|
||||||
|
|
||||||
| Command | Prepares | Use case |
|
| Command | Prepares | Use case |
|
||||||
@@ -125,17 +125,8 @@ The setup mode defines **which runtime layers are prepared**.
|
|||||||
| **make setup** | Python venv **and** Nix | Full development & CI |
|
| **make setup** | Python venv **and** Nix | Full development & CI |
|
||||||
| **make setup-venv** | Python venv only | Local user setup |
|
| **make setup-venv** | Python venv only | Local user setup |
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Install & setup
|
##### Full setup (venv + Nix)
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
|
||||||
cd package-manager
|
|
||||||
make install
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Full setup (venv + Nix)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup
|
make setup
|
||||||
@@ -143,7 +134,7 @@ make setup
|
|||||||
|
|
||||||
Use this for CI, servers, containers and full development workflows.
|
Use this for CI, servers, containers and full development workflows.
|
||||||
|
|
||||||
#### Venv-only setup
|
##### Venv-only setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup-venv
|
make setup-venv
|
||||||
@@ -154,38 +145,77 @@ Use this if you want PKGMGR isolated without Nix integration.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Run without installation (Nix)
|
Alles klar 🙂
|
||||||
|
Hier ist der **RUN-Abschnitt ohne Gedankenstriche**, klar nach **Nix, Docker und venv** getrennt:
|
||||||
|
|
||||||
Run PKGMGR directly via Nix Flakes.
|
---
|
||||||
|
|
||||||
|
## Run PKGMGR 🧰
|
||||||
|
|
||||||
|
PKGMGR can be executed in different environments.
|
||||||
|
All modes expose the same CLI and commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Run via Nix (no installation)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
|
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- --help
|
||||||
```
|
```
|
||||||
|
|
||||||
Example:
|
---
|
||||||
|
|
||||||
```bash
|
### Run via Docker 🐳
|
||||||
nix run github:kevinveenbirkenbach/package-manager#pkgmgr -- version pkgmgr
|
|
||||||
```
|
|
||||||
|
|
||||||
Notes:
|
PKGMGR can be executed **inside Docker containers** for CI, testing and isolated
|
||||||
|
workflows.
|
||||||
|
---
|
||||||
|
|
||||||
* full flake URL required
|
#### Container types
|
||||||
* `--` separates Nix and PKGMGR arguments
|
|
||||||
* can be used alongside any setup mode
|
Two container types are available.
|
||||||
|
|
||||||
|
|
||||||
|
| Image type | Contains | Typical use |
|
||||||
|
| ---------- | ----------------------------- | ----------------------- |
|
||||||
|
| **Virgin** | Base OS + system dependencies | Clean test environments |
|
||||||
|
| **Stable** | PKGMGR + Nix (flakes enabled) | Ready-to-use workflows |
|
||||||
|
|
||||||
|
Example images:
|
||||||
|
|
||||||
|
* Virgin: `pkgmgr-arch-virgin`
|
||||||
|
* Stable: `ghcr.io/kevinveenbirkenbach/pkgmgr:stable`
|
||||||
|
|
||||||
|
|
||||||
|
Use **virgin images** for isolated test runs,
|
||||||
|
use the **stable image** for fast, reproducible execution.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Usage 🧰
|
#### Run examples
|
||||||
|
|
||||||
After installation, the main entry point is:
|
```bash
|
||||||
|
docker run --rm -it \
|
||||||
|
-v "$PWD":/src \
|
||||||
|
-w /src \
|
||||||
|
ghcr.io/kevinveenbirkenbach/pkgmgr:stable \
|
||||||
|
pkgmgr --help
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Run via virtual environment (venv)
|
||||||
|
|
||||||
|
After activating the venv:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pkgmgr --help
|
pkgmgr --help
|
||||||
```
|
```
|
||||||
|
|
||||||
This prints a list of all available subcommands.
|
---
|
||||||
The help for each command is available via:
|
|
||||||
|
This allows you to choose between zero install execution using Nix, fully prebuilt
|
||||||
|
Docker environments or local isolated venv setups with identical command behavior.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -26,17 +26,13 @@
|
|||||||
packages = forAllSystems (system:
|
packages = forAllSystems (system:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
# Single source of truth for pkgmgr: Python 3.11
|
|
||||||
# - Matches pyproject.toml: requires-python = ">=3.11"
|
|
||||||
# - Uses python311Packages so that PyYAML etc. are available
|
|
||||||
python = pkgs.python311;
|
python = pkgs.python311;
|
||||||
pyPkgs = pkgs.python311Packages;
|
pyPkgs = pkgs.python311Packages;
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "1.3.1";
|
version = "1.5.0";
|
||||||
|
|
||||||
# Use the git repo as source
|
# Use the git repo as source
|
||||||
src = ./.;
|
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]
|
[project]
|
||||||
name = "package-manager"
|
name = "package-manager"
|
||||||
version = "1.3.1"
|
version = "1.5.0"
|
||||||
description = "Kevin's package-manager tool (pkgmgr)"
|
description = "Kevin's package-manager tool (pkgmgr)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
resolve_base_image() {
|
: "${BASE_IMAGE_ARCH:=archlinux:latest}"
|
||||||
local distro="$1"
|
: "${BASE_IMAGE_DEBIAN:=debian:stable-slim}"
|
||||||
|
: "${BASE_IMAGE_UBUNTU:=ubuntu:latest}"
|
||||||
|
: "${BASE_IMAGE_FEDORA:=fedora:latest}"
|
||||||
|
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
|
||||||
|
|
||||||
case "$distro" in
|
resolve_base_image() {
|
||||||
|
local PKGMGR_DISTRO="$1"
|
||||||
|
case "$PKGMGR_DISTRO" in
|
||||||
arch) echo "$BASE_IMAGE_ARCH" ;;
|
arch) echo "$BASE_IMAGE_ARCH" ;;
|
||||||
debian) echo "$BASE_IMAGE_DEBIAN" ;;
|
debian) echo "$BASE_IMAGE_DEBIAN" ;;
|
||||||
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
|
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
|
||||||
fedora) echo "$BASE_IMAGE_FEDORA" ;;
|
fedora) echo "$BASE_IMAGE_FEDORA" ;;
|
||||||
centos) echo "$BASE_IMAGE_CENTOS" ;;
|
centos) echo "$BASE_IMAGE_CENTOS" ;;
|
||||||
*)
|
*) echo "ERROR: Unknown distro '$PKGMGR_DISTRO'" >&2; exit 1 ;;
|
||||||
echo "ERROR: Unknown distro '$distro'" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
# shellcheck source=/dev/null
|
|
||||||
|
# shellcheck source=./scripts/build/base.sh
|
||||||
source "${SCRIPT_DIR}/base.sh"
|
source "${SCRIPT_DIR}/base.sh"
|
||||||
|
|
||||||
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}"
|
: "${PKGMGR_DISTRO:?Environment variable 'PKGMGR_DISTRO' must be set (arch|debian|ubuntu|fedora|centos)}"
|
||||||
|
|
||||||
NO_CACHE=0
|
NO_CACHE=0
|
||||||
MISSING_ONLY=0
|
MISSING_ONLY=0
|
||||||
TARGET=""
|
TARGET=""
|
||||||
IMAGE_TAG="" # derive later unless --tag is provided
|
IMAGE_TAG="" # local image name or base tag (without registry)
|
||||||
|
PUSH=0 # if 1 -> use buildx and push (requires docker buildx)
|
||||||
|
PUBLISH=0 # if 1 -> push with semantic tags (latest/version/stable + arch aliases)
|
||||||
|
REGISTRY="" # e.g. ghcr.io
|
||||||
|
OWNER="" # e.g. github org/user
|
||||||
|
REPO_PREFIX="pkgmgr" # image base name (pkgmgr)
|
||||||
|
VERSION="" # X.Y.Z (required for --publish)
|
||||||
|
IS_STABLE="false" # "true" -> publish stable tags
|
||||||
|
DEFAULT_DISTRO="arch"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
local default_tag="pkgmgr-${distro}"
|
local default_tag="pkgmgr-${PKGMGR_DISTRO}"
|
||||||
if [[ -n "${TARGET:-}" ]]; then
|
if [[ -n "${TARGET:-}" ]]; then
|
||||||
default_tag="${default_tag}-${TARGET}"
|
default_tag="${default_tag}-${TARGET}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: distro=<distro> $0 [--missing] [--no-cache] [--target <name>] [--tag <image>]
|
Usage: PKGMGR_DISTRO=<distro> $0 [options]
|
||||||
|
|
||||||
Options:
|
Build options:
|
||||||
--missing Build only if the image does not already exist
|
--missing Build only if the image does not already exist (local build only)
|
||||||
--no-cache Build with --no-cache
|
--no-cache Build with --no-cache
|
||||||
--target <name> Build a specific Dockerfile target (e.g. virgin|full)
|
--target <name> Build a specific Dockerfile target (e.g. virgin)
|
||||||
--tag <image> Override the output image tag (default: ${default_tag})
|
--tag <image> Override the output image tag (default: ${default_tag})
|
||||||
-h, --help Show help
|
|
||||||
|
Publish options:
|
||||||
|
--push Push the built image (uses docker buildx build --push)
|
||||||
|
--publish Publish semantic tags (latest, <version>, optional stable) + arch aliases
|
||||||
|
--registry <reg> Registry (e.g. ghcr.io)
|
||||||
|
--owner <owner> Registry namespace (e.g. \${GITHUB_REPOSITORY_OWNER})
|
||||||
|
--repo-prefix <name> Image base name (default: pkgmgr)
|
||||||
|
--version <X.Y.Z> Version for --publish
|
||||||
|
--stable <true|false> Whether to publish :stable tags (default: false)
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- --publish implies --push and requires --registry, --owner, and --version.
|
||||||
|
- Local build (no --push) uses "docker build" and creates local images like "pkgmgr-arch" / "pkgmgr-arch-virgin".
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,18 +57,39 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--missing) MISSING_ONLY=1; shift ;;
|
--missing) MISSING_ONLY=1; shift ;;
|
||||||
--target)
|
--target)
|
||||||
TARGET="${2:-}"
|
TARGET="${2:-}"
|
||||||
if [[ -z "${TARGET}" ]]; then
|
[[ -n "${TARGET}" ]] || { echo "ERROR: --target requires a value (e.g. virgin)"; exit 2; }
|
||||||
echo "ERROR: --target requires a value (e.g. virgin|full)" >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--tag)
|
--tag)
|
||||||
IMAGE_TAG="${2:-}"
|
IMAGE_TAG="${2:-}"
|
||||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
[[ -n "${IMAGE_TAG}" ]] || { echo "ERROR: --tag requires a value"; exit 2; }
|
||||||
echo "ERROR: --tag requires a value" >&2
|
shift 2
|
||||||
exit 2
|
;;
|
||||||
fi
|
--push) PUSH=1; shift ;;
|
||||||
|
--publish) PUBLISH=1; PUSH=1; shift ;;
|
||||||
|
--registry)
|
||||||
|
REGISTRY="${2:-}"
|
||||||
|
[[ -n "${REGISTRY}" ]] || { echo "ERROR: --registry requires a value"; exit 2; }
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--owner)
|
||||||
|
OWNER="${2:-}"
|
||||||
|
[[ -n "${OWNER}" ]] || { echo "ERROR: --owner requires a value"; exit 2; }
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--repo-prefix)
|
||||||
|
REPO_PREFIX="${2:-}"
|
||||||
|
[[ -n "${REPO_PREFIX}" ]] || { echo "ERROR: --repo-prefix requires a value"; exit 2; }
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--version)
|
||||||
|
VERSION="${2:-}"
|
||||||
|
[[ -n "${VERSION}" ]] || { echo "ERROR: --version requires a value"; exit 2; }
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--stable)
|
||||||
|
IS_STABLE="${2:-}"
|
||||||
|
[[ -n "${IS_STABLE}" ]] || { echo "ERROR: --stable requires a value (true|false)"; exit 2; }
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
@@ -79,32 +101,61 @@ while [[ $# -gt 0 ]]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Auto-tag: if --tag not provided, derive from distro (+ target suffix)
|
# Derive default local tag if not provided
|
||||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||||
IMAGE_TAG="pkgmgr-${distro}"
|
IMAGE_TAG="${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||||
if [[ -n "${TARGET}" ]]; then
|
if [[ -n "${TARGET}" ]]; then
|
||||||
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
|
IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
BASE_IMAGE="$(resolve_base_image "$distro")"
|
BASE_IMAGE="$(resolve_base_image "$PKGMGR_DISTRO")"
|
||||||
|
|
||||||
|
# Local-only "missing" shortcut
|
||||||
if [[ "${MISSING_ONLY}" == "1" ]]; then
|
if [[ "${MISSING_ONLY}" == "1" ]]; then
|
||||||
|
if [[ "${PUSH}" == "1" ]]; then
|
||||||
|
echo "ERROR: --missing is only supported for local builds (without --push/--publish)" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
|
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
|
||||||
echo "[build] Image already exists: ${IMAGE_TAG} (skipping due to --missing)"
|
echo "[build] Image already exists: ${IMAGE_TAG} (skipping due to --missing)"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Validate publish parameters
|
||||||
|
if [[ "${PUBLISH}" == "1" ]]; then
|
||||||
|
[[ -n "${REGISTRY}" ]] || { echo "ERROR: --publish requires --registry"; exit 2; }
|
||||||
|
[[ -n "${OWNER}" ]] || { echo "ERROR: --publish requires --owner"; exit 2; }
|
||||||
|
[[ -n "${VERSION}" ]] || { echo "ERROR: --publish requires --version"; exit 2; }
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Guard: --push without --publish requires fully-qualified --tag
|
||||||
|
if [[ "${PUSH}" == "1" && "${PUBLISH}" != "1" ]]; then
|
||||||
|
if [[ "${IMAGE_TAG}" != */* ]]; then
|
||||||
|
echo "ERROR: --push requires --tag with a fully-qualified name (e.g. ghcr.io/<owner>/<image>:tag), or use --publish" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
echo "[build] Building image: ${IMAGE_TAG}"
|
echo "[build] Building image"
|
||||||
echo "distro = ${distro}"
|
echo "distro = ${PKGMGR_DISTRO}"
|
||||||
echo "BASE_IMAGE = ${BASE_IMAGE}"
|
echo "BASE_IMAGE = ${BASE_IMAGE}"
|
||||||
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
|
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
|
||||||
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
|
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
|
||||||
|
if [[ "${PUSH}" == "1" ]]; then echo "push = enabled"; fi
|
||||||
|
if [[ "${PUBLISH}" == "1" ]]; then
|
||||||
|
echo "publish = enabled"
|
||||||
|
echo "registry = ${REGISTRY}"
|
||||||
|
echo "owner = ${OWNER}"
|
||||||
|
echo "version = ${VERSION}"
|
||||||
|
echo "stable = ${IS_STABLE}"
|
||||||
|
fi
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
|
|
||||||
|
# Common build args
|
||||||
build_args=(--build-arg "BASE_IMAGE=${BASE_IMAGE}")
|
build_args=(--build-arg "BASE_IMAGE=${BASE_IMAGE}")
|
||||||
|
|
||||||
if [[ "${NO_CACHE}" == "1" ]]; then
|
if [[ "${NO_CACHE}" == "1" ]]; then
|
||||||
@@ -115,6 +166,62 @@ if [[ -n "${TARGET}" ]]; then
|
|||||||
build_args+=(--target "${TARGET}")
|
build_args+=(--target "${TARGET}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build_args+=(-t "${IMAGE_TAG}" .)
|
compute_publish_tags() {
|
||||||
|
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${PKGMGR_DISTRO}"
|
||||||
|
local alias_tag_base=""
|
||||||
|
|
||||||
docker build "${build_args[@]}"
|
if [[ -n "${TARGET}" ]]; then
|
||||||
|
distro_tag_base="${distro_tag_base}-${TARGET}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${PKGMGR_DISTRO}" == "${DEFAULT_DISTRO}" ]]; then
|
||||||
|
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
|
||||||
|
if [[ -n "${TARGET}" ]]; then
|
||||||
|
alias_tag_base="${alias_tag_base}-${TARGET}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tags=()
|
||||||
|
tags+=("${distro_tag_base}:latest")
|
||||||
|
tags+=("${distro_tag_base}:${VERSION}")
|
||||||
|
|
||||||
|
if [[ "${IS_STABLE}" == "true" ]]; then
|
||||||
|
tags+=("${distro_tag_base}:stable")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${alias_tag_base}" ]]; then
|
||||||
|
tags+=("${alias_tag_base}:latest")
|
||||||
|
tags+=("${alias_tag_base}:${VERSION}")
|
||||||
|
if [[ "${IS_STABLE}" == "true" ]]; then
|
||||||
|
tags+=("${alias_tag_base}:stable")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${tags[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${PUSH}" == "1" ]]; then
|
||||||
|
bx_args=(docker buildx build --push)
|
||||||
|
|
||||||
|
if [[ "${PUBLISH}" == "1" ]]; then
|
||||||
|
while IFS= read -r t; do
|
||||||
|
bx_args+=(-t "$t")
|
||||||
|
done < <(compute_publish_tags)
|
||||||
|
else
|
||||||
|
bx_args+=(-t "${IMAGE_TAG}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
bx_args+=("${build_args[@]}")
|
||||||
|
bx_args+=(.)
|
||||||
|
|
||||||
|
echo "[build] Running: ${bx_args[*]}"
|
||||||
|
"${bx_args[@]}"
|
||||||
|
else
|
||||||
|
local_args=(docker build)
|
||||||
|
local_args+=("${build_args[@]}")
|
||||||
|
local_args+=(-t "${IMAGE_TAG}")
|
||||||
|
local_args+=(.)
|
||||||
|
|
||||||
|
echo "[build] Running: ${local_args[*]}"
|
||||||
|
"${local_args[@]}"
|
||||||
|
fi
|
||||||
|
|||||||
55
scripts/build/publish.sh
Executable file
55
scripts/build/publish.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Publish all distro images (full + virgin) to a registry via image.sh --publish
|
||||||
|
#
|
||||||
|
# Required env:
|
||||||
|
# OWNER (e.g. GITHUB_REPOSITORY_OWNER)
|
||||||
|
# VERSION (e.g. 1.2.3)
|
||||||
|
#
|
||||||
|
# Optional env:
|
||||||
|
# REGISTRY (default: ghcr.io)
|
||||||
|
# IS_STABLE (default: false)
|
||||||
|
# DISTROS (default: "arch debian ubuntu fedora centos")
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
REGISTRY="${REGISTRY:-ghcr.io}"
|
||||||
|
IS_STABLE="${IS_STABLE:-false}"
|
||||||
|
DISTROS="${DISTROS:-arch debian ubuntu fedora centos}"
|
||||||
|
|
||||||
|
: "${OWNER:?Environment variable OWNER must be set (e.g. github.repository_owner)}"
|
||||||
|
: "${VERSION:?Environment variable VERSION must be set (e.g. 1.2.3)}"
|
||||||
|
|
||||||
|
echo "[publish] REGISTRY=${REGISTRY}"
|
||||||
|
echo "[publish] OWNER=${OWNER}"
|
||||||
|
echo "[publish] VERSION=${VERSION}"
|
||||||
|
echo "[publish] IS_STABLE=${IS_STABLE}"
|
||||||
|
echo "[publish] DISTROS=${DISTROS}"
|
||||||
|
|
||||||
|
for d in ${DISTROS}; do
|
||||||
|
echo
|
||||||
|
echo "============================================================"
|
||||||
|
echo "[publish] PKGMGR_DISTRO=${d}"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
|
# virgin
|
||||||
|
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||||
|
--publish \
|
||||||
|
--registry "${REGISTRY}" \
|
||||||
|
--owner "${OWNER}" \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
--stable "${IS_STABLE}" \
|
||||||
|
--target virgin
|
||||||
|
|
||||||
|
# full (default target)
|
||||||
|
PKGMGR_DISTRO="${d}" bash "${SCRIPT_DIR}/image.sh" \
|
||||||
|
--publish \
|
||||||
|
--registry "${REGISTRY}" \
|
||||||
|
--owner "${OWNER}" \
|
||||||
|
--version "${VERSION}" \
|
||||||
|
--stable "${IS_STABLE}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "[publish] Done."
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
|
|
||||||
echo "[docker] Starting package-manager container"
|
echo "[docker] Starting package-manager container"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -3,22 +3,19 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
# shellcheck disable=SC1091
|
||||||
source "${SCRIPT_DIR}/lib.sh"
|
source "${SCRIPT_DIR}/os_resolver.sh"
|
||||||
|
|
||||||
OS_ID="$(detect_os_id)"
|
OS_ID="$(osr_get_os_id)"
|
||||||
|
|
||||||
echo "[run-dependencies] Detected OS: ${OS_ID}"
|
echo "[run-dependencies] Detected OS: ${OS_ID}"
|
||||||
|
|
||||||
case "${OS_ID}" in
|
if ! osr_is_supported "${OS_ID}"; then
|
||||||
arch|debian|ubuntu|fedora|centos)
|
echo "[run-dependencies] Unsupported OS: ${OS_ID}"
|
||||||
DEP_SCRIPT="${SCRIPT_DIR}/${OS_ID}/dependencies.sh"
|
exit 1
|
||||||
;;
|
fi
|
||||||
*)
|
|
||||||
echo "[run-dependencies] Unsupported OS: ${OS_ID}"
|
DEP_SCRIPT="$(osr_script_path_for "${SCRIPT_DIR}" "${OS_ID}" "dependencies")"
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ ! -f "${DEP_SCRIPT}" ]]; then
|
if [[ ! -f "${DEP_SCRIPT}" ]]; then
|
||||||
echo "[run-dependencies] Dependency script not found: ${DEP_SCRIPT}"
|
echo "[run-dependencies] Dependency script not found: ${DEP_SCRIPT}"
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/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
|
|
||||||
}
|
|
||||||
82
scripts/installation/os_resolver.sh
Executable file
82
scripts/installation/os_resolver.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# OsResolver (bash "class-style" module)
|
||||||
|
# Centralizes OS detection + normalization + supported checks + script paths.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
osr_detect_raw_id() {
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
echo "${ID:-unknown}"
|
||||||
|
else
|
||||||
|
echo "unknown"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
osr_detect_id_like() {
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
echo "${ID_LIKE:-}"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
osr_normalize_id() {
|
||||||
|
local raw="${1:-unknown}"
|
||||||
|
local like="${2:-}"
|
||||||
|
|
||||||
|
# Explicit mapping first (your bugfix: manjaro -> arch everywhere)
|
||||||
|
case "${raw}" in
|
||||||
|
manjaro) echo "arch"; return 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Keep direct IDs when they are already supported
|
||||||
|
case "${raw}" in
|
||||||
|
arch|debian|ubuntu|fedora|centos) echo "${raw}"; return 0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Fallback mapping via ID_LIKE for better portability
|
||||||
|
# Example: many Arch derivatives expose ID_LIKE="arch"
|
||||||
|
if [[ " ${like} " == *" arch "* ]]; then
|
||||||
|
echo "arch"; return 0
|
||||||
|
fi
|
||||||
|
if [[ " ${like} " == *" debian "* ]]; then
|
||||||
|
echo "debian"; return 0
|
||||||
|
fi
|
||||||
|
if [[ " ${like} " == *" fedora "* ]]; then
|
||||||
|
echo "fedora"; return 0
|
||||||
|
fi
|
||||||
|
if [[ " ${like} " == *" rhel "* || " ${like} " == *" centos "* ]]; then
|
||||||
|
echo "centos"; return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${raw}"
|
||||||
|
}
|
||||||
|
|
||||||
|
osr_get_os_id() {
|
||||||
|
local raw like
|
||||||
|
raw="$(osr_detect_raw_id)"
|
||||||
|
like="$(osr_detect_id_like)"
|
||||||
|
osr_normalize_id "${raw}" "${like}"
|
||||||
|
}
|
||||||
|
|
||||||
|
osr_is_supported() {
|
||||||
|
local id="${1:-unknown}"
|
||||||
|
case "${id}" in
|
||||||
|
arch|debian|ubuntu|fedora|centos) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
osr_script_path_for() {
|
||||||
|
local script_dir="${1:?script_dir required}"
|
||||||
|
local os_id="${2:?os_id required}"
|
||||||
|
local kind="${3:?kind required}" # "dependencies" or "package"
|
||||||
|
|
||||||
|
echo "${script_dir}/${os_id}/${kind}.sh"
|
||||||
|
}
|
||||||
@@ -3,28 +3,19 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
# shellcheck disable=SC1091
|
||||||
source "${SCRIPT_DIR}/lib.sh"
|
source "${SCRIPT_DIR}/os_resolver.sh"
|
||||||
|
|
||||||
OS_ID="$(detect_os_id)"
|
OS_ID="$(osr_get_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}"
|
echo "[package] Detected OS: ${OS_ID}"
|
||||||
|
|
||||||
case "${OS_ID}" in
|
if ! osr_is_supported "${OS_ID}"; then
|
||||||
arch|debian|ubuntu|fedora|centos)
|
echo "[package] Unsupported OS: ${OS_ID}"
|
||||||
PKG_SCRIPT="${SCRIPT_DIR}/${OS_ID}/package.sh"
|
exit 1
|
||||||
;;
|
fi
|
||||||
*)
|
|
||||||
echo "[package] Unsupported OS: ${OS_ID}"
|
PKG_SCRIPT="$(osr_script_path_for "${SCRIPT_DIR}" "${OS_ID}" "package")"
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ ! -f "${PKG_SCRIPT}" ]]; then
|
if [[ ! -f "${PKG_SCRIPT}" ]]; then
|
||||||
echo "[package] Package script not found: ${PKG_SCRIPT}"
|
echo "[package] Package script not found: ${PKG_SCRIPT}"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ It is invoked during package installation (Arch/Debian/Fedora scriptlets) and ca
|
|||||||
|
|
||||||
The entry point sources small, focused modules from *scripts/nix/lib/*:
|
The entry point sources small, focused modules from *scripts/nix/lib/*:
|
||||||
|
|
||||||
- *config.sh* — configuration defaults (installer URL, retry timing)
|
- *bootstrap_config.sh* — configuration defaults (installer URL, retry timing)
|
||||||
- *detect.sh* — container detection helpers
|
- *detect.sh* — container detection helpers
|
||||||
- *path.sh* — PATH adjustments and `nix` binary resolution helpers
|
- *path.sh* — PATH adjustments and `nix` binary resolution helpers
|
||||||
- *symlinks.sh* — user/global symlink helpers for stable `nix` discovery
|
- *symlinks.sh* — user/global symlink helpers for stable `nix` discovery
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# shellcheck source=lib/config.sh
|
|
||||||
# shellcheck source=lib/detect.sh
|
|
||||||
# shellcheck source=lib/path.sh
|
|
||||||
# shellcheck source=lib/symlinks.sh
|
|
||||||
# shellcheck source=lib/users.sh
|
|
||||||
# shellcheck source=lib/install.sh
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "${SCRIPT_DIR}/lib/config.sh"
|
# shellcheck source=./scripts/nix/lib/bootstrap_config.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/bootstrap_config.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/detect.sh
|
||||||
source "${SCRIPT_DIR}/lib/detect.sh"
|
source "${SCRIPT_DIR}/lib/detect.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/path.sh
|
||||||
source "${SCRIPT_DIR}/lib/path.sh"
|
source "${SCRIPT_DIR}/lib/path.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/symlinks.sh
|
||||||
source "${SCRIPT_DIR}/lib/symlinks.sh"
|
source "${SCRIPT_DIR}/lib/symlinks.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/users.sh
|
||||||
source "${SCRIPT_DIR}/lib/users.sh"
|
source "${SCRIPT_DIR}/lib/users.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/install.sh
|
||||||
source "${SCRIPT_DIR}/lib/install.sh"
|
source "${SCRIPT_DIR}/lib/install.sh"
|
||||||
|
|
||||||
|
# shellcheck source=./scripts/nix/lib/nix_conf_file.sh
|
||||||
|
source "${SCRIPT_DIR}/lib/nix_conf_file.sh"
|
||||||
|
|
||||||
echo "[init-nix] Starting Nix initialization..."
|
echo "[init-nix] Starting Nix initialization..."
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
@@ -26,6 +33,7 @@ main() {
|
|||||||
ensure_nix_on_path
|
ensure_nix_on_path
|
||||||
|
|
||||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||||
|
nixconf_ensure_experimental_features
|
||||||
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
|
ensure_global_nix_symlinks "$(resolve_nix_bin 2>/dev/null || true)"
|
||||||
else
|
else
|
||||||
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
|
ensure_user_nix_symlink "$(resolve_nix_bin 2>/dev/null || true)"
|
||||||
@@ -106,6 +114,10 @@ main() {
|
|||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
ensure_nix_on_path
|
ensure_nix_on_path
|
||||||
|
|
||||||
|
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||||
|
nixconf_ensure_experimental_features
|
||||||
|
fi
|
||||||
|
|
||||||
local nix_bin_post
|
local nix_bin_post
|
||||||
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"
|
nix_bin_post="$(resolve_nix_bin 2>/dev/null || true)"
|
||||||
|
|
||||||
|
|||||||
0
scripts/nix/lib/config.sh → scripts/nix/lib/bootstrap_config.sh
Normal file → Executable file
0
scripts/nix/lib/config.sh → scripts/nix/lib/bootstrap_config.sh
Normal file → Executable file
0
scripts/nix/lib/detect.sh
Normal file → Executable file
0
scripts/nix/lib/detect.sh
Normal file → Executable file
0
scripts/nix/lib/install.sh
Normal file → Executable file
0
scripts/nix/lib/install.sh
Normal file → Executable file
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}"
|
||||||
|
}
|
||||||
0
scripts/nix/lib/path.sh
Normal file → Executable file
0
scripts/nix/lib/path.sh
Normal file → Executable file
52
scripts/nix/lib/retry_403.sh
Executable file
52
scripts/nix/lib/retry_403.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ -n "${PKGMGR_NIX_RETRY_403_SH:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
PKGMGR_NIX_RETRY_403_SH=1
|
||||||
|
|
||||||
|
# Retry only when we see the GitHub API rate limit 403 error during nix flake evaluation.
|
||||||
|
# Retries 7 times with delays: 10, 30, 50, 80, 130, 210, 420 seconds.
|
||||||
|
run_with_github_403_retry() {
|
||||||
|
local -a delays=(10 30 50 80 130 210 420)
|
||||||
|
local attempt=0
|
||||||
|
local max_retries="${#delays[@]}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local err tmp
|
||||||
|
tmp="$(mktemp -t nix-err.XXXXXX)"
|
||||||
|
err=0
|
||||||
|
|
||||||
|
# Run the command; capture stderr for inspection while preserving stdout.
|
||||||
|
if "$@" 2>"$tmp"; then
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
err=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only retry on the specific GitHub API rate limit 403 case.
|
||||||
|
if grep -qE 'HTTP error 403' "$tmp" && grep -qiE 'API rate limit exceeded|api\.github\.com' "$tmp"; then
|
||||||
|
if (( attempt >= max_retries )); then
|
||||||
|
cat "$tmp" >&2
|
||||||
|
rm -f "$tmp"
|
||||||
|
return "$err"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local sleep_s="${delays[$attempt]}"
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
|
||||||
|
echo "[nix-retry] GitHub API rate-limit (403). Retry ${attempt}/${max_retries} in ${sleep_s}s: $*" >&2
|
||||||
|
cat "$tmp" >&2
|
||||||
|
rm -f "$tmp"
|
||||||
|
sleep "$sleep_s"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Not our retry case -> fail fast with original stderr.
|
||||||
|
cat "$tmp" >&2
|
||||||
|
rm -f "$tmp"
|
||||||
|
return "$err"
|
||||||
|
done
|
||||||
|
}
|
||||||
0
scripts/nix/lib/symlinks.sh
Normal file → Executable file
0
scripts/nix/lib/symlinks.sh
Normal file → Executable file
0
scripts/nix/lib/users.sh
Normal file → Executable file
0
scripts/nix/lib/users.sh
Normal file → Executable file
@@ -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] Nix mode enabled (NIX_ENABLED=1)."
|
||||||
echo "[setup] Skipping virtualenv creation and dependency installation."
|
echo "[setup] Skipping virtualenv creation and dependency installation."
|
||||||
echo "[setup] Running main.py install via system python3..."
|
echo "[setup] Running install via system python3..."
|
||||||
python3 main.py install
|
python3 -m pkgmgr install
|
||||||
echo "[setup] Setup finished (Nix mode)."
|
echo "[setup] Setup finished (Nix mode)."
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
cd "${PROJECT_ROOT}"
|
cd "${PROJECT_ROOT}"
|
||||||
|
|
||||||
VENV_DIR="${HOME}/.venvs/pkgmgr"
|
VENV_DIR="${HOME}/.venvs/pkgmgr"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
|
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -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] 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"
|
echo "[setup] Ensuring global virtualenv root: ${HOME}/.venvs"
|
||||||
mkdir -p "${HOME}/.venvs"
|
mkdir -p "${HOME}/.venvs"
|
||||||
|
|
||||||
@@ -90,8 +88,8 @@ for rc in "${HOME}/.bashrc" "${HOME}/.zshrc"; do
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "[setup] Running main.py install via venv Python..."
|
echo "[setup] Running install via venv Python..."
|
||||||
"${VENV_DIR}/bin/python" main.py install
|
"${VENV_DIR}/bin/python" -m pkgmgr install
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "[setup] Developer setup complete."
|
echo "[setup] Developer setup complete."
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running E2E tests: $distro"
|
echo ">>> Running E2E tests: $PKGMGR_DISTRO"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v "pkgmgr_nix_store_${distro}:/nix" \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
"pkgmgr-${distro}" \
|
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ docker run --rm \
|
|||||||
# Gitdir path shown in the "dubious ownership" error
|
# Gitdir path shown in the "dubious ownership" error
|
||||||
git config --global --add safe.directory /src/.git || true
|
git config --global --add safe.directory /src/.git || true
|
||||||
# Ephemeral CI containers: allow all paths as a last resort
|
# Ephemeral CI containers: allow all paths as a last resort
|
||||||
git config --global --add safe.directory '*' || true
|
git config --global --add safe.directory "*" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run the E2E tests inside the Nix development shell
|
# Run the E2E tests inside the Nix development shell
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMAGE="pkgmgr-${distro}"
|
IMAGE="pkgmgr-${PKGMGR_DISTRO}"
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running Nix flake-only test in ${distro} container"
|
echo ">>> Running Nix flake-only test in ${PKGMGR_DISTRO} container"
|
||||||
echo ">>> Image: ${IMAGE}"
|
echo ">>> Image: ${IMAGE}"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v "pkgmgr_nix_store_${distro}:/nix" \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
"${IMAGE}" \
|
"${IMAGE}" \
|
||||||
@@ -27,7 +27,7 @@ docker run --rm \
|
|||||||
echo ">>> preflight: nix must exist in image"
|
echo ">>> preflight: nix must exist in image"
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
echo "NO_NIX"
|
echo "NO_NIX"
|
||||||
echo "ERROR: nix not found in image '\'''"${IMAGE}"''\'' (distro='"${distro}"')"
|
echo "ERROR: nix not found in image '"${IMAGE}"' (PKGMGR_DISTRO='"${PKGMGR_DISTRO}"')"
|
||||||
echo "HINT: Ensure Nix is installed during image build for this distro."
|
echo "HINT: Ensure Nix is installed during image build for this distro."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -35,14 +35,28 @@ docker run --rm \
|
|||||||
echo ">>> nix version"
|
echo ">>> nix version"
|
||||||
nix --version
|
nix --version
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Retry helper for GitHub API rate-limit (HTTP 403)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if [[ -f /src/scripts/nix/lib/retry_403.sh ]]; then
|
||||||
|
# shellcheck source=./scripts/nix/lib/retry_403.sh
|
||||||
|
source /src/scripts/nix/lib/retry_403.sh
|
||||||
|
elif [[ -f ./scripts/nix/lib/retry_403.sh ]]; then
|
||||||
|
# shellcheck source=./scripts/nix/lib/retry_403.sh
|
||||||
|
source ./scripts/nix/lib/retry_403.sh
|
||||||
|
else
|
||||||
|
echo "ERROR: retry helper not found: scripts/nix/lib/retry_403.sh"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ">>> nix flake show"
|
echo ">>> nix flake show"
|
||||||
nix flake show . --no-write-lock-file >/dev/null
|
run_with_github_403_retry nix flake show . --no-write-lock-file >/dev/null
|
||||||
|
|
||||||
echo ">>> nix build .#default"
|
echo ">>> nix build .#default"
|
||||||
nix build .#default --no-link --no-write-lock-file
|
run_with_github_403_retry nix build .#default --no-link --no-write-lock-file
|
||||||
|
|
||||||
echo ">>> nix run .#pkgmgr -- --help"
|
echo ">>> nix run .#pkgmgr -- --help"
|
||||||
nix run .#pkgmgr -- --help --no-write-lock-file
|
run_with_github_403_retry nix run .#pkgmgr -- --help --no-write-lock-file
|
||||||
|
|
||||||
echo ">>> OK: Nix flake-only test succeeded."
|
echo ">>> OK: Nix flake-only test succeeded."
|
||||||
'
|
'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
IMAGE="pkgmgr-$distro"
|
IMAGE="pkgmgr-$PKGMGR_DISTRO"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "------------------------------------------------------------"
|
echo "------------------------------------------------------------"
|
||||||
@@ -16,9 +16,9 @@ echo
|
|||||||
# Run the command and capture the output
|
# Run the command and capture the output
|
||||||
if OUTPUT=$(docker run --rm \
|
if OUTPUT=$(docker run --rm \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
"$IMAGE" 2>&1); then
|
"$IMAGE" 2>&1); then
|
||||||
echo "$OUTPUT"
|
echo "$OUTPUT"
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running INTEGRATION tests in ${distro} container"
|
echo ">>> Running INTEGRATION tests in ${PKGMGR_DISTRO} container"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
"pkgmgr-${distro}" \
|
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -e;
|
set -e;
|
||||||
git config --global --add safe.directory /src || true;
|
git config --global --add safe.directory /src || true;
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running UNIT tests in ${distro} container"
|
echo ">>> Running UNIT tests in ${PKGMGR_DISTRO} container"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v "$(pwd):/src" \
|
-v "$(pwd):/src" \
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
-v "pkgmgr_nix_cache_${PKGMGR_DISTRO}:/root/.cache/nix" \
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
-v "pkgmgr_nix_store_${PKGMGR_DISTRO}:/nix" \
|
||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e REINSTALL_PKGMGR=1 \
|
-e REINSTALL_PKGMGR=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
"pkgmgr-${distro}" \
|
"pkgmgr-${PKGMGR_DISTRO}" \
|
||||||
bash -lc '
|
bash -lc '
|
||||||
set -e;
|
set -e;
|
||||||
git config --global --add safe.directory /src || true;
|
git config --global --add safe.directory /src || true;
|
||||||
|
|||||||
@@ -19,12 +19,20 @@ fi
|
|||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Remove auto-activation lines from shell RC files
|
# Remove auto-activation lines from shell RC files
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
RC_PATTERN='\.venvs\/pkgmgr\/bin\/activate"; if \[ -n "\$${PS1:-}" \]; then echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi; fi'
|
# Matches:
|
||||||
|
# ~/.venvs/pkgmgr/bin/activate
|
||||||
|
# ./.venvs/pkgmgr/bin/activate
|
||||||
|
RC_PATTERN='(\./)?\.venvs/pkgmgr/bin/activate'
|
||||||
|
|
||||||
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
|
echo "[uninstall] Cleaning up ~/.bashrc and ~/.zshrc entries..."
|
||||||
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
|
||||||
if [[ -f "$rc" ]]; then
|
if [[ -f "$rc" ]]; then
|
||||||
sed -i "/$RC_PATTERN/d" "$rc"
|
# Remove activation lines (functional)
|
||||||
|
sed -E -i "/$RC_PATTERN/d" "$rc"
|
||||||
|
|
||||||
|
# Remove leftover echo / cosmetic lines referencing pkgmgr venv
|
||||||
|
sed -i '/\.venvs\/pkgmgr/d' "$rc"
|
||||||
|
|
||||||
echo "[uninstall] Cleaned $rc"
|
echo "[uninstall] Cleaned $rc"
|
||||||
else
|
else
|
||||||
echo "[uninstall] File not found: $rc (skipped)"
|
echo "[uninstall] File not found: $rc (skipped)"
|
||||||
|
|||||||
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 yaml
|
||||||
import os
|
import os
|
||||||
|
from pkgmgr.core.config.save import save_user_config
|
||||||
|
|
||||||
def interactive_add(config,USER_CONFIG_PATH:str):
|
def interactive_add(config,USER_CONFIG_PATH:str):
|
||||||
"""Interactively prompt the user to add a new repository entry to the user config."""
|
"""Interactively prompt the user to add a new repository entry to the user config."""
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ def config_init(
|
|||||||
# Announce where we will write the result
|
# Announce where we will write the result
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
print("============================================================")
|
print("============================================================")
|
||||||
print(f"[INIT] Writing user configuration to:")
|
print("[INIT] Writing user configuration to:")
|
||||||
print(f" {user_config_path}")
|
print(f" {user_config_path}")
|
||||||
print("============================================================")
|
print("============================================================")
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ def config_init(
|
|||||||
defaults_config["directories"]["repositories"]
|
defaults_config["directories"]["repositories"]
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"[INIT] Scanning repository base directory:")
|
print("[INIT] Scanning repository base directory:")
|
||||||
print(f" {repositories_base_dir}")
|
print(f" {repositories_base_dir}")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ def config_init(
|
|||||||
if new_entries:
|
if new_entries:
|
||||||
user_config.setdefault("repositories", []).extend(new_entries)
|
user_config.setdefault("repositories", []).extend(new_entries)
|
||||||
save_user_config(user_config, user_config_path)
|
save_user_config(user_config, user_config_path)
|
||||||
print(f"[SAVE] Wrote user configuration to:")
|
print("[SAVE] Wrote user configuration to:")
|
||||||
print(f" {user_config_path}")
|
print(f" {user_config_path}")
|
||||||
else:
|
else:
|
||||||
print("[INFO] No new repositories were added.")
|
print("[INFO] No new repositories were added.")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# src/pkgmgr/actions/install/__init__.py
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
@@ -36,10 +37,8 @@ from pkgmgr.actions.install.installers.makefile import (
|
|||||||
)
|
)
|
||||||
from pkgmgr.actions.install.pipeline import InstallationPipeline
|
from pkgmgr.actions.install.pipeline import InstallationPipeline
|
||||||
|
|
||||||
|
|
||||||
Repository = Dict[str, Any]
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
# All available installers, in the order they should be considered.
|
|
||||||
INSTALLERS = [
|
INSTALLERS = [
|
||||||
ArchPkgbuildInstaller(),
|
ArchPkgbuildInstaller(),
|
||||||
DebianControlInstaller(),
|
DebianControlInstaller(),
|
||||||
@@ -50,11 +49,6 @@ INSTALLERS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Internal helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def _ensure_repo_dir(
|
def _ensure_repo_dir(
|
||||||
repo: Repository,
|
repo: Repository,
|
||||||
repositories_base_dir: str,
|
repositories_base_dir: str,
|
||||||
@@ -74,7 +68,7 @@ def _ensure_repo_dir(
|
|||||||
if not os.path.exists(repo_dir):
|
if not os.path.exists(repo_dir):
|
||||||
print(
|
print(
|
||||||
f"Repository directory '{repo_dir}' does not exist. "
|
f"Repository directory '{repo_dir}' does not exist. "
|
||||||
f"Cloning it now..."
|
"Cloning it now..."
|
||||||
)
|
)
|
||||||
clone_repos(
|
clone_repos(
|
||||||
[repo],
|
[repo],
|
||||||
@@ -87,7 +81,7 @@ def _ensure_repo_dir(
|
|||||||
if not os.path.exists(repo_dir):
|
if not os.path.exists(repo_dir):
|
||||||
print(
|
print(
|
||||||
f"Cloning failed for repository {identifier}. "
|
f"Cloning failed for repository {identifier}. "
|
||||||
f"Skipping installation."
|
"Skipping installation."
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -137,6 +131,7 @@ def _create_context(
|
|||||||
quiet: bool,
|
quiet: bool,
|
||||||
clone_mode: str,
|
clone_mode: str,
|
||||||
update_dependencies: bool,
|
update_dependencies: bool,
|
||||||
|
force_update: bool,
|
||||||
) -> RepoContext:
|
) -> RepoContext:
|
||||||
"""
|
"""
|
||||||
Build a RepoContext instance for the given repository.
|
Build a RepoContext instance for the given repository.
|
||||||
@@ -153,14 +148,10 @@ def _create_context(
|
|||||||
quiet=quiet,
|
quiet=quiet,
|
||||||
clone_mode=clone_mode,
|
clone_mode=clone_mode,
|
||||||
update_dependencies=update_dependencies,
|
update_dependencies=update_dependencies,
|
||||||
|
force_update=force_update,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Public API
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def install_repos(
|
def install_repos(
|
||||||
selected_repos: List[Repository],
|
selected_repos: List[Repository],
|
||||||
repositories_base_dir: str,
|
repositories_base_dir: str,
|
||||||
@@ -171,10 +162,14 @@ def install_repos(
|
|||||||
quiet: bool,
|
quiet: bool,
|
||||||
clone_mode: str,
|
clone_mode: str,
|
||||||
update_dependencies: bool,
|
update_dependencies: bool,
|
||||||
|
force_update: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Install one or more repositories according to the configured installers
|
Install one or more repositories according to the configured installers
|
||||||
and the CLI layer precedence rules.
|
and the CLI layer precedence rules.
|
||||||
|
|
||||||
|
If force_update=True, installers of the currently active layer are allowed
|
||||||
|
to run again (upgrade/refresh), even if that layer is already loaded.
|
||||||
"""
|
"""
|
||||||
pipeline = InstallationPipeline(INSTALLERS)
|
pipeline = InstallationPipeline(INSTALLERS)
|
||||||
|
|
||||||
@@ -213,6 +208,7 @@ def install_repos(
|
|||||||
quiet=quiet,
|
quiet=quiet,
|
||||||
clone_mode=clone_mode,
|
clone_mode=clone_mode,
|
||||||
update_dependencies=update_dependencies,
|
update_dependencies=update_dependencies,
|
||||||
|
force_update=force_update,
|
||||||
)
|
)
|
||||||
|
|
||||||
pipeline.run(ctx)
|
pipeline.run(ctx)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# src/pkgmgr/actions/install/context.py
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
@@ -28,3 +29,6 @@ class RepoContext:
|
|||||||
quiet: bool
|
quiet: bool
|
||||||
clone_mode: str
|
clone_mode: str
|
||||||
update_dependencies: bool
|
update_dependencies: bool
|
||||||
|
|
||||||
|
# If True, allow re-running installers of the currently active layer.
|
||||||
|
force_update: bool = False
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# src/pkgmgr/actions/install/installers/makefile.py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -9,89 +10,45 @@ from pkgmgr.core.command.run import run_command
|
|||||||
|
|
||||||
|
|
||||||
class MakefileInstaller(BaseInstaller):
|
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"
|
layer = "makefile"
|
||||||
MAKEFILE_NAME = "Makefile"
|
MAKEFILE_NAME = "Makefile"
|
||||||
|
|
||||||
def supports(self, ctx: RepoContext) -> bool:
|
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 os.environ.get("PKGMGR_DISABLE_MAKEFILE_INSTALLER") == "1":
|
||||||
if not ctx.quiet:
|
if not ctx.quiet:
|
||||||
print(
|
print("[INFO] PKGMGR_DISABLE_MAKEFILE_INSTALLER=1 – skipping MakefileInstaller.")
|
||||||
"[INFO] MakefileInstaller is disabled via "
|
|
||||||
"PKGMGR_DISABLE_MAKEFILE_INSTALLER."
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
|
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
|
||||||
return os.path.exists(makefile_path)
|
return os.path.exists(makefile_path)
|
||||||
|
|
||||||
def _has_install_target(self, makefile_path: str) -> bool:
|
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:
|
try:
|
||||||
with open(makefile_path, "r", encoding="utf-8", errors="ignore") as f:
|
with open(makefile_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Simple heuristics: look for "install:" or targets starting with "install-"
|
|
||||||
if re.search(r"^install\s*:", content, flags=re.MULTILINE):
|
if re.search(r"^install\s*:", content, flags=re.MULTILINE):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if re.search(r"^install-[a-zA-Z0-9_-]*\s*:", content, flags=re.MULTILINE):
|
if re.search(r"^install-[a-zA-Z0-9_-]*\s*:", content, flags=re.MULTILINE):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def run(self, ctx: RepoContext) -> None:
|
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)
|
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
|
||||||
|
|
||||||
if not os.path.exists(makefile_path):
|
if not os.path.exists(makefile_path):
|
||||||
if not ctx.quiet:
|
|
||||||
print(
|
|
||||||
f"[pkgmgr] Makefile '{makefile_path}' not found, "
|
|
||||||
"skipping MakefileInstaller."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._has_install_target(makefile_path):
|
if not self._has_install_target(makefile_path):
|
||||||
if not ctx.quiet:
|
if not ctx.quiet:
|
||||||
print(
|
print(f"[pkgmgr] No 'install' target found in {makefile_path}.")
|
||||||
f"[pkgmgr] No 'install' target found in {makefile_path}."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if not ctx.quiet:
|
if not ctx.quiet:
|
||||||
print(
|
print(f"[pkgmgr] Running make install for {ctx.identifier} (MakefileInstaller)")
|
||||||
f"[pkgmgr] Running 'make install' in {ctx.repo_dir} "
|
|
||||||
f"(MakefileInstaller)"
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd = "make install"
|
run_command("make install", cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
|
||||||
|
if ctx.force_update and not ctx.quiet:
|
||||||
|
print(f"[makefile] repo '{ctx.identifier}' successfully upgraded.")
|
||||||
|
|||||||
@@ -1,32 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
from __future__ import annotations
|
||||||
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 one or more flake outputs via `nix profile install`.
|
|
||||||
- For the package-manager repo:
|
|
||||||
* `pkgmgr` is mandatory (CLI), `default` is optional.
|
|
||||||
- For all other repos:
|
|
||||||
* `default` is mandatory.
|
|
||||||
|
|
||||||
Special handling:
|
|
||||||
- If PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 is set, the installer is
|
|
||||||
globally disabled (useful for CI or debugging).
|
|
||||||
|
|
||||||
The higher-level InstallationPipeline and CLI-layer model decide when this
|
|
||||||
installer is allowed to run, based on where the current CLI comes from
|
|
||||||
(e.g. Nix, OS packages, Python, Makefile).
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
from typing import TYPE_CHECKING, List, Tuple
|
from typing import TYPE_CHECKING, List, Tuple
|
||||||
|
|
||||||
from pkgmgr.actions.install.installers.base import BaseInstaller
|
from pkgmgr.actions.install.installers.base import BaseInstaller
|
||||||
@@ -34,132 +14,225 @@ from pkgmgr.core.command.run import run_command
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pkgmgr.actions.install.context import RepoContext
|
from pkgmgr.actions.install.context import RepoContext
|
||||||
from pkgmgr.actions.install import InstallContext
|
|
||||||
|
|
||||||
|
|
||||||
class NixFlakeInstaller(BaseInstaller):
|
class NixFlakeInstaller(BaseInstaller):
|
||||||
"""Install Nix flake profiles for repositories that define flake.nix."""
|
|
||||||
|
|
||||||
# Logical layer name, used by capability matchers.
|
|
||||||
layer = "nix"
|
layer = "nix"
|
||||||
|
|
||||||
FLAKE_FILE = "flake.nix"
|
FLAKE_FILE = "flake.nix"
|
||||||
PROFILE_NAME = "package-manager"
|
|
||||||
|
|
||||||
def supports(self, ctx: "RepoContext") -> bool:
|
def supports(self, ctx: "RepoContext") -> bool:
|
||||||
"""
|
|
||||||
Only support repositories that:
|
|
||||||
- Are NOT explicitly disabled via PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1,
|
|
||||||
- Have a flake.nix,
|
|
||||||
- And have the `nix` command available.
|
|
||||||
"""
|
|
||||||
# Optional global kill-switch for CI or debugging.
|
|
||||||
if os.environ.get("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER") == "1":
|
if os.environ.get("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER") == "1":
|
||||||
print(
|
if not ctx.quiet:
|
||||||
"[INFO] PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 – "
|
print("[INFO] PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 – skipping NixFlakeInstaller.")
|
||||||
"NixFlakeInstaller is disabled."
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Nix must be available.
|
|
||||||
if shutil.which("nix") is None:
|
if shutil.which("nix") is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# flake.nix must exist in the repository.
|
return os.path.exists(os.path.join(ctx.repo_dir, self.FLAKE_FILE))
|
||||||
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 _profile_outputs(self, ctx: "RepoContext") -> List[Tuple[str, bool]]:
|
def _profile_outputs(self, ctx: "RepoContext") -> List[Tuple[str, bool]]:
|
||||||
"""
|
# (output_name, allow_failure)
|
||||||
Decide which flake outputs to install and whether failures are fatal.
|
if ctx.identifier in {"pkgmgr", "package-manager"}:
|
||||||
|
|
||||||
Returns a list of (output_name, allow_failure) tuples.
|
|
||||||
|
|
||||||
Rules:
|
|
||||||
- For the package-manager repo (identifier 'pkgmgr' or 'package-manager'):
|
|
||||||
[("pkgmgr", False), ("default", True)]
|
|
||||||
- For all other repos:
|
|
||||||
[("default", False)]
|
|
||||||
"""
|
|
||||||
ident = ctx.identifier
|
|
||||||
|
|
||||||
if ident in {"pkgmgr", "package-manager"}:
|
|
||||||
# pkgmgr: main CLI output is "pkgmgr" (mandatory),
|
|
||||||
# "default" is nice-to-have (non-fatal).
|
|
||||||
return [("pkgmgr", False), ("default", True)]
|
return [("pkgmgr", False), ("default", True)]
|
||||||
|
|
||||||
# Generic repos: we expect a sensible "default" package/app.
|
|
||||||
# Failure to install it is considered fatal.
|
|
||||||
return [("default", False)]
|
return [("default", False)]
|
||||||
|
|
||||||
def run(self, ctx: "InstallContext") -> None:
|
def _installable(self, ctx: "RepoContext", output: str) -> str:
|
||||||
"""
|
return f"{ctx.repo_dir}#{output}"
|
||||||
Install Nix flake profile outputs.
|
|
||||||
|
|
||||||
For the package-manager repo, failure installing 'pkgmgr' is fatal,
|
def _run(self, ctx: "RepoContext", cmd: str, allow_failure: bool = True):
|
||||||
failure installing 'default' is non-fatal.
|
return run_command(
|
||||||
For other repos, failure installing 'default' is fatal.
|
cmd,
|
||||||
"""
|
cwd=ctx.repo_dir,
|
||||||
# Reuse supports() to keep logic in one place.
|
preview=ctx.preview,
|
||||||
if not self.supports(ctx): # type: ignore[arg-type]
|
allow_failure=allow_failure,
|
||||||
return
|
|
||||||
|
|
||||||
outputs = self._profile_outputs(ctx) # list of (name, allow_failure)
|
|
||||||
|
|
||||||
print(
|
|
||||||
"Nix flake detected in "
|
|
||||||
f"{ctx.identifier}, attempting to install profile outputs: "
|
|
||||||
+ ", ".join(name for name, _ in outputs)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle the "already installed" case up-front for the shared profile.
|
def _profile_list_json(self, ctx: "RepoContext") -> dict:
|
||||||
self._ensure_old_profile_removed(ctx) # type: ignore[arg-type]
|
"""
|
||||||
|
Read current Nix profile entries as JSON (best-effort).
|
||||||
|
|
||||||
for output, allow_failure in outputs:
|
NOTE: Nix versions differ:
|
||||||
cmd = f"nix profile install {ctx.repo_dir}#{output}"
|
- Newer: {"elements": [ { "index": 0, "attrPath": "...", ... }, ... ]}
|
||||||
print(f"[INFO] Running: {cmd}")
|
- Older: {"elements": [ "nixpkgs#hello", ... ]} (strings)
|
||||||
ret = os.system(cmd)
|
|
||||||
|
|
||||||
# Extract real exit code from os.system() result
|
We return {} on failure or in preview mode.
|
||||||
if os.WIFEXITED(ret):
|
"""
|
||||||
exit_code = os.WEXITSTATUS(ret)
|
if ctx.preview:
|
||||||
else:
|
return {}
|
||||||
# abnormal termination (signal etc.) – keep raw value
|
|
||||||
exit_code = ret
|
|
||||||
|
|
||||||
if exit_code == 0:
|
proc = subprocess.run(
|
||||||
print(f"Nix flake output '{output}' successfully installed.")
|
["nix", "profile", "list", "--json"],
|
||||||
|
check=False,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
env=os.environ.copy(),
|
||||||
|
)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(proc.stdout or "{}")
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _find_installed_indices_for_output(self, ctx: "RepoContext", output: str) -> List[int]:
|
||||||
|
"""
|
||||||
|
Find installed profile indices for a given output.
|
||||||
|
|
||||||
|
Works across Nix JSON variants:
|
||||||
|
- If elements are dicts: we can extract indices.
|
||||||
|
- If elements are strings: we cannot extract indices -> return [].
|
||||||
|
"""
|
||||||
|
data = self._profile_list_json(ctx)
|
||||||
|
elements = data.get("elements", []) or []
|
||||||
|
|
||||||
|
matches: List[int] = []
|
||||||
|
|
||||||
|
for el in elements:
|
||||||
|
# Legacy JSON format: plain strings -> no index information
|
||||||
|
if not isinstance(el, dict):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(f"[Error] Failed to install Nix flake output '{output}'")
|
idx = el.get("index")
|
||||||
print(f"[Error] Command exited with code {exit_code}")
|
if idx is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if not allow_failure:
|
attr_path = el.get("attrPath") or el.get("attr_path") or ""
|
||||||
raise SystemExit(exit_code)
|
pname = el.get("pname") or ""
|
||||||
|
name = el.get("name") or ""
|
||||||
|
|
||||||
|
if attr_path == output:
|
||||||
|
matches.append(int(idx))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pname == output or name == output:
|
||||||
|
matches.append(int(idx))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(attr_path, str) and attr_path.endswith(f".{output}"):
|
||||||
|
matches.append(int(idx))
|
||||||
|
continue
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
def _upgrade_index(self, ctx: "RepoContext", index: int) -> bool:
|
||||||
|
cmd = f"nix profile upgrade --refresh {index}"
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] upgrade: {cmd}")
|
||||||
|
res = self._run(ctx, cmd, allow_failure=True)
|
||||||
|
return res.returncode == 0
|
||||||
|
|
||||||
|
def _remove_index(self, ctx: "RepoContext", index: int) -> None:
|
||||||
|
cmd = f"nix profile remove {index}"
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] remove: {cmd}")
|
||||||
|
self._run(ctx, cmd, allow_failure=True)
|
||||||
|
|
||||||
|
def _install_only(self, ctx: "RepoContext", output: str, allow_failure: bool) -> None:
|
||||||
|
"""
|
||||||
|
Install output; on failure, try index-based upgrade/remove+install if possible.
|
||||||
|
"""
|
||||||
|
installable = self._installable(ctx, output)
|
||||||
|
install_cmd = f"nix profile install {installable}"
|
||||||
|
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] install: {install_cmd}")
|
||||||
|
|
||||||
|
res = self._run(ctx, install_cmd, allow_failure=True)
|
||||||
|
if res.returncode == 0:
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] output '{output}' successfully installed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not ctx.quiet:
|
||||||
print(
|
print(
|
||||||
"[Warning] Continuing despite failure to install "
|
f"[nix] install failed for '{output}' (exit {res.returncode}), "
|
||||||
f"optional output '{output}'."
|
"trying index-based upgrade/remove+install..."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
indices = self._find_installed_indices_for_output(ctx, output)
|
||||||
|
|
||||||
|
# 1) Try upgrading existing indices (only possible on newer JSON format)
|
||||||
|
upgraded = False
|
||||||
|
for idx in indices:
|
||||||
|
if self._upgrade_index(ctx, idx):
|
||||||
|
upgraded = True
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] output '{output}' successfully upgraded (index {idx}).")
|
||||||
|
|
||||||
|
if upgraded:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2) Remove matching indices and retry install
|
||||||
|
if indices and not ctx.quiet:
|
||||||
|
print(f"[nix] upgrade failed; removing indices {indices} and reinstalling '{output}'.")
|
||||||
|
|
||||||
|
for idx in indices:
|
||||||
|
self._remove_index(ctx, idx)
|
||||||
|
|
||||||
|
final = self._run(ctx, install_cmd, allow_failure=True)
|
||||||
|
if final.returncode == 0:
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] output '{output}' successfully re-installed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = f"[ERROR] Failed to install Nix flake output '{output}' (exit {final.returncode})"
|
||||||
|
print(msg)
|
||||||
|
|
||||||
|
if not allow_failure:
|
||||||
|
raise SystemExit(final.returncode)
|
||||||
|
|
||||||
|
print(f"[WARNING] Continuing despite failure of optional output '{output}'.")
|
||||||
|
|
||||||
|
def _force_upgrade_output(self, ctx: "RepoContext", output: str, allow_failure: bool) -> None:
|
||||||
|
"""
|
||||||
|
force_update path:
|
||||||
|
- Prefer upgrading existing entries via indices (if we can discover them).
|
||||||
|
- If no indices (legacy JSON) or upgrade fails, fall back to install-only logic.
|
||||||
|
"""
|
||||||
|
indices = self._find_installed_indices_for_output(ctx, output)
|
||||||
|
|
||||||
|
upgraded_any = False
|
||||||
|
for idx in indices:
|
||||||
|
if self._upgrade_index(ctx, idx):
|
||||||
|
upgraded_any = True
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(f"[nix] output '{output}' successfully upgraded (index {idx}).")
|
||||||
|
|
||||||
|
if upgraded_any:
|
||||||
|
# Make upgrades visible to tests
|
||||||
|
print(f"[nix] output '{output}' successfully upgraded.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if indices and not ctx.quiet:
|
||||||
|
print(f"[nix] upgrade failed; removing indices {indices} and reinstalling '{output}'.")
|
||||||
|
|
||||||
|
for idx in indices:
|
||||||
|
self._remove_index(ctx, idx)
|
||||||
|
|
||||||
|
# Ensure installed (includes its own fallback logic)
|
||||||
|
self._install_only(ctx, output, allow_failure)
|
||||||
|
|
||||||
|
# Make upgrades visible to tests (semantic: update requested)
|
||||||
|
print(f"[nix] output '{output}' successfully upgraded.")
|
||||||
|
|
||||||
|
def run(self, ctx: "RepoContext") -> None:
|
||||||
|
if not self.supports(ctx):
|
||||||
|
return
|
||||||
|
|
||||||
|
outputs = self._profile_outputs(ctx)
|
||||||
|
|
||||||
|
if not ctx.quiet:
|
||||||
|
print(
|
||||||
|
"[nix] flake detected in "
|
||||||
|
f"{ctx.identifier}, ensuring outputs: "
|
||||||
|
+ ", ".join(name for name, _ in outputs)
|
||||||
|
)
|
||||||
|
|
||||||
|
for output, allow_failure in outputs:
|
||||||
|
if ctx.force_update:
|
||||||
|
self._force_upgrade_output(ctx, output, allow_failure)
|
||||||
|
else:
|
||||||
|
self._install_only(ctx, output, allow_failure)
|
||||||
|
|||||||
@@ -1,104 +1,40 @@
|
|||||||
#!/usr/bin/env python3
|
# src/pkgmgr/actions/install/installers/python.py
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
PythonInstaller — install Python projects defined via pyproject.toml.
|
|
||||||
|
|
||||||
Installation rules:
|
|
||||||
|
|
||||||
1. pip command resolution:
|
|
||||||
a) If PKGMGR_PIP is set → use it exactly as provided.
|
|
||||||
b) Else if running inside a virtualenv → use `sys.executable -m pip`.
|
|
||||||
c) Else → create/use a per-repository virtualenv under ~/.venvs/<repo>/.
|
|
||||||
|
|
||||||
2. Installation target:
|
|
||||||
- Always install into the resolved pip environment.
|
|
||||||
- Never modify system Python, never rely on --user.
|
|
||||||
- Nix-immutable systems (PEP 668) are automatically avoided because we
|
|
||||||
never touch system Python.
|
|
||||||
|
|
||||||
3. The installer is skipped when:
|
|
||||||
- PKGMGR_DISABLE_PYTHON_INSTALLER=1 is set.
|
|
||||||
- The repository has no pyproject.toml.
|
|
||||||
|
|
||||||
All pip failures are treated as fatal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import subprocess
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from pkgmgr.actions.install.installers.base import BaseInstaller
|
from pkgmgr.actions.install.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.actions.install.context import RepoContext
|
||||||
from pkgmgr.core.command.run import run_command
|
from pkgmgr.core.command.run import run_command
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from pkgmgr.actions.install.context import RepoContext
|
|
||||||
from pkgmgr.actions.install import InstallContext
|
|
||||||
|
|
||||||
|
|
||||||
class PythonInstaller(BaseInstaller):
|
class PythonInstaller(BaseInstaller):
|
||||||
"""Install Python projects and dependencies via pip using isolated environments."""
|
|
||||||
|
|
||||||
layer = "python"
|
layer = "python"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
# Installer activation logic
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def supports(self, ctx: "RepoContext") -> bool:
|
|
||||||
"""
|
|
||||||
Return True if this installer should handle this repository.
|
|
||||||
|
|
||||||
The installer is active only when:
|
|
||||||
- A pyproject.toml exists in the repo, and
|
|
||||||
- PKGMGR_DISABLE_PYTHON_INSTALLER is not set.
|
|
||||||
"""
|
|
||||||
if os.environ.get("PKGMGR_DISABLE_PYTHON_INSTALLER") == "1":
|
if os.environ.get("PKGMGR_DISABLE_PYTHON_INSTALLER") == "1":
|
||||||
print("[INFO] PythonInstaller disabled via PKGMGR_DISABLE_PYTHON_INSTALLER.")
|
print("[INFO] PythonInstaller disabled via PKGMGR_DISABLE_PYTHON_INSTALLER.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return os.path.exists(os.path.join(ctx.repo_dir, "pyproject.toml"))
|
return os.path.exists(os.path.join(ctx.repo_dir, "pyproject.toml"))
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Virtualenv handling
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def _in_virtualenv(self) -> bool:
|
def _in_virtualenv(self) -> bool:
|
||||||
"""Detect whether the current interpreter is inside a venv."""
|
|
||||||
if os.environ.get("VIRTUAL_ENV"):
|
if os.environ.get("VIRTUAL_ENV"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
base = getattr(sys, "base_prefix", sys.prefix)
|
base = getattr(sys, "base_prefix", sys.prefix)
|
||||||
return sys.prefix != base
|
return sys.prefix != base
|
||||||
|
|
||||||
def _ensure_repo_venv(self, ctx: "InstallContext") -> str:
|
def _ensure_repo_venv(self, ctx: RepoContext) -> str:
|
||||||
"""
|
|
||||||
Ensure that ~/.venvs/<identifier>/ exists and contains a minimal venv.
|
|
||||||
|
|
||||||
Returns the venv directory path.
|
|
||||||
"""
|
|
||||||
venv_dir = os.path.expanduser(f"~/.venvs/{ctx.identifier}")
|
venv_dir = os.path.expanduser(f"~/.venvs/{ctx.identifier}")
|
||||||
python = sys.executable
|
python = sys.executable
|
||||||
|
|
||||||
if not os.path.isdir(venv_dir):
|
if not os.path.exists(venv_dir):
|
||||||
print(f"[python-installer] Creating virtualenv: {venv_dir}")
|
run_command(f"{python} -m venv {venv_dir}", preview=ctx.preview)
|
||||||
subprocess.check_call([python, "-m", "venv", venv_dir])
|
|
||||||
|
|
||||||
return venv_dir
|
return venv_dir
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
def _pip_cmd(self, ctx: RepoContext) -> str:
|
||||||
# pip command resolution
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def _pip_cmd(self, ctx: "InstallContext") -> str:
|
|
||||||
"""
|
|
||||||
Determine which pip command to use.
|
|
||||||
|
|
||||||
Priority:
|
|
||||||
1. PKGMGR_PIP override given by user or automation.
|
|
||||||
2. Active virtualenv → use sys.executable -m pip.
|
|
||||||
3. Per-repository venv → ~/.venvs/<repo>/bin/pip
|
|
||||||
"""
|
|
||||||
explicit = os.environ.get("PKGMGR_PIP", "").strip()
|
explicit = os.environ.get("PKGMGR_PIP", "").strip()
|
||||||
if explicit:
|
if explicit:
|
||||||
return explicit
|
return explicit
|
||||||
@@ -107,33 +43,19 @@ class PythonInstaller(BaseInstaller):
|
|||||||
return f"{sys.executable} -m pip"
|
return f"{sys.executable} -m pip"
|
||||||
|
|
||||||
venv_dir = self._ensure_repo_venv(ctx)
|
venv_dir = self._ensure_repo_venv(ctx)
|
||||||
pip_path = os.path.join(venv_dir, "bin", "pip")
|
return os.path.join(venv_dir, "bin", "pip")
|
||||||
return pip_path
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
def run(self, ctx: RepoContext) -> None:
|
||||||
# Execution
|
if not self.supports(ctx):
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def run(self, ctx: "InstallContext") -> None:
|
|
||||||
"""
|
|
||||||
Install the project defined by pyproject.toml.
|
|
||||||
|
|
||||||
Uses the resolved pip environment. Installation is isolated and never
|
|
||||||
touches system Python.
|
|
||||||
"""
|
|
||||||
if not self.supports(ctx): # type: ignore[arg-type]
|
|
||||||
return
|
|
||||||
|
|
||||||
pyproject = os.path.join(ctx.repo_dir, "pyproject.toml")
|
|
||||||
if not os.path.exists(pyproject):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print(f"[python-installer] Installing Python project for {ctx.identifier}...")
|
print(f"[python-installer] Installing Python project for {ctx.identifier}...")
|
||||||
|
|
||||||
pip_cmd = self._pip_cmd(ctx)
|
pip_cmd = self._pip_cmd(ctx)
|
||||||
|
run_command(f"{pip_cmd} install .", cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
|
||||||
# Final install command: ALWAYS isolated, never system-wide.
|
if ctx.force_update:
|
||||||
install_cmd = f"{pip_cmd} install ."
|
# test-visible marker
|
||||||
|
print(f"[python-installer] repo '{ctx.identifier}' successfully upgraded.")
|
||||||
run_command(install_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
|
||||||
|
|
||||||
print(f"[python-installer] Installation finished for {ctx.identifier}.")
|
print(f"[python-installer] Installation finished for {ctx.identifier}.")
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
|
# src/pkgmgr/actions/install/pipeline.py
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Installation pipeline orchestration for repositories.
|
Installation pipeline orchestration for repositories.
|
||||||
|
|
||||||
This module implements the "Setup Controller" logic:
|
|
||||||
|
|
||||||
1. Detect current CLI command for the repo (if any).
|
|
||||||
2. Classify it into a layer (os-packages, nix, python, makefile).
|
|
||||||
3. Iterate over installers in layer order:
|
|
||||||
- Skip installers whose layer is weaker than an already-loaded one.
|
|
||||||
- Run only installers that support() the repo and add new capabilities.
|
|
||||||
- After each installer, re-resolve the command and update the layer.
|
|
||||||
4. Maintain the repo["command"] field and create/update symlinks via create_ink().
|
|
||||||
|
|
||||||
The goal is to prevent conflicting installations and make the layering
|
|
||||||
behaviour explicit and testable.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -36,34 +24,15 @@ from pkgmgr.core.command.resolve import resolve_command_for_repo
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CommandState:
|
class CommandState:
|
||||||
"""
|
|
||||||
Represents the current CLI state for a repository:
|
|
||||||
|
|
||||||
- command: absolute or relative path to the CLI entry point
|
|
||||||
- layer: which conceptual layer this command belongs to
|
|
||||||
"""
|
|
||||||
|
|
||||||
command: Optional[str]
|
command: Optional[str]
|
||||||
layer: Optional[CliLayer]
|
layer: Optional[CliLayer]
|
||||||
|
|
||||||
|
|
||||||
class CommandResolver:
|
class CommandResolver:
|
||||||
"""
|
|
||||||
Small helper responsible for resolving the current command for a repo
|
|
||||||
and mapping it into a CommandState.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, ctx: RepoContext) -> None:
|
def __init__(self, ctx: RepoContext) -> None:
|
||||||
self._ctx = ctx
|
self._ctx = ctx
|
||||||
|
|
||||||
def resolve(self) -> CommandState:
|
def resolve(self) -> CommandState:
|
||||||
"""
|
|
||||||
Resolve the current command for this repository.
|
|
||||||
|
|
||||||
If resolve_command_for_repo raises SystemExit (e.g. Python package
|
|
||||||
without installed entry point), we treat this as "no command yet"
|
|
||||||
from the point of view of the installers.
|
|
||||||
"""
|
|
||||||
repo = self._ctx.repo
|
repo = self._ctx.repo
|
||||||
identifier = self._ctx.identifier
|
identifier = self._ctx.identifier
|
||||||
repo_dir = self._ctx.repo_dir
|
repo_dir = self._ctx.repo_dir
|
||||||
@@ -85,28 +54,10 @@ class CommandResolver:
|
|||||||
|
|
||||||
|
|
||||||
class InstallationPipeline:
|
class InstallationPipeline:
|
||||||
"""
|
|
||||||
High-level orchestrator that applies a sequence of installers
|
|
||||||
to a repository based on CLI layer precedence.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, installers: Sequence[BaseInstaller]) -> None:
|
def __init__(self, installers: Sequence[BaseInstaller]) -> None:
|
||||||
self._installers = list(installers)
|
self._installers = list(installers)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Public API
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
def run(self, ctx: RepoContext) -> None:
|
def run(self, ctx: RepoContext) -> None:
|
||||||
"""
|
|
||||||
Execute the installation pipeline for a single repository.
|
|
||||||
|
|
||||||
- Detect initial command & layer.
|
|
||||||
- Optionally create a symlink.
|
|
||||||
- Run installers in order, skipping those whose layer is weaker
|
|
||||||
than an already-loaded CLI.
|
|
||||||
- After each installer, re-resolve the command and refresh the
|
|
||||||
symlink if needed.
|
|
||||||
"""
|
|
||||||
repo = ctx.repo
|
repo = ctx.repo
|
||||||
repo_dir = ctx.repo_dir
|
repo_dir = ctx.repo_dir
|
||||||
identifier = ctx.identifier
|
identifier = ctx.identifier
|
||||||
@@ -119,7 +70,6 @@ class InstallationPipeline:
|
|||||||
resolver = CommandResolver(ctx)
|
resolver = CommandResolver(ctx)
|
||||||
state = resolver.resolve()
|
state = resolver.resolve()
|
||||||
|
|
||||||
# Persist initial command (if any) and create a symlink.
|
|
||||||
if state.command:
|
if state.command:
|
||||||
repo["command"] = state.command
|
repo["command"] = state.command
|
||||||
create_ink(
|
create_ink(
|
||||||
@@ -135,11 +85,9 @@ class InstallationPipeline:
|
|||||||
|
|
||||||
provided_capabilities: Set[str] = set()
|
provided_capabilities: Set[str] = set()
|
||||||
|
|
||||||
# Main installer loop
|
|
||||||
for installer in self._installers:
|
for installer in self._installers:
|
||||||
layer_name = getattr(installer, "layer", None)
|
layer_name = getattr(installer, "layer", None)
|
||||||
|
|
||||||
# Installers without a layer participate without precedence logic.
|
|
||||||
if layer_name is None:
|
if layer_name is None:
|
||||||
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
||||||
continue
|
continue
|
||||||
@@ -147,42 +95,33 @@ class InstallationPipeline:
|
|||||||
try:
|
try:
|
||||||
installer_layer = CliLayer(layer_name)
|
installer_layer = CliLayer(layer_name)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Unknown layer string → treat as lowest priority.
|
|
||||||
installer_layer = None
|
installer_layer = None
|
||||||
|
|
||||||
# "Previous/Current layer already loaded?"
|
|
||||||
if state.layer is not None and installer_layer is not None:
|
if state.layer is not None and installer_layer is not None:
|
||||||
current_prio = layer_priority(state.layer)
|
current_prio = layer_priority(state.layer)
|
||||||
installer_prio = layer_priority(installer_layer)
|
installer_prio = layer_priority(installer_layer)
|
||||||
|
|
||||||
if current_prio < installer_prio:
|
if current_prio < installer_prio:
|
||||||
# Current CLI comes from a higher-priority layer,
|
|
||||||
# so we skip this installer entirely.
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
print(
|
print(
|
||||||
f"[pkgmgr] Skipping installer "
|
"[pkgmgr] Skipping installer "
|
||||||
f"{installer.__class__.__name__} for {identifier} – "
|
f"{installer.__class__.__name__} for {identifier} – "
|
||||||
f"CLI already provided by layer {state.layer.value!r}."
|
f"CLI already provided by layer {state.layer.value!r}."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if current_prio == installer_prio:
|
if current_prio == installer_prio and not ctx.force_update:
|
||||||
# Same layer already provides a CLI; usually there is no
|
|
||||||
# need to run another installer on top of it.
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
print(
|
print(
|
||||||
f"[pkgmgr] Skipping installer "
|
"[pkgmgr] Skipping installer "
|
||||||
f"{installer.__class__.__name__} for {identifier} – "
|
f"{installer.__class__.__name__} for {identifier} – "
|
||||||
f"layer {installer_layer.value!r} is already loaded."
|
f"layer {installer_layer.value!r} is already loaded."
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if this installer is applicable at all.
|
|
||||||
if not installer.supports(ctx):
|
if not installer.supports(ctx):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Capabilities: if everything this installer would provide is already
|
|
||||||
# covered, we can safely skip it.
|
|
||||||
caps = installer.discover_capabilities(ctx)
|
caps = installer.discover_capabilities(ctx)
|
||||||
if caps and caps.issubset(provided_capabilities):
|
if caps and caps.issubset(provided_capabilities):
|
||||||
if not quiet:
|
if not quiet:
|
||||||
@@ -193,18 +132,22 @@ class InstallationPipeline:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not quiet:
|
if not quiet:
|
||||||
print(
|
if ctx.force_update and state.layer is not None and installer_layer == state.layer:
|
||||||
f"[pkgmgr] Running installer {installer.__class__.__name__} "
|
print(
|
||||||
f"for {identifier} in '{repo_dir}' "
|
f"[pkgmgr] Running installer {installer.__class__.__name__} "
|
||||||
f"(new capabilities: {caps or set()})..."
|
f"for {identifier} in '{repo_dir}' (upgrade requested)..."
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"[pkgmgr] Running installer {installer.__class__.__name__} "
|
||||||
|
f"for {identifier} in '{repo_dir}' "
|
||||||
|
f"(new capabilities: {caps or set()})..."
|
||||||
|
)
|
||||||
|
|
||||||
# Run the installer with error reporting.
|
|
||||||
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
self._run_installer(installer, ctx, identifier, repo_dir, quiet)
|
||||||
|
|
||||||
provided_capabilities.update(caps)
|
provided_capabilities.update(caps)
|
||||||
|
|
||||||
# After running an installer, re-resolve the command and layer.
|
|
||||||
new_state = resolver.resolve()
|
new_state = resolver.resolve()
|
||||||
if new_state.command:
|
if new_state.command:
|
||||||
repo["command"] = new_state.command
|
repo["command"] = new_state.command
|
||||||
@@ -221,9 +164,6 @@ class InstallationPipeline:
|
|||||||
|
|
||||||
state = new_state
|
state = new_state
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Internal helpers
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _run_installer(
|
def _run_installer(
|
||||||
installer: BaseInstaller,
|
installer: BaseInstaller,
|
||||||
@@ -232,9 +172,6 @@ class InstallationPipeline:
|
|||||||
repo_dir: str,
|
repo_dir: str,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Execute a single installer with unified error handling.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
installer.run(ctx)
|
installer.run(ctx)
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
High-level mirror actions.
|
High-level mirror actions.
|
||||||
|
|
||||||
@@ -10,6 +8,7 @@ Public API:
|
|||||||
- setup_mirrors
|
- setup_mirrors
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
from .types import Repository, MirrorMap
|
from .types import Repository, MirrorMap
|
||||||
from .list_cmd import list_mirrors
|
from .list_cmd import list_mirrors
|
||||||
from .diff_cmd import diff_mirrors
|
from .diff_cmd import diff_mirrors
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ def ensure_origin_remote(
|
|||||||
current = current_origin_url(repo_dir)
|
current = current_origin_url(repo_dir)
|
||||||
if current == url or not url:
|
if current == url or not url:
|
||||||
print(
|
print(
|
||||||
f"[INFO] 'origin' already points to "
|
"[INFO] 'origin' already points to "
|
||||||
f"{current or '<unknown>'} (no change needed)."
|
f"{current or '<unknown>'} (no change needed)."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from typing import List, Mapping
|
from typing import Mapping
|
||||||
|
|
||||||
from .types import MirrorMap, Repository
|
from .types import MirrorMap, Repository
|
||||||
|
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ def update_spec_version(
|
|||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
print(
|
print(
|
||||||
f"[PREVIEW] Would update spec file "
|
"[PREVIEW] Would update spec file "
|
||||||
f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..."
|
f"{os.path.basename(spec_path)} to Version: {new_version}, Release: 1..."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from pkgmgr.actions.branch import close_branch
|
from pkgmgr.actions.branch import close_branch
|
||||||
from pkgmgr.core.git import get_current_branch, GitError
|
from pkgmgr.core.git import get_current_branch, GitError
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
|
||||||
import yaml
|
import yaml
|
||||||
from pkgmgr.core.command.alias import generate_alias
|
from pkgmgr.core.command.alias import generate_alias
|
||||||
from pkgmgr.core.config.save import save_user_config
|
from pkgmgr.core.config.save import save_user_config
|
||||||
|
|||||||
@@ -1,15 +1,32 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
|
||||||
from pkgmgr.core.repository.dir import get_repo_dir
|
|
||||||
|
|
||||||
def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, preview=False):
|
from pkgmgr.core.command.run import run_command
|
||||||
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
|
|
||||||
|
|
||||||
|
def deinstall_repos(
|
||||||
|
selected_repos,
|
||||||
|
repositories_base_dir,
|
||||||
|
bin_dir,
|
||||||
|
all_repos,
|
||||||
|
preview: bool = False,
|
||||||
|
) -> None:
|
||||||
for repo in selected_repos:
|
for repo in selected_repos:
|
||||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||||
alias_path = os.path.join(bin_dir, repo_identifier)
|
|
||||||
|
|
||||||
|
# Resolve repository directory
|
||||||
|
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||||
|
|
||||||
|
# Prefer alias if available; fall back to identifier
|
||||||
|
alias_name = str(repo.get("alias") or repo_identifier)
|
||||||
|
alias_path = os.path.join(os.path.expanduser(bin_dir), alias_name)
|
||||||
|
|
||||||
|
# Remove alias link/file (interactive)
|
||||||
if os.path.exists(alias_path):
|
if os.path.exists(alias_path):
|
||||||
confirm = input(f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: ").strip().lower()
|
confirm = input(
|
||||||
|
f"Are you sure you want to delete link '{alias_path}' for {repo_identifier}? [y/N]: "
|
||||||
|
).strip().lower()
|
||||||
if confirm == "y":
|
if confirm == "y":
|
||||||
if preview:
|
if preview:
|
||||||
print(f"[Preview] Would remove link '{alias_path}'.")
|
print(f"[Preview] Would remove link '{alias_path}'.")
|
||||||
@@ -19,10 +36,13 @@ def deinstall_repos(selected_repos, repositories_base_dir, bin_dir, all_repos, p
|
|||||||
else:
|
else:
|
||||||
print(f"No link found for {repo_identifier} in {bin_dir}.")
|
print(f"No link found for {repo_identifier} in {bin_dir}.")
|
||||||
|
|
||||||
|
# Run make deinstall if repository exists and has a Makefile
|
||||||
makefile_path = os.path.join(repo_dir, "Makefile")
|
makefile_path = os.path.join(repo_dir, "Makefile")
|
||||||
if os.path.exists(makefile_path):
|
if os.path.exists(makefile_path):
|
||||||
print(f"Makefile found in {repo_identifier}, running 'make deinstall'...")
|
print(f"Makefile found in {repo_identifier}, running 'make deinstall'...")
|
||||||
try:
|
try:
|
||||||
run_command("make deinstall", cwd=repo_dir, preview=preview)
|
run_command("make deinstall", cwd=repo_dir, preview=preview)
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
print(f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}")
|
print(
|
||||||
|
f"[Warning] Failed to run 'make deinstall' for {repo_identifier}: {e}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ def list_repositories(
|
|||||||
f"{'STATUS'.ljust(status_width)} "
|
f"{'STATUS'.ljust(status_width)} "
|
||||||
f"{'CATEGORIES'.ljust(cat_width)} "
|
f"{'CATEGORIES'.ljust(cat_width)} "
|
||||||
f"{'TAGS'.ljust(tag_width)} "
|
f"{'TAGS'.ljust(tag_width)} "
|
||||||
f"DIR"
|
"DIR"
|
||||||
f"{RESET}"
|
f"{RESET}"
|
||||||
)
|
)
|
||||||
print(header)
|
print(header)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
|
||||||
from pkgmgr.core.repository.dir import get_repo_dir
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
from pkgmgr.core.repository.verify import verify_repository
|
from pkgmgr.core.repository.verify import verify_repository
|
||||||
|
|
||||||
|
|
||||||
@@ -17,13 +20,6 @@ def pull_with_verification(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Execute `git pull` for each repository with verification.
|
Execute `git pull` for each repository with verification.
|
||||||
|
|
||||||
- Uses verify_repository() 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.
|
|
||||||
- In preview mode, no interactive prompts are performed and no
|
|
||||||
Git commands are executed; only the would-be command is printed.
|
|
||||||
"""
|
"""
|
||||||
for repo in selected_repos:
|
for repo in selected_repos:
|
||||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
repo_identifier = get_repo_identifier(repo, all_repos)
|
||||||
@@ -34,18 +30,13 @@ def pull_with_verification(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
verified_info = repo.get("verified")
|
verified_info = repo.get("verified")
|
||||||
verified_ok, errors, commit_hash, signing_key = verify_repository(
|
verified_ok, errors, _commit_hash, _signing_key = verify_repository(
|
||||||
repo,
|
repo,
|
||||||
repo_dir,
|
repo_dir,
|
||||||
mode="pull",
|
mode="pull",
|
||||||
no_verification=no_verification,
|
no_verification=no_verification,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only prompt the user if:
|
|
||||||
# - we are NOT in preview mode
|
|
||||||
# - verification is enabled
|
|
||||||
# - the repo has verification info configured
|
|
||||||
# - verification failed
|
|
||||||
if (
|
if (
|
||||||
not preview
|
not preview
|
||||||
and not no_verification
|
and not no_verification
|
||||||
@@ -59,16 +50,14 @@ def pull_with_verification(
|
|||||||
if choice != "y":
|
if choice != "y":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build the git pull command (include extra args if present)
|
|
||||||
args_part = " ".join(extra_args) if extra_args else ""
|
args_part = " ".join(extra_args) if extra_args else ""
|
||||||
full_cmd = f"git pull{(' ' + args_part) if args_part else ''}"
|
full_cmd = f"git pull{(' ' + args_part) if args_part else ''}"
|
||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
# Preview mode: only show the command, do not execute or prompt.
|
|
||||||
print(f"[Preview] In '{repo_dir}': {full_cmd}")
|
print(f"[Preview] In '{repo_dir}': {full_cmd}")
|
||||||
else:
|
else:
|
||||||
print(f"Running in '{repo_dir}': {full_cmd}")
|
print(f"Running in '{repo_dir}': {full_cmd}")
|
||||||
result = subprocess.run(full_cmd, cwd=repo_dir, shell=True)
|
result = subprocess.run(full_cmd, cwd=repo_dir, shell=True, check=False)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print(
|
print(
|
||||||
f"'git pull' for {repo_identifier} failed "
|
f"'git pull' for {repo_identifier} failed "
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import sys
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pkgmgr.actions.proxy import exec_proxy_command
|
from pkgmgr.actions.proxy import exec_proxy_command
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import sys
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pkgmgr.actions.repository.pull import pull_with_verification
|
|
||||||
from pkgmgr.actions.install import install_repos
|
from pkgmgr.actions.install import install_repos
|
||||||
|
from pkgmgr.actions.repository.pull import pull_with_verification
|
||||||
|
|
||||||
|
|
||||||
def update_repos(
|
def update_repos(
|
||||||
@@ -16,21 +18,10 @@ def update_repos(
|
|||||||
quiet: bool,
|
quiet: bool,
|
||||||
update_dependencies: bool,
|
update_dependencies: bool,
|
||||||
clone_mode: str,
|
clone_mode: str,
|
||||||
):
|
force_update: bool = True,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Update repositories by pulling latest changes and installing them.
|
Update repositories by pulling latest changes and installing them.
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- selected_repos: List of selected repositories.
|
|
||||||
- repositories_base_dir: Base directory for repositories.
|
|
||||||
- bin_dir: Directory for symbolic links.
|
|
||||||
- all_repos: All repository configurations.
|
|
||||||
- no_verification: Whether to skip verification.
|
|
||||||
- system_update: Whether to run system update.
|
|
||||||
- preview: If True, only show commands without executing.
|
|
||||||
- quiet: If True, suppress messages.
|
|
||||||
- update_dependencies: Whether to update dependent repositories.
|
|
||||||
- clone_mode: Method to clone repositories (ssh or https).
|
|
||||||
"""
|
"""
|
||||||
pull_with_verification(
|
pull_with_verification(
|
||||||
selected_repos,
|
selected_repos,
|
||||||
@@ -51,18 +42,17 @@ def update_repos(
|
|||||||
quiet,
|
quiet,
|
||||||
clone_mode,
|
clone_mode,
|
||||||
update_dependencies,
|
update_dependencies,
|
||||||
|
force_update=force_update,
|
||||||
)
|
)
|
||||||
|
|
||||||
if system_update:
|
if system_update:
|
||||||
from pkgmgr.core.command.run import run_command
|
from pkgmgr.core.command.run import run_command
|
||||||
|
|
||||||
# Nix: upgrade all profile entries (if Nix is available)
|
|
||||||
if shutil.which("nix") is not None:
|
if shutil.which("nix") is not None:
|
||||||
try:
|
try:
|
||||||
run_command("nix profile upgrade '.*'", preview=preview)
|
run_command("nix profile upgrade '.*'", preview=preview)
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
print(f"[Warning] 'nix profile upgrade' failed: {e}")
|
print(f"[Warning] 'nix profile upgrade' failed: {e}")
|
||||||
|
|
||||||
# Arch / AUR system update
|
|
||||||
run_command("sudo -u aur_builder yay -Syu --noconfirm", preview=preview)
|
run_command("sudo -u aur_builder yay -Syu --noconfirm", preview=preview)
|
||||||
run_command("sudo pacman -Syyu --noconfirm", preview=preview)
|
run_command("sudo pacman -Syyu --noconfirm", preview=preview)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -9,7 +8,7 @@ from pkgmgr.cli.context import CLIContext
|
|||||||
from pkgmgr.core.repository.dir import get_repo_dir
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
from pkgmgr.core.repository.identifier import get_repo_identifier
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
from pkgmgr.core.git import get_tags
|
from pkgmgr.core.git import get_tags
|
||||||
from pkgmgr.core.version.semver import SemVer, extract_semver_from_tags
|
from pkgmgr.core.version.semver import extract_semver_from_tags
|
||||||
from pkgmgr.actions.changelog import generate_changelog
|
from pkgmgr.actions.changelog import generate_changelog
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ from typing import Any, Dict, List
|
|||||||
|
|
||||||
from pkgmgr.cli.context import CLIContext
|
from pkgmgr.cli.context import CLIContext
|
||||||
from pkgmgr.actions.install import install_repos
|
from pkgmgr.actions.install import install_repos
|
||||||
|
from pkgmgr.actions.repository.update import update_repos
|
||||||
from pkgmgr.actions.repository.deinstall import deinstall_repos
|
from pkgmgr.actions.repository.deinstall import deinstall_repos
|
||||||
from pkgmgr.actions.repository.delete import delete_repos
|
from pkgmgr.actions.repository.delete import delete_repos
|
||||||
from pkgmgr.actions.repository.update import update_repos
|
|
||||||
from pkgmgr.actions.repository.status import status_repos
|
from pkgmgr.actions.repository.status import status_repos
|
||||||
from pkgmgr.actions.repository.list import list_repositories
|
from pkgmgr.actions.repository.list import list_repositories
|
||||||
from pkgmgr.core.command.run import run_command
|
|
||||||
from pkgmgr.actions.repository.create import create_repo
|
from pkgmgr.actions.repository.create import create_repo
|
||||||
from pkgmgr.core.repository.selected import get_selected_repos
|
from pkgmgr.core.command.run import run_command
|
||||||
from pkgmgr.core.repository.dir import get_repo_dir
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
|
|
||||||
Repository = Dict[str, Any]
|
Repository = Dict[str, Any]
|
||||||
@@ -52,7 +51,7 @@ def handle_repos_command(
|
|||||||
selected: List[Repository],
|
selected: List[Repository],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Handle core repository commands (install/update/deinstall/delete/.../list).
|
Handle core repository commands (install/update/deinstall/delete/status/list/path/shell/create).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
@@ -69,6 +68,7 @@ def handle_repos_command(
|
|||||||
args.quiet,
|
args.quiet,
|
||||||
args.clone_mode,
|
args.clone_mode,
|
||||||
args.dependencies,
|
args.dependencies,
|
||||||
|
force_update=getattr(args, "update", False),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -82,11 +82,12 @@ def handle_repos_command(
|
|||||||
ctx.binaries_dir,
|
ctx.binaries_dir,
|
||||||
ctx.all_repositories,
|
ctx.all_repositories,
|
||||||
args.no_verification,
|
args.no_verification,
|
||||||
args.system,
|
args.system_update,
|
||||||
args.preview,
|
args.preview,
|
||||||
args.quiet,
|
args.quiet,
|
||||||
args.dependencies,
|
args.dependencies,
|
||||||
args.clone_mode,
|
args.clone_mode,
|
||||||
|
force_update=True,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -147,9 +148,7 @@ def handle_repos_command(
|
|||||||
f"{repository.get('account', '?')}/"
|
f"{repository.get('account', '?')}/"
|
||||||
f"{repository.get('repository', '?')}"
|
f"{repository.get('repository', '?')}"
|
||||||
)
|
)
|
||||||
print(
|
print(f"[WARN] Could not resolve directory for {ident}: {exc}")
|
||||||
f"[WARN] Could not resolve directory for {ident}: {exc}"
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print(repo_dir)
|
print(repo_dir)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
from .common import add_install_update_arguments, add_identifier_arguments
|
from pkgmgr.cli.parser.common import (
|
||||||
|
add_install_update_arguments,
|
||||||
|
add_identifier_arguments,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_install_update_subparsers(
|
def add_install_update_subparsers(
|
||||||
@@ -14,11 +15,17 @@ def add_install_update_subparsers(
|
|||||||
"""
|
"""
|
||||||
Register install / update / deinstall / delete commands.
|
Register install / update / deinstall / delete commands.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
install_parser = subparsers.add_parser(
|
install_parser = subparsers.add_parser(
|
||||||
"install",
|
"install",
|
||||||
help="Setup repository/repositories alias links to executables",
|
help="Setup repository/repositories alias links to executables",
|
||||||
)
|
)
|
||||||
add_install_update_arguments(install_parser)
|
add_install_update_arguments(install_parser)
|
||||||
|
install_parser.add_argument(
|
||||||
|
"--update",
|
||||||
|
action="store_true",
|
||||||
|
help="Force re-run installers (upgrade/refresh) even if the CLI layer is already loaded",
|
||||||
|
)
|
||||||
|
|
||||||
update_parser = subparsers.add_parser(
|
update_parser = subparsers.add_parser(
|
||||||
"update",
|
"update",
|
||||||
@@ -30,6 +37,7 @@ def add_install_update_subparsers(
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Include system update commands",
|
help="Include system update commands",
|
||||||
)
|
)
|
||||||
|
# KEIN --update hier nötig → update impliziert force_update=True
|
||||||
|
|
||||||
deinstall_parser = subparsers.add_parser(
|
deinstall_parser = subparsers.add_parser(
|
||||||
"deinstall",
|
"deinstall",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ PROXY_COMMANDS: Dict[str, List[str]] = {
|
|||||||
"reset",
|
"reset",
|
||||||
"revert",
|
"revert",
|
||||||
"rebase",
|
"rebase",
|
||||||
|
"status",
|
||||||
"commit",
|
"commit",
|
||||||
],
|
],
|
||||||
"docker": [
|
"docker": [
|
||||||
|
|||||||
30
src/pkgmgr/core/command/layer.py
Normal file
30
src/pkgmgr/core/command/layer.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# src/pkgmgr/core/command/layer.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class CliLayer(str, Enum):
|
||||||
|
"""
|
||||||
|
CLI layer precedence (lower number = stronger layer).
|
||||||
|
"""
|
||||||
|
OS_PACKAGES = "os-packages"
|
||||||
|
NIX = "nix"
|
||||||
|
PYTHON = "python"
|
||||||
|
MAKEFILE = "makefile"
|
||||||
|
|
||||||
|
|
||||||
|
_LAYER_PRIORITY: dict[CliLayer, int] = {
|
||||||
|
CliLayer.OS_PACKAGES: 0,
|
||||||
|
CliLayer.NIX: 1,
|
||||||
|
CliLayer.PYTHON: 2,
|
||||||
|
CliLayer.MAKEFILE: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def layer_priority(layer: CliLayer) -> int:
|
||||||
|
"""
|
||||||
|
Return precedence priority for the given layer.
|
||||||
|
Lower value means higher priority (stronger layer).
|
||||||
|
"""
|
||||||
|
return _LAYER_PRIORITY.get(layer, 999)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
from typing import Optional
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Optional, List, Dict, Any
|
from typing import Optional, List, Dict, Any
|
||||||
@@ -201,8 +200,8 @@ def resolve_command_for_repo(
|
|||||||
print(
|
print(
|
||||||
f"[INFO] Repository '{repo_identifier}' appears to be a Python "
|
f"[INFO] Repository '{repo_identifier}' appears to be a Python "
|
||||||
f"package at '{python_package_root}' but no CLI entry point was "
|
f"package at '{python_package_root}' but no CLI entry point was "
|
||||||
f"found (PATH, Nix, main.sh/main.py). Treating it as a "
|
"found (PATH, Nix, main.sh/main.py). Treating it as a "
|
||||||
f"library-only repository with no command."
|
"library-only repository with no command."
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from typing import Optional
|
from __future__ import annotations
|
||||||
# pkgmgr/run_command.py
|
|
||||||
|
import selectors
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Optional, Union
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
CommandType = Union[str, List[str]]
|
CommandType = Union[str, List[str]]
|
||||||
|
|
||||||
|
|
||||||
@@ -15,32 +15,97 @@ def run_command(
|
|||||||
allow_failure: bool = False,
|
allow_failure: bool = False,
|
||||||
) -> subprocess.CompletedProcess:
|
) -> subprocess.CompletedProcess:
|
||||||
"""
|
"""
|
||||||
Run a command and optionally exit on error.
|
Run a command with live output while capturing stdout/stderr.
|
||||||
|
|
||||||
- If `cmd` is a string, it is executed with `shell=True`.
|
- Output is streamed live to the terminal.
|
||||||
- If `cmd` is a list of strings, it is executed without a shell.
|
- Output is captured in memory.
|
||||||
|
- On failure, captured stdout/stderr are printed again so errors are never lost.
|
||||||
|
- Command is executed exactly once.
|
||||||
"""
|
"""
|
||||||
if isinstance(cmd, str):
|
display = cmd if isinstance(cmd, str) else " ".join(cmd)
|
||||||
display = cmd
|
|
||||||
else:
|
|
||||||
display = " ".join(cmd)
|
|
||||||
|
|
||||||
where = cwd or "."
|
where = cwd or "."
|
||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
print(f"[Preview] In '{where}': {display}")
|
print(f"[Preview] In '{where}': {display}")
|
||||||
# Fake a successful result; most callers ignore the return value anyway
|
|
||||||
return subprocess.CompletedProcess(cmd, 0) # type: ignore[arg-type]
|
return subprocess.CompletedProcess(cmd, 0) # type: ignore[arg-type]
|
||||||
|
|
||||||
print(f"Running in '{where}': {display}")
|
print(f"Running in '{where}': {display}")
|
||||||
|
|
||||||
if isinstance(cmd, str):
|
process = subprocess.Popen(
|
||||||
result = subprocess.run(cmd, cwd=cwd, shell=True)
|
cmd,
|
||||||
else:
|
cwd=cwd,
|
||||||
result = subprocess.run(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:
|
assert process.stdout is not None
|
||||||
print(f"Command failed with exit code {result.returncode}. Exiting.")
|
assert process.stderr is not None
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
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 os
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
def get_repo_dir(repositories_base_dir:str,repo:{})->str:
|
|
||||||
try:
|
def get_repo_dir(repositories_base_dir: str, repo: Dict[str, Any]) -> str:
|
||||||
return os.path.join(repositories_base_dir, repo.get("provider"), repo.get("account"), repo.get("repository"))
|
"""
|
||||||
except TypeError as e:
|
Build the local repository directory path from:
|
||||||
if repositories_base_dir:
|
repositories_base_dir/provider/account/repository
|
||||||
print(f"Error: {e} \nThe repository {repo} seems not correct configured.\nPlease configure it correct.")
|
|
||||||
for key in ["provider","account","repository"]:
|
Exits with code 3 and prints diagnostics if the input config is invalid.
|
||||||
if not repo.get(key,False):
|
"""
|
||||||
print(f"Key '{key}' is missing.")
|
# Base dir must be set and non-empty
|
||||||
else:
|
if not repositories_base_dir:
|
||||||
print(f"Error: {e} \nThe base {base} seems not correct configured.\nPlease configure it correct.")
|
print(
|
||||||
|
"Error: repositories_base_dir is missing.\n"
|
||||||
|
"The base directory for repositories seems not correctly configured.\n"
|
||||||
|
"Please configure it correctly."
|
||||||
|
)
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
|
|
||||||
|
# Repo must be a dict-like object
|
||||||
|
if not isinstance(repo, dict):
|
||||||
|
print(
|
||||||
|
f"Error: invalid repo object '{repo}'.\n"
|
||||||
|
"The repository entry seems not correctly configured.\n"
|
||||||
|
"Please configure it correctly."
|
||||||
|
)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
base_dir = os.path.expanduser(str(repositories_base_dir))
|
||||||
|
|
||||||
|
provider = repo.get("provider")
|
||||||
|
account = repo.get("account")
|
||||||
|
repository = repo.get("repository")
|
||||||
|
|
||||||
|
missing = [k for k, v in [("provider", provider), ("account", account), ("repository", repository)] if not v]
|
||||||
|
if missing:
|
||||||
|
print(
|
||||||
|
"Error: repository entry is missing required keys.\n"
|
||||||
|
f"Repository: {repo}\n"
|
||||||
|
"Please configure it correctly."
|
||||||
|
)
|
||||||
|
for k in missing:
|
||||||
|
print(f"Key '{k}' is missing.")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
return os.path.join(base_dir, str(provider), str(account), str(repository))
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
def resolve_repos(identifiers:[], all_repos:[]):
|
def resolve_repos(identifiers:[], all_repos:[]):
|
||||||
"""
|
"""
|
||||||
|
|||||||
24
tests/e2e/_util.py
Normal file
24
tests/e2e/_util.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd, *, cwd=None, env=None, shell=False) -> str:
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
env=env,
|
||||||
|
shell=shell,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("----- BEGIN COMMAND -----")
|
||||||
|
print(cmd if isinstance(cmd, str) else " ".join(cmd))
|
||||||
|
print("----- OUTPUT -----")
|
||||||
|
print(proc.stdout.rstrip())
|
||||||
|
print("----- END COMMAND -----")
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise AssertionError(proc.stdout)
|
||||||
|
|
||||||
|
return proc.stdout
|
||||||
@@ -27,7 +27,7 @@ class TestIntegrationBranchCommands(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
# argv[0] is the program name; the rest are CLI arguments.
|
# argv[0] is the program name; the rest are CLI arguments.
|
||||||
sys.argv = ["pkgmgr"] + list(extra_args)
|
sys.argv = ["pkgmgr"] + list(extra_args)
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
finally:
|
finally:
|
||||||
sys.argv = original_argv
|
sys.argv = original_argv
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ def _run_pkgmgr_help(argv_tail: list[str]) -> str:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with redirect_stdout(buffer), redirect_stderr(buffer):
|
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:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else None
|
code = exc.code if isinstance(exc.code, int) else None
|
||||||
if code not in (0, None):
|
if code not in (0, None):
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class TestIntegrationChangelogCommands(unittest.TestCase):
|
|||||||
sys.argv = ["pkgmgr", "changelog"] + list(extra_args)
|
sys.argv = ["pkgmgr", "changelog"] + list(extra_args)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class TestIntegrationCloneAllHttps(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
# Execute main.py as if it was called from CLI.
|
# Execute main.py as if it was called from CLI.
|
||||||
# This will run the full clone pipeline inside the container.
|
# 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:
|
except SystemExit as exc:
|
||||||
# Determine the exit code (int or string)
|
# Determine the exit code (int or string)
|
||||||
exit_code = exc.code
|
exit_code = exc.code
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ def _run_pkgmgr_config(extra_args: list[str]) -> None:
|
|||||||
sys.argv = ["pkgmgr"] + extra_args
|
sys.argv = ["pkgmgr"] + extra_args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
25
tests/e2e/test_install_makefile_three_times.py
Normal file
25
tests/e2e/test_install_makefile_three_times.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from tests.e2e._util import run
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class TestMakefileThreeTimes(unittest.TestCase):
|
||||||
|
def test_make_install_three_times(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="makefile-3x-") as tmp:
|
||||||
|
repo = Path(tmp)
|
||||||
|
|
||||||
|
# Minimal Makefile with install target
|
||||||
|
(repo / "Makefile").write_text(
|
||||||
|
"install:\n\t@echo install >> install.log\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(1, 4):
|
||||||
|
print(f"\n=== RUN {i}/3 ===")
|
||||||
|
run(["make", "install"], cwd=repo)
|
||||||
|
|
||||||
|
log = (repo / "install.log").read_text().splitlines()
|
||||||
|
self.assertEqual(
|
||||||
|
len(log),
|
||||||
|
3,
|
||||||
|
"make install should have been executed exactly three times",
|
||||||
|
)
|
||||||
@@ -139,7 +139,7 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Execute installation via main.py
|
# Execute installation via main.py
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
|
|
||||||
# Debug: interactive shell test
|
# Debug: interactive shell test
|
||||||
pkgmgr_help_debug()
|
pkgmgr_help_debug()
|
||||||
|
|||||||
37
tests/e2e/test_install_pkgmgr_three_times_nix.py
Normal file
37
tests/e2e/test_install_pkgmgr_three_times_nix.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import os
|
||||||
|
from tests.e2e._util import run
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class TestPkgmgrInstallThreeTimesNix(unittest.TestCase):
|
||||||
|
def test_three_times_install_nix(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="pkgmgr-nix-") as tmp:
|
||||||
|
tmp_path = Path(tmp)
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HOME"] = tmp
|
||||||
|
|
||||||
|
# Ensure nix is found
|
||||||
|
env["PATH"] = "/nix/var/nix/profiles/default/bin:" + os.environ.get("PATH", "")
|
||||||
|
|
||||||
|
# IMPORTANT:
|
||||||
|
# nix run uses git+file:///src internally -> Git will reject /src if it's not a safe.directory.
|
||||||
|
# Our test sets HOME to a temp dir, so we must provide a temp global gitconfig.
|
||||||
|
gitconfig = tmp_path / ".gitconfig"
|
||||||
|
gitconfig.write_text(
|
||||||
|
"[safe]\n"
|
||||||
|
"\tdirectory = /src\n"
|
||||||
|
"\tdirectory = /src/.git\n"
|
||||||
|
"\tdirectory = *\n"
|
||||||
|
)
|
||||||
|
env["GIT_CONFIG_GLOBAL"] = str(gitconfig)
|
||||||
|
|
||||||
|
for i in range(1, 4):
|
||||||
|
print(f"\n=== RUN {i}/3 ===")
|
||||||
|
run(
|
||||||
|
"nix run .#pkgmgr -- install pkgmgr --update --clone-mode shallow --no-verification",
|
||||||
|
env=env,
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
34
tests/e2e/test_install_pkgmgr_three_times_venv.py
Normal file
34
tests/e2e/test_install_pkgmgr_three_times_venv.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from tests.e2e._util import run
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class TestPkgmgrInstallThreeTimesVenv(unittest.TestCase):
|
||||||
|
def test_three_times_install_venv(self):
|
||||||
|
with tempfile.TemporaryDirectory(prefix="pkgmgr-venv-") as tmp:
|
||||||
|
home = Path(tmp)
|
||||||
|
bin_dir = home / ".local" / "bin"
|
||||||
|
bin_dir.mkdir(parents=True)
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HOME"] = tmp
|
||||||
|
|
||||||
|
# pkgmgr kommt aus dem Projekt-venv
|
||||||
|
env["PATH"] = (
|
||||||
|
f"{Path.cwd() / '.venv' / 'bin'}:"
|
||||||
|
f"{bin_dir}:"
|
||||||
|
+ os.environ.get("PATH", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# nix explizit deaktivieren → Python/Venv-Pfad
|
||||||
|
env["PKGMGR_DISABLE_NIX_FLAKE_INSTALLER"] = "1"
|
||||||
|
|
||||||
|
for i in range(1, 4):
|
||||||
|
print(f"\n=== RUN {i}/3 ===")
|
||||||
|
run(
|
||||||
|
"pkgmgr install pkgmgr --update --clone-mode shallow --no-verification",
|
||||||
|
env=env,
|
||||||
|
shell=True,
|
||||||
|
)
|
||||||
@@ -27,7 +27,7 @@ class TestIntegrationListCommands(unittest.TestCase):
|
|||||||
sys.argv = ["pkgmgr"] + args
|
sys.argv = ["pkgmgr"] + args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TestIntegrationMakeCommands(unittest.TestCase):
|
|||||||
sys.argv = ["pkgmgr"] + extra_args
|
sys.argv = ["pkgmgr"] + extra_args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
with redirect_stdout(buffer), redirect_stderr(buffer):
|
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:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else None
|
code = exc.code if isinstance(exc.code, int) else None
|
||||||
if code not in (0, None):
|
if code not in (0, None):
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class TestPathCommandsE2E(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
# Capture stdout while running the CLI entry point.
|
# Capture stdout while running the CLI entry point.
|
||||||
with redirect_stdout(buffer):
|
with redirect_stdout(buffer):
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
# Determine the exit code (int or string)
|
# Determine the exit code (int or string)
|
||||||
exit_code = exc.code
|
exit_code = exc.code
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class TestIntegrationProxyCommands(unittest.TestCase):
|
|||||||
sys.argv = ["pkgmgr"] + args
|
sys.argv = ["pkgmgr"] + args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TestIntegrationReleaseCommand(unittest.TestCase):
|
|||||||
try:
|
try:
|
||||||
# argv[0] is the program name; the rest are CLI arguments.
|
# argv[0] is the program name; the rest are CLI arguments.
|
||||||
sys.argv = ["pkgmgr"] + list(extra_args)
|
sys.argv = ["pkgmgr"] + list(extra_args)
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
finally:
|
finally:
|
||||||
sys.argv = original_argv
|
sys.argv = original_argv
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ class TestIntegrationReleaseCommand(unittest.TestCase):
|
|||||||
# argparse will call sys.exit(), so we expect a SystemExit here.
|
# argparse will call sys.exit(), so we expect a SystemExit here.
|
||||||
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
|
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
|
||||||
with self.assertRaises(SystemExit) as cm:
|
with self.assertRaises(SystemExit) as cm:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
finally:
|
finally:
|
||||||
sys.argv = original_argv
|
sys.argv = original_argv
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class TestIntegrationToolsCommands(unittest.TestCase):
|
|||||||
sys.argv = ["pkgmgr"] + extra_args
|
sys.argv = ["pkgmgr"] + extra_args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
@@ -12,20 +12,11 @@ which we treat as success and suppress in the helper.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
|
||||||
import runpy
|
import runpy
|
||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
from typing import List
|
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:
|
def _run_main(argv: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Helper to run main.py with the given argv.
|
Helper to run main.py with the given argv.
|
||||||
@@ -40,7 +31,7 @@ def _run_main(argv: List[str]) -> None:
|
|||||||
try:
|
try:
|
||||||
sys.argv = ["pkgmgr"] + argv
|
sys.argv = ["pkgmgr"] + argv
|
||||||
try:
|
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
|
except SystemExit as exc: # argparse uses this for --help
|
||||||
# SystemExit.code can be int, str or None; for our purposes:
|
# SystemExit.code can be int, str or None; for our purposes:
|
||||||
code = exc.code
|
code = exc.code
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class TestIntegrationVersionCommands(unittest.TestCase):
|
|||||||
sys.argv = ["pkgmgr", "version"] + extra_args
|
sys.argv = ["pkgmgr", "version"] + extra_args
|
||||||
|
|
||||||
try:
|
try:
|
||||||
runpy.run_module("main", run_name="__main__")
|
runpy.run_module("pkgmgr", run_name="__main__")
|
||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
code = exc.code if isinstance(exc.code, int) else str(exc.code)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pkgmgr.actions.branch.utils import _resolve_base_branch
|
from pkgmgr.actions.branch.utils import _resolve_base_branch
|
||||||
from pkgmgr.core.git import GitError
|
from pkgmgr.core.git import GitError
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pkgmgr.actions.branch.close_branch import close_branch
|
from pkgmgr.actions.branch.close_branch import close_branch
|
||||||
from pkgmgr.core.git import GitError
|
from pkgmgr.core.git import GitError
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pkgmgr.actions.branch.drop_branch import drop_branch
|
from pkgmgr.actions.branch.drop_branch import drop_branch
|
||||||
from pkgmgr.core.git import GitError
|
from pkgmgr.core.git import GitError
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch
|
||||||
|
|
||||||
from pkgmgr.actions.branch.open_branch import open_branch
|
from pkgmgr.actions.branch.open_branch import open_branch
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# tests/unit/pkgmgr/test_capabilities.py
|
# tests/unit/pkgmgr/test_capabilities.py
|
||||||
|
|
||||||
import os
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch, mock_open
|
from unittest.mock import patch, mock_open
|
||||||
|
|
||||||
|
|||||||
0
tests/unit/pkgmgr/actions/repository/__init__.py
Normal file
0
tests/unit/pkgmgr/actions/repository/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user