Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcf9d4b59b | ||
|
|
b483dbfaad | ||
|
|
9630917570 | ||
|
|
6a4432dd04 | ||
|
|
cfb91d825a | ||
|
|
a3b21f23fc | ||
|
|
e49dd85200 | ||
|
|
c9dec5ecd6 | ||
|
|
f3c5460e48 | ||
|
|
39b16b87a8 | ||
|
|
26c9d79814 | ||
|
|
2776d18a42 | ||
|
|
7057ccfb95 | ||
|
|
1807949c6f | ||
|
|
d611720b8f | ||
|
|
bf871650a8 | ||
|
|
5ca1adda7b | ||
|
|
acb18adf76 | ||
|
|
c18490f5d3 | ||
|
|
eeda944b73 | ||
|
|
52cfbebba4 | ||
|
|
f4385807f1 | ||
|
|
e9e083c9dd | ||
|
|
3218b2b39f | ||
|
|
ba296a79c9 | ||
|
|
62e05e2f5b | ||
|
|
77d8b68ba5 | ||
|
|
bb0a801396 | ||
|
|
ee968efc4b | ||
|
|
644b2b8fa0 | ||
|
|
0f74907f82 | ||
|
|
5a8b1b11de | ||
|
|
389ec40163 | ||
|
|
1d03055491 | ||
|
|
7775c6d974 | ||
|
|
a24a819511 |
@@ -25,7 +25,5 @@ venv/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Arch pkg artifacts
|
# Logs
|
||||||
*.pkg.tar.*
|
|
||||||
*.log
|
*.log
|
||||||
package-manager-*
|
|
||||||
26
.github/workflows/ci.yml
vendored
Normal file
26
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-unit:
|
||||||
|
uses: ./.github/workflows/test-unit.yml
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
uses: ./.github/workflows/test-integration.yml
|
||||||
|
|
||||||
|
test-container:
|
||||||
|
uses: ./.github/workflows/test-container.yml
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
uses: ./.github/workflows/test-e2e.yml
|
||||||
|
|
||||||
|
test-virgin-user:
|
||||||
|
uses: ./.github/workflows/test-virgin-user.yml
|
||||||
|
|
||||||
|
test-virgin-root:
|
||||||
|
uses: ./.github/workflows/test-virgin-root.yml
|
||||||
64
.github/workflows/mark-stable.yml
vendored
Normal file
64
.github/workflows/mark-stable.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
name: Mark stable commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-unit:
|
||||||
|
uses: ./.github/workflows/test-unit.yml
|
||||||
|
|
||||||
|
test-integration:
|
||||||
|
uses: ./.github/workflows/test-integration.yml
|
||||||
|
|
||||||
|
test-container:
|
||||||
|
uses: ./.github/workflows/test-container.yml
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
uses: ./.github/workflows/test-e2e.yml
|
||||||
|
|
||||||
|
test-virgin-user:
|
||||||
|
uses: ./.github/workflows/test-virgin-user.yml
|
||||||
|
|
||||||
|
test-virgin-root:
|
||||||
|
uses: ./.github/workflows/test-virgin-root.yml
|
||||||
|
|
||||||
|
mark-stable:
|
||||||
|
needs:
|
||||||
|
- test-unit
|
||||||
|
- test-integration
|
||||||
|
- test-container
|
||||||
|
- test-e2e
|
||||||
|
- test-virgin-user
|
||||||
|
- test-virgin-root
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # to move the tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Move 'stable' tag to this commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
echo "Tagging commit $GITHUB_SHA as stable…"
|
||||||
|
|
||||||
|
# delete local tag if exists
|
||||||
|
git tag -d stable 2>/dev/null || true
|
||||||
|
# delete remote tag if exists
|
||||||
|
git push origin :refs/tags/stable || true
|
||||||
|
|
||||||
|
# create new tag on this commit
|
||||||
|
git tag stable "$GITHUB_SHA"
|
||||||
|
git push origin stable
|
||||||
|
|
||||||
|
echo "✅ Stable tag updated."
|
||||||
21
.github/workflows/test-container.yml
vendored
21
.github/workflows/test-container.yml
vendored
@@ -1,25 +1,28 @@
|
|||||||
name: Test OS Containers
|
name: Test OS Containers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-container:
|
test-container:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Show commit SHA
|
||||||
|
run: git rev-parse HEAD
|
||||||
|
|
||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run container tests
|
- name: Run container tests (${{ matrix.distro }})
|
||||||
run: make test-container
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
distro="${{ matrix.distro }}" make test-container
|
||||||
|
|||||||
20
.github/workflows/test-e2e.yml
vendored
20
.github/workflows/test-e2e.yml
vendored
@@ -1,18 +1,16 @@
|
|||||||
name: Test End-To-End
|
name: Test End-To-End
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-e2e:
|
test-e2e:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60 # E2E + all distros can be heavier
|
timeout-minutes: 60 # E2E can be heavier
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
distro: [arch, debian, ubuntu, fedora, centos]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -21,5 +19,7 @@ jobs:
|
|||||||
- name: Show Docker version
|
- name: Show Docker version
|
||||||
run: docker version
|
run: docker version
|
||||||
|
|
||||||
- name: Run E2E tests via make (all distros)
|
- name: Run E2E tests via make (${{ matrix.distro }})
|
||||||
run: make test-e2e
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
distro="${{ matrix.distro }}" make test-e2e
|
||||||
|
|||||||
10
.github/workflows/test-integration.yml
vendored
10
.github/workflows/test-integration.yml
vendored
@@ -1,13 +1,7 @@
|
|||||||
name: Test Code Integration
|
name: Test Code Integration
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-integration:
|
test-integration:
|
||||||
@@ -22,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 DISTROS="arch"
|
run: make test-integration distro="arch"
|
||||||
|
|||||||
10
.github/workflows/test-unit.yml
vendored
10
.github/workflows/test-unit.yml
vendored
@@ -1,13 +1,7 @@
|
|||||||
name: Test Units
|
name: Test Units
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-unit:
|
test-unit:
|
||||||
@@ -22,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 DISTROS="arch"
|
run: make test-unit distro="arch"
|
||||||
|
|||||||
8
.github/workflows/test-virgin-root.yml
vendored
8
.github/workflows/test-virgin-root.yml
vendored
@@ -1,13 +1,7 @@
|
|||||||
name: Test Virgin Root
|
name: Test Virgin Root
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-virgin-root:
|
test-virgin-root:
|
||||||
|
|||||||
8
.github/workflows/test-virgin-user.yml
vendored
8
.github/workflows/test-virgin-user.yml
vendored
@@ -1,13 +1,7 @@
|
|||||||
name: Test Virgin User
|
name: Test Virgin User
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
- "*"
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-virgin-user:
|
test-virgin-user:
|
||||||
|
|||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -14,17 +14,8 @@ venv/
|
|||||||
dist/
|
dist/
|
||||||
build/*
|
build/*
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
pkg
|
|
||||||
src/source
|
|
||||||
package-manager-*
|
package-manager-*
|
||||||
|
|
||||||
# debian
|
|
||||||
debian/package-manager/
|
|
||||||
debian/debhelper-build-stamp
|
|
||||||
debian/files
|
|
||||||
debian/.debhelper/
|
|
||||||
debian/package-manager.substvars
|
|
||||||
|
|
||||||
# Editor files
|
# Editor files
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
@@ -35,6 +26,9 @@ Thumbs.db
|
|||||||
|
|
||||||
# Nix Cache to speed up tests
|
# Nix Cache to speed up tests
|
||||||
.nix/
|
.nix/
|
||||||
|
.nix-dev-installed
|
||||||
|
|
||||||
# Ignore logs
|
# Ignore logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
result
|
||||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -1,3 +1,50 @@
|
|||||||
|
## [0.10.1] - 2025-12-11
|
||||||
|
|
||||||
|
* Fixed Debian\Ubuntu to pass container e2e tests
|
||||||
|
|
||||||
|
|
||||||
|
## [0.10.0] - 2025-12-11
|
||||||
|
|
||||||
|
* **Changes since v0.9.1**
|
||||||
|
|
||||||
|
**Mirror System**
|
||||||
|
|
||||||
|
* Added SSH mirror support including multi-push and remote probing
|
||||||
|
* Introduced mirror management commands and refactored the CLI parser into modules
|
||||||
|
|
||||||
|
**CI/CD**
|
||||||
|
|
||||||
|
* Migrated to reusable workflows with improved debugging instrumentation
|
||||||
|
* Made stable-tag automation reliable for workflow_run events and permissions
|
||||||
|
* Ensured deterministic test results by rebuilding all test containers with no-cache
|
||||||
|
|
||||||
|
**E2E and Container Tests**
|
||||||
|
|
||||||
|
* Fixed Git safe.directory handling across all containers
|
||||||
|
* Restored Dockerfile ENTRYPOINT to resolve Nix TLS issues
|
||||||
|
* Fixed missing volume errors and hardened the E2E runner
|
||||||
|
* Added full Nix flake E2E test matrix across all distro containers
|
||||||
|
* Disabled Nix sandboxing for cross-distro builds where required
|
||||||
|
|
||||||
|
**Nix and Python Environment**
|
||||||
|
|
||||||
|
* Unified Nix Python environment and introduced lazy CLI imports
|
||||||
|
* Ensured PyYAML availability and improved Python 3.13 compatibility
|
||||||
|
* Refactored flake.nix to remove side effects and rely on generic python3
|
||||||
|
|
||||||
|
**Packaging**
|
||||||
|
|
||||||
|
* Removed Debian’s hard dependency on Nix
|
||||||
|
* Restructured packaging layout and refined build paths
|
||||||
|
* Excluded assets from Arch PKGBUILD rsync
|
||||||
|
* Cleaned up obsolete ignore files
|
||||||
|
|
||||||
|
**Repository Layout**
|
||||||
|
|
||||||
|
* Restructured repository to align local, Nix-based, and distro-based build workflows
|
||||||
|
* Added Arch support and refined build/purge scripts
|
||||||
|
|
||||||
|
|
||||||
## [0.9.1] - 2025-12-10
|
## [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.
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Base image selector — overridden by Makefile
|
# Base image selector — overridden by Makefile
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
ARG BASE_IMAGE=archlinux:latest
|
ARG BASE_IMAGE
|
||||||
FROM ${BASE_IMAGE}
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
|
RUN echo "BASE_IMAGE=${BASE_IMAGE}" && \
|
||||||
|
cat /etc/os-release || true
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Nix environment defaults
|
# Nix environment defaults
|
||||||
#
|
#
|
||||||
|
|||||||
3
MIRRORS
Normal file
3
MIRRORS
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
git@github.com:kevinveenbirkenbach/package-manager.git
|
||||||
|
ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git
|
||||||
|
ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git
|
||||||
14
Makefile
14
Makefile
@@ -2,11 +2,15 @@
|
|||||||
test build build-no-cache test-unit test-e2e test-integration \
|
test build build-no-cache test-unit test-e2e test-integration \
|
||||||
test-container
|
test-container
|
||||||
|
|
||||||
|
# Distro
|
||||||
|
# Options: arch debian ubuntu fedora centos
|
||||||
|
distro ?= arch
|
||||||
|
export distro
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Distro list and base images
|
# Base images
|
||||||
# (kept for documentation/reference; actual build logic is in scripts/build)
|
# (kept for documentation/reference; actual build logic is in scripts/build)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
DISTROS := arch debian ubuntu fedora centos
|
|
||||||
BASE_IMAGE_ARCH := archlinux:latest
|
BASE_IMAGE_ARCH := archlinux:latest
|
||||||
BASE_IMAGE_DEBIAN := debian:stable-slim
|
BASE_IMAGE_DEBIAN := debian:stable-slim
|
||||||
BASE_IMAGE_UBUNTU := ubuntu:latest
|
BASE_IMAGE_UBUNTU := ubuntu:latest
|
||||||
@@ -14,7 +18,6 @@ BASE_IMAGE_FEDORA := fedora:latest
|
|||||||
BASE_IMAGE_CENTOS := quay.io/centos/centos:stream9
|
BASE_IMAGE_CENTOS := quay.io/centos/centos:stream9
|
||||||
|
|
||||||
# Make them available in scripts
|
# Make them available in scripts
|
||||||
export DISTROS
|
|
||||||
export BASE_IMAGE_ARCH
|
export BASE_IMAGE_ARCH
|
||||||
export BASE_IMAGE_DEBIAN
|
export BASE_IMAGE_DEBIAN
|
||||||
export BASE_IMAGE_UBUNTU
|
export BASE_IMAGE_UBUNTU
|
||||||
@@ -65,6 +68,11 @@ build-missing:
|
|||||||
# Combined test target for local + CI (unit + integration + e2e)
|
# Combined test target for local + CI (unit + integration + e2e)
|
||||||
test: test-container test-unit test-integration test-e2e
|
test: test-container test-unit test-integration test-e2e
|
||||||
|
|
||||||
|
delete-volumes:
|
||||||
|
@docker volume rm pkgmgr_nix_store_${distro} pkgmgr_nix_cache_${distro} || true
|
||||||
|
|
||||||
|
purge: delete-volumes build-no-cache
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# System install (native packages, calls scripts/installation/run-package.sh)
|
# System install (native packages, calls scripts/installation/run-package.sh)
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ installation layers, and setup controller flow:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
**Diagram status:** *Stand: 10. Dezember 2025*
|
**Diagram status:** *Stand: 11. Dezember 2025*
|
||||||
**Always-up-to-date version:** https://s.veen.world/pkgmgrmp
|
**Always-up-to-date version:** https://s.veen.world/pkgmgrmp
|
||||||
|
|
||||||
## Installation ⚙️
|
## Installation ⚙️
|
||||||
|
|||||||
6
TODO.md
Normal file
6
TODO.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# to-dos
|
||||||
|
|
||||||
|
For the following checkout the implementation map:
|
||||||
|
|
||||||
|
- Implement TAGS
|
||||||
|
- Implement SIGNING_KEY
|
||||||
BIN
assets/map.png
BIN
assets/map.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1765186076,
|
||||||
|
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
35
flake.nix
35
flake.nix
@@ -26,12 +26,17 @@
|
|||||||
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;
|
||||||
pyPkgs = pkgs.python311Packages;
|
pyPkgs = pkgs.python311Packages;
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "0.9.1";
|
version = "0.10.1";
|
||||||
|
|
||||||
# Use the git repo as source
|
# Use the git repo as source
|
||||||
src = ./.;
|
src = ./.;
|
||||||
@@ -45,7 +50,7 @@
|
|||||||
pyPkgs.wheel
|
pyPkgs.wheel
|
||||||
];
|
];
|
||||||
|
|
||||||
# Runtime dependencies (matches [project.dependencies])
|
# Runtime dependencies (matches [project.dependencies] in pyproject.toml)
|
||||||
propagatedBuildInputs = [
|
propagatedBuildInputs = [
|
||||||
pyPkgs.pyyaml
|
pyPkgs.pyyaml
|
||||||
pyPkgs.pip
|
pyPkgs.pip
|
||||||
@@ -55,6 +60,7 @@
|
|||||||
|
|
||||||
pythonImportsCheck = [ "pkgmgr" ];
|
pythonImportsCheck = [ "pkgmgr" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
default = pkgmgr;
|
default = pkgmgr;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -65,29 +71,42 @@
|
|||||||
devShells = forAllSystems (system:
|
devShells = forAllSystems (system:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
pkgmgrPkg = self.packages.${system}.pkgmgr;
|
|
||||||
|
|
||||||
ansiblePkg =
|
ansiblePkg =
|
||||||
if pkgs ? ansible-core then pkgs.ansible-core
|
if pkgs ? ansible-core then pkgs.ansible-core
|
||||||
else pkgs.ansible;
|
else pkgs.ansible;
|
||||||
|
|
||||||
# Python 3 + pip für alles, was "python3 -m pip" macht
|
# Use the same Python version as the package (3.11)
|
||||||
pythonWithPip = pkgs.python3.withPackages (ps: [
|
python = pkgs.python311;
|
||||||
|
|
||||||
|
pythonWithDeps = python.withPackages (ps: [
|
||||||
ps.pip
|
ps.pip
|
||||||
|
ps.pyyaml
|
||||||
]);
|
]);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
buildInputs = [
|
buildInputs = [
|
||||||
pythonWithPip
|
pythonWithDeps
|
||||||
pkgmgrPkg
|
|
||||||
pkgs.git
|
pkgs.git
|
||||||
ansiblePkg
|
ansiblePkg
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
# Ensure our Python with dependencies is preferred on PATH
|
||||||
|
export PATH=${pythonWithDeps}/bin:$PATH
|
||||||
|
|
||||||
|
# Ensure src/ layout is importable:
|
||||||
|
# pkgmgr lives in ./src/pkgmgr
|
||||||
|
export PYTHONPATH="$PWD/src:${PYTHONPATH:-}"
|
||||||
|
# Also add repo root in case tools/tests rely on it
|
||||||
|
export PYTHONPATH="$PWD:$PYTHONPATH"
|
||||||
|
|
||||||
echo "Entered pkgmgr development shell for ${system}"
|
echo "Entered pkgmgr development shell for ${system}"
|
||||||
echo "pkgmgr CLI is available via the flake build"
|
echo "Python used in this shell:"
|
||||||
|
python --version
|
||||||
|
echo "pkgmgr CLI (from source) is available via:"
|
||||||
|
echo " python -m pkgmgr.cli --help"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
8
main.py
8
main.py
@@ -1,4 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
from pkgmgr.cli import main
|
||||||
|
|
||||||
|
|||||||
6
packaging/arch/.gitignore
vendored
Normal file
6
packaging/arch/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Arch pkg artifacts
|
||||||
|
*.pkg.tar.*
|
||||||
|
*.log
|
||||||
|
package-manager-*
|
||||||
|
src/
|
||||||
|
pkg/
|
||||||
@@ -15,7 +15,7 @@ makedepends=('rsync')
|
|||||||
install=${pkgname}.install
|
install=${pkgname}.install
|
||||||
|
|
||||||
# Local source checkout — avoids the tarball requirement.
|
# Local source checkout — avoids the tarball requirement.
|
||||||
# This assumes you build the package from inside the main project repository.
|
# We build from the project root (two levels above packaging/arch/).
|
||||||
source=()
|
source=()
|
||||||
sha256sums=()
|
sha256sums=()
|
||||||
|
|
||||||
@@ -24,12 +24,18 @@ _srcdir_name="source"
|
|||||||
|
|
||||||
prepare() {
|
prepare() {
|
||||||
mkdir -p "$srcdir/$_srcdir_name"
|
mkdir -p "$srcdir/$_srcdir_name"
|
||||||
|
|
||||||
|
local project_root
|
||||||
|
project_root="$(cd "$startdir/../.." && pwd)"
|
||||||
|
|
||||||
rsync -a \
|
rsync -a \
|
||||||
--exclude=".git" \
|
--exclude=".git" \
|
||||||
--exclude=".github" \
|
--exclude=".github" \
|
||||||
--exclude="pkg" \
|
--exclude="pkg" \
|
||||||
--exclude="srcpkg" \
|
--exclude="srcpkg" \
|
||||||
"$startdir/" "$srcdir/$_srcdir_name/"
|
--exclude="packaging" \
|
||||||
|
--exclude="assets" \
|
||||||
|
"$project_root/" "$srcdir/$_srcdir_name/"
|
||||||
}
|
}
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
@@ -62,7 +68,8 @@ package() {
|
|||||||
"$pkgdir/usr/lib/package-manager/PKGBUILD" \
|
"$pkgdir/usr/lib/package-manager/PKGBUILD" \
|
||||||
"$pkgdir/usr/lib/package-manager/Dockerfile" \
|
"$pkgdir/usr/lib/package-manager/Dockerfile" \
|
||||||
"$pkgdir/usr/lib/package-manager/debian" \
|
"$pkgdir/usr/lib/package-manager/debian" \
|
||||||
|
"$pkgdir/usr/lib/package-manager/packaging" \
|
||||||
"$pkgdir/usr/lib/package-manager/.gitignore" \
|
"$pkgdir/usr/lib/package-manager/.gitignore" \
|
||||||
"$pkgdir/usr/lib/package-manager/__pycache__" \
|
"$pkgdir/usr/lib/package-manager/__pycache__" \
|
||||||
"$pkgdir/usr/lib/package-manager/.gitkeep"
|
"$pkgdir/usr/lib/package-manager/.gitkeep" || true
|
||||||
}
|
}
|
||||||
6
packaging/debian/.gitignore
vendored
Normal file
6
packaging/debian/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# debian
|
||||||
|
package-manager/
|
||||||
|
debhelper-build-stamp
|
||||||
|
files
|
||||||
|
.debhelper/
|
||||||
|
package-manager.substvars
|
||||||
@@ -9,7 +9,7 @@ Homepage: https://github.com/kevinveenbirkenbach/package-manager
|
|||||||
|
|
||||||
Package: package-manager
|
Package: package-manager
|
||||||
Architecture: any
|
Architecture: any
|
||||||
Depends: nix, ${misc:Depends}
|
Depends: sudo, ${misc:Depends}
|
||||||
Description: Wrapper that runs Kevin's package-manager via Nix flake
|
Description: Wrapper that runs Kevin's package-manager via Nix flake
|
||||||
This package provides the `pkgmgr` command, which runs Kevin's package
|
This package provides the `pkgmgr` command, which runs Kevin's package
|
||||||
manager via a local Nix flake
|
manager via a local Nix flake
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from pkgmgr.cli.proxy import register_proxy_commands
|
|
||||||
|
|
||||||
|
|
||||||
class SortedSubParsersAction(argparse._SubParsersAction):
|
|
||||||
"""
|
|
||||||
Subparsers action that keeps choices sorted alphabetically.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def add_parser(self, name, **kwargs):
|
|
||||||
parser = super().add_parser(name, **kwargs)
|
|
||||||
# Sort choices alphabetically by dest (subcommand name)
|
|
||||||
self._choices_actions.sort(key=lambda a: a.dest)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def add_identifier_arguments(subparser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
|
||||||
Common identifier / selection arguments for many subcommands.
|
|
||||||
|
|
||||||
Selection modes (mutual intent, not hard-enforced):
|
|
||||||
- identifiers (positional): select by alias / provider/account/repo
|
|
||||||
- --all: select all repositories
|
|
||||||
- --category / --string / --tag: filter-based selection on top
|
|
||||||
of the full repository set
|
|
||||||
"""
|
|
||||||
subparser.add_argument(
|
|
||||||
"identifiers",
|
|
||||||
nargs="*",
|
|
||||||
help=(
|
|
||||||
"Identifier(s) for repositories. "
|
|
||||||
"Default: Repository of current folder."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--all",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help=(
|
|
||||||
"Apply the subcommand to all repositories in the config. "
|
|
||||||
"Some subcommands ask for confirmation. If you want to give this "
|
|
||||||
"confirmation for all repositories, pipe 'yes'. E.g: "
|
|
||||||
"yes | pkgmgr {subcommand} --all"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--category",
|
|
||||||
nargs="+",
|
|
||||||
default=[],
|
|
||||||
help=(
|
|
||||||
"Filter repositories by category patterns derived from config "
|
|
||||||
"filenames or repo metadata (use filename without .yml/.yaml, "
|
|
||||||
"or /regex/ to use a regular expression)."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--string",
|
|
||||||
default="",
|
|
||||||
help=(
|
|
||||||
"Filter repositories whose identifier / name / path contains this "
|
|
||||||
"substring (case-insensitive). Use /regex/ for regular expressions."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--tag",
|
|
||||||
action="append",
|
|
||||||
default=[],
|
|
||||||
help=(
|
|
||||||
"Filter repositories by tag. Matches tags from the repository "
|
|
||||||
"collector and category tags. Use /regex/ for regular expressions."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--preview",
|
|
||||||
action="store_true",
|
|
||||||
help="Preview changes without executing commands",
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--list",
|
|
||||||
action="store_true",
|
|
||||||
help="List affected repositories (with preview or status)",
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"-a",
|
|
||||||
"--args",
|
|
||||||
nargs=argparse.REMAINDER,
|
|
||||||
dest="extra_args",
|
|
||||||
help="Additional parameters to be attached.",
|
|
||||||
default=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def add_install_update_arguments(subparser: argparse.ArgumentParser) -> None:
|
|
||||||
"""
|
|
||||||
Common arguments for install/update commands.
|
|
||||||
"""
|
|
||||||
add_identifier_arguments(subparser)
|
|
||||||
subparser.add_argument(
|
|
||||||
"-q",
|
|
||||||
"--quiet",
|
|
||||||
action="store_true",
|
|
||||||
help="Suppress warnings and info messages",
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--no-verification",
|
|
||||||
action="store_true",
|
|
||||||
default=False,
|
|
||||||
help="Disable verification via commit/gpg",
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--dependencies",
|
|
||||||
action="store_true",
|
|
||||||
help="Also pull and update dependencies",
|
|
||||||
)
|
|
||||||
subparser.add_argument(
|
|
||||||
"--clone-mode",
|
|
||||||
choices=["ssh", "https", "shallow"],
|
|
||||||
default="ssh",
|
|
||||||
help=(
|
|
||||||
"Specify the clone mode: ssh, https, or shallow "
|
|
||||||
"(HTTPS shallow clone; default: ssh)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_parser(description_text: str) -> argparse.ArgumentParser:
|
|
||||||
"""
|
|
||||||
Create the top-level argument parser for pkgmgr.
|
|
||||||
"""
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description=description_text,
|
|
||||||
formatter_class=argparse.RawTextHelpFormatter,
|
|
||||||
)
|
|
||||||
subparsers = parser.add_subparsers(
|
|
||||||
dest="command",
|
|
||||||
help="Subcommands",
|
|
||||||
action=SortedSubParsersAction,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# install / update / deinstall / delete
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
install_parser = subparsers.add_parser(
|
|
||||||
"install",
|
|
||||||
help="Setup repository/repositories alias links to executables",
|
|
||||||
)
|
|
||||||
add_install_update_arguments(install_parser)
|
|
||||||
|
|
||||||
update_parser = subparsers.add_parser(
|
|
||||||
"update",
|
|
||||||
help="Update (pull + install) repository/repositories",
|
|
||||||
)
|
|
||||||
add_install_update_arguments(update_parser)
|
|
||||||
update_parser.add_argument(
|
|
||||||
"--system",
|
|
||||||
action="store_true",
|
|
||||||
help="Include system update commands",
|
|
||||||
)
|
|
||||||
|
|
||||||
deinstall_parser = subparsers.add_parser(
|
|
||||||
"deinstall",
|
|
||||||
help="Remove alias links to repository/repositories",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(deinstall_parser)
|
|
||||||
|
|
||||||
delete_parser = subparsers.add_parser(
|
|
||||||
"delete",
|
|
||||||
help="Delete repository/repositories alias links to executables",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(delete_parser)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# create
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
create_cmd_parser = subparsers.add_parser(
|
|
||||||
"create",
|
|
||||||
help=(
|
|
||||||
"Create new repository entries: add them to the config if not "
|
|
||||||
"already present, initialize the local repository, and push "
|
|
||||||
"remotely if --remote is set."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
add_identifier_arguments(create_cmd_parser)
|
|
||||||
create_cmd_parser.add_argument(
|
|
||||||
"--remote",
|
|
||||||
action="store_true",
|
|
||||||
help="If set, add the remote and push the initial commit.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# status
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
status_parser = subparsers.add_parser(
|
|
||||||
"status",
|
|
||||||
help="Show status for repository/repositories or system",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(status_parser)
|
|
||||||
status_parser.add_argument(
|
|
||||||
"--system",
|
|
||||||
action="store_true",
|
|
||||||
help="Show system status",
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# config
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
config_parser = subparsers.add_parser(
|
|
||||||
"config",
|
|
||||||
help="Manage configuration",
|
|
||||||
)
|
|
||||||
config_subparsers = config_parser.add_subparsers(
|
|
||||||
dest="subcommand",
|
|
||||||
help="Config subcommands",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
config_show = config_subparsers.add_parser(
|
|
||||||
"show",
|
|
||||||
help="Show configuration",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(config_show)
|
|
||||||
|
|
||||||
config_subparsers.add_parser(
|
|
||||||
"add",
|
|
||||||
help="Interactively add a new repository entry",
|
|
||||||
)
|
|
||||||
|
|
||||||
config_subparsers.add_parser(
|
|
||||||
"edit",
|
|
||||||
help="Edit configuration file with nano",
|
|
||||||
)
|
|
||||||
|
|
||||||
config_subparsers.add_parser(
|
|
||||||
"init",
|
|
||||||
help="Initialize user configuration by scanning the base directory",
|
|
||||||
)
|
|
||||||
|
|
||||||
config_delete = config_subparsers.add_parser(
|
|
||||||
"delete",
|
|
||||||
help="Delete repository entry from user config",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(config_delete)
|
|
||||||
|
|
||||||
config_ignore = config_subparsers.add_parser(
|
|
||||||
"ignore",
|
|
||||||
help="Set ignore flag for repository entries in user config",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(config_ignore)
|
|
||||||
config_ignore.add_argument(
|
|
||||||
"--set",
|
|
||||||
choices=["true", "false"],
|
|
||||||
required=True,
|
|
||||||
help="Set ignore to true or false",
|
|
||||||
)
|
|
||||||
|
|
||||||
config_subparsers.add_parser(
|
|
||||||
"update",
|
|
||||||
help=(
|
|
||||||
"Update default config files in ~/.config/pkgmgr/ from the "
|
|
||||||
"installed pkgmgr package (does not touch config.yaml)."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# path / explore / terminal / code / shell
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
path_parser = subparsers.add_parser(
|
|
||||||
"path",
|
|
||||||
help="Print the path(s) of repository/repositories",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(path_parser)
|
|
||||||
|
|
||||||
explore_parser = subparsers.add_parser(
|
|
||||||
"explore",
|
|
||||||
help="Open repository in Nautilus file manager",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(explore_parser)
|
|
||||||
|
|
||||||
terminal_parser = subparsers.add_parser(
|
|
||||||
"terminal",
|
|
||||||
help="Open repository in a new GNOME Terminal tab",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(terminal_parser)
|
|
||||||
|
|
||||||
code_parser = subparsers.add_parser(
|
|
||||||
"code",
|
|
||||||
help="Open repository workspace with VS Code",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(code_parser)
|
|
||||||
|
|
||||||
shell_parser = subparsers.add_parser(
|
|
||||||
"shell",
|
|
||||||
help="Execute a shell command in each repository",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(shell_parser)
|
|
||||||
shell_parser.add_argument(
|
|
||||||
"-c",
|
|
||||||
"--command",
|
|
||||||
nargs=argparse.REMAINDER,
|
|
||||||
dest="shell_command",
|
|
||||||
help=(
|
|
||||||
"The shell command (and its arguments) to execute in each "
|
|
||||||
"repository"
|
|
||||||
),
|
|
||||||
default=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# branch
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
branch_parser = subparsers.add_parser(
|
|
||||||
"branch",
|
|
||||||
help="Branch-related utilities (e.g. open/close feature branches)",
|
|
||||||
)
|
|
||||||
branch_subparsers = branch_parser.add_subparsers(
|
|
||||||
dest="subcommand",
|
|
||||||
help="Branch subcommands",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
branch_open = branch_subparsers.add_parser(
|
|
||||||
"open",
|
|
||||||
help="Create and push a new branch on top of a base branch",
|
|
||||||
)
|
|
||||||
branch_open.add_argument(
|
|
||||||
"name",
|
|
||||||
nargs="?",
|
|
||||||
help=(
|
|
||||||
"Name of the new branch (optional; will be asked interactively "
|
|
||||||
"if omitted)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
branch_open.add_argument(
|
|
||||||
"--base",
|
|
||||||
default="main",
|
|
||||||
help="Base branch to create the new branch from (default: main)",
|
|
||||||
)
|
|
||||||
|
|
||||||
branch_close = branch_subparsers.add_parser(
|
|
||||||
"close",
|
|
||||||
help="Merge a feature branch into base and delete it",
|
|
||||||
)
|
|
||||||
branch_close.add_argument(
|
|
||||||
"name",
|
|
||||||
nargs="?",
|
|
||||||
help=(
|
|
||||||
"Name of the branch to close (optional; current branch is used "
|
|
||||||
"if omitted)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
branch_close.add_argument(
|
|
||||||
"--base",
|
|
||||||
default="main",
|
|
||||||
help=(
|
|
||||||
"Base branch to merge into (default: main; falls back to master "
|
|
||||||
"internally if main does not exist)"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# release
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
release_parser = subparsers.add_parser(
|
|
||||||
"release",
|
|
||||||
help=(
|
|
||||||
"Create a release for repository/ies by incrementing version "
|
|
||||||
"and updating the changelog."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
release_parser.add_argument(
|
|
||||||
"release_type",
|
|
||||||
choices=["major", "minor", "patch"],
|
|
||||||
help="Type of version increment for the release (major, minor, patch).",
|
|
||||||
)
|
|
||||||
release_parser.add_argument(
|
|
||||||
"-m",
|
|
||||||
"--message",
|
|
||||||
default=None,
|
|
||||||
help=(
|
|
||||||
"Optional release message to add to the changelog and tag."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# Generic selection / preview / list / extra_args
|
|
||||||
add_identifier_arguments(release_parser)
|
|
||||||
# Close current branch after successful release
|
|
||||||
release_parser.add_argument(
|
|
||||||
"--close",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Close the current branch after a successful release in each "
|
|
||||||
"repository, if it is not main/master."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
# Force: skip preview+confirmation and run release directly
|
|
||||||
release_parser.add_argument(
|
|
||||||
"-f",
|
|
||||||
"--force",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Skip the interactive preview+confirmation step and run the "
|
|
||||||
"release directly."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# version
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
version_parser = subparsers.add_parser(
|
|
||||||
"version",
|
|
||||||
help=(
|
|
||||||
"Show version information for repository/ies "
|
|
||||||
"(git tags, pyproject.toml, flake.nix, PKGBUILD, debian, spec, "
|
|
||||||
"Ansible Galaxy)."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
add_identifier_arguments(version_parser)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# changelog
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
changelog_parser = subparsers.add_parser(
|
|
||||||
"changelog",
|
|
||||||
help=(
|
|
||||||
"Show changelog derived from Git history. "
|
|
||||||
"By default, shows the changes between the last two SemVer tags."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
changelog_parser.add_argument(
|
|
||||||
"range",
|
|
||||||
nargs="?",
|
|
||||||
default="",
|
|
||||||
help=(
|
|
||||||
"Optional tag or range (e.g. v1.2.3 or v1.2.0..v1.2.3). "
|
|
||||||
"If omitted, the changelog between the last two SemVer "
|
|
||||||
"tags is shown."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
add_identifier_arguments(changelog_parser)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# list
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
list_parser = subparsers.add_parser(
|
|
||||||
"list",
|
|
||||||
help="List all repositories with details and status",
|
|
||||||
)
|
|
||||||
# dieselbe Selektionslogik wie bei install/update/etc.:
|
|
||||||
add_identifier_arguments(list_parser)
|
|
||||||
list_parser.add_argument(
|
|
||||||
"--status",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help=(
|
|
||||||
"Filter repositories by status (case insensitive). "
|
|
||||||
"Use /regex/ for regular expressions."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
list_parser.add_argument(
|
|
||||||
"--description",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"Show an additional detailed section per repository "
|
|
||||||
"(description, homepage, tags, categories, paths)."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# make
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
make_parser = subparsers.add_parser(
|
|
||||||
"make",
|
|
||||||
help="Executes make commands",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(make_parser)
|
|
||||||
make_subparsers = make_parser.add_subparsers(
|
|
||||||
dest="subcommand",
|
|
||||||
help="Make subcommands",
|
|
||||||
required=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
make_install = make_subparsers.add_parser(
|
|
||||||
"install",
|
|
||||||
help="Executes the make install command",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(make_install)
|
|
||||||
|
|
||||||
make_deinstall = make_subparsers.add_parser(
|
|
||||||
"deinstall",
|
|
||||||
help="Executes the make deinstall command",
|
|
||||||
)
|
|
||||||
add_identifier_arguments(make_deinstall)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Proxy commands (git, docker, docker compose, ...)
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
register_proxy_commands(subparsers)
|
|
||||||
|
|
||||||
return parser
|
|
||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "package-manager"
|
name = "package-manager"
|
||||||
version = "0.9.1"
|
version = "0.10.1"
|
||||||
description = "Kevin's package-manager tool (pkgmgr)"
|
description = "Kevin's package-manager tool (pkgmgr)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
@@ -39,13 +39,13 @@ pkgmgr = "pkgmgr.cli:main"
|
|||||||
# -----------------------------
|
# -----------------------------
|
||||||
# setuptools configuration
|
# setuptools configuration
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# We use find_packages(), not a fixed list,
|
# Source layout: all packages live under "src/"
|
||||||
# and explicitly include pkgmgr* and config*
|
[tool.setuptools]
|
||||||
|
package-dir = { "" = "src", "config" = "config" }
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["src", "."]
|
||||||
include = ["pkgmgr*", "config*"]
|
include = ["pkgmgr*", "config*"]
|
||||||
|
|
||||||
# Ensure defaults.yaml is shipped inside wheels & nix builds
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"config" = ["defaults.yaml"]
|
"config" = ["defaults.yaml"]
|
||||||
|
|||||||
@@ -4,32 +4,21 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/resolve-base-image.sh"
|
source "${SCRIPT_DIR}/resolve-base-image.sh"
|
||||||
|
|
||||||
echo "============================================================"
|
IMAGE="package-manager-test-$distro"
|
||||||
echo ">>> Building ONLY missing container images"
|
BASE_IMAGE="$(resolve_base_image "$distro")"
|
||||||
echo "============================================================"
|
|
||||||
|
|
||||||
for distro in $DISTROS; do
|
if docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
||||||
IMAGE="package-manager-test-$distro"
|
echo "[build-missing] Image already exists: $IMAGE (skipping)"
|
||||||
BASE_IMAGE="$(resolve_base_image "$distro")"
|
exit 0
|
||||||
|
fi
|
||||||
if docker image inspect "$IMAGE" >/dev/null 2>&1; then
|
|
||||||
echo "[build-missing] Image already exists: $IMAGE (skipping)"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "------------------------------------------------------------"
|
|
||||||
echo "[build-missing] Building missing image: $IMAGE"
|
|
||||||
echo "BASE_IMAGE = $BASE_IMAGE"
|
|
||||||
echo "------------------------------------------------------------"
|
|
||||||
|
|
||||||
docker build \
|
|
||||||
--build-arg BASE_IMAGE="$BASE_IMAGE" \
|
|
||||||
-t "$IMAGE" \
|
|
||||||
.
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "============================================================"
|
echo "------------------------------------------------------------"
|
||||||
echo ">>> build-missing: Done"
|
echo "[build-missing] Building missing image: $IMAGE"
|
||||||
echo "============================================================"
|
echo "BASE_IMAGE = $BASE_IMAGE"
|
||||||
|
echo "------------------------------------------------------------"
|
||||||
|
|
||||||
|
docker build \
|
||||||
|
--build-arg BASE_IMAGE="$BASE_IMAGE" \
|
||||||
|
-t "$IMAGE" \
|
||||||
|
.
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/resolve-base-image.sh"
|
source "${SCRIPT_DIR}/resolve-base-image.sh"
|
||||||
|
|
||||||
for distro in $DISTROS; do
|
base_image="$(resolve_base_image "$distro")"
|
||||||
base_image="$(resolve_base_image "$distro")"
|
|
||||||
|
|
||||||
echo ">>> Building test image for distro '$distro' with NO CACHE (BASE_IMAGE=$base_image)..."
|
echo ">>> Building test image for distro '$distro' with NO CACHE (BASE_IMAGE=$base_image)..."
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
--build-arg BASE_IMAGE="$base_image" \
|
--build-arg BASE_IMAGE="$base_image" \
|
||||||
-t "package-manager-test-$distro" \
|
-t "package-manager-test-$distro" \
|
||||||
.
|
.
|
||||||
done
|
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ set -euo pipefail
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
source "${SCRIPT_DIR}/resolve-base-image.sh"
|
source "${SCRIPT_DIR}/resolve-base-image.sh"
|
||||||
|
|
||||||
for distro in $DISTROS; do
|
base_image="$(resolve_base_image "$distro")"
|
||||||
base_image="$(resolve_base_image "$distro")"
|
|
||||||
|
|
||||||
echo ">>> Building test image for distro '$distro' (BASE_IMAGE=$base_image)..."
|
echo ">>> Building test image for distro '$distro' (BASE_IMAGE=$base_image)..."
|
||||||
|
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg BASE_IMAGE="$base_image" \
|
--build-arg BASE_IMAGE="$base_image" \
|
||||||
-t "package-manager-test-$distro" \
|
-t "package-manager-test-$distro" \
|
||||||
.
|
.
|
||||||
done
|
|
||||||
|
|||||||
@@ -45,6 +45,26 @@ ensure_nix_on_path() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helper: ensure Nix build group and users exist (build-users-group = nixbld)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ensure_nix_build_group() {
|
||||||
|
# Ensure nixbld group (build-users-group for Nix)
|
||||||
|
if ! getent group nixbld >/dev/null 2>&1; then
|
||||||
|
echo "[init-nix] Creating group 'nixbld'..."
|
||||||
|
groupadd -r nixbld
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure Nix build users (nixbld1..nixbld10) as members of nixbld
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
if ! id "nixbld$i" >/dev/null 2>&1; then
|
||||||
|
echo "[init-nix] Creating build user nixbld$i..."
|
||||||
|
# -r: system account, -g: primary group, -G: supplementary (ensures membership is listed)
|
||||||
|
useradd -r -g nixbld -G nixbld -s /usr/sbin/nologin "nixbld$i"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fast path: Nix already available
|
# Fast path: Nix already available
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -76,20 +96,8 @@ fi
|
|||||||
if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
||||||
echo "[init-nix] Running as root inside a container – using dedicated 'nix' user."
|
echo "[init-nix] Running as root inside a container – using dedicated 'nix' user."
|
||||||
|
|
||||||
# Ensure nixbld group (required by Nix)
|
# Ensure build group/users for Nix
|
||||||
if ! getent group nixbld >/dev/null 2>&1; then
|
ensure_nix_build_group
|
||||||
echo "[init-nix] Creating group 'nixbld'..."
|
|
||||||
groupadd -r nixbld
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure Nix build users (nixbld1..nixbld10) as members of nixbld
|
|
||||||
for i in $(seq 1 10); do
|
|
||||||
if ! id "nixbld$i" >/dev/null 2>&1; then
|
|
||||||
echo "[init-nix] Creating build user nixbld$i..."
|
|
||||||
# -r: system account, -g: primary group, -G: supplementary (ensures membership is listed)
|
|
||||||
useradd -r -g nixbld -G nixbld -s /usr/sbin/nologin "nixbld$i"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Ensure "nix" user (home at /home/nix)
|
# Ensure "nix" user (home at /home/nix)
|
||||||
if ! id nix >/dev/null 2>&1; then
|
if ! id nix >/dev/null 2>&1; then
|
||||||
@@ -187,14 +195,25 @@ if [[ "${IN_CONTAINER}" -eq 0 ]]; then
|
|||||||
# Real host
|
# Real host
|
||||||
if command -v systemctl >/dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
echo "[init-nix] Host with systemd – using multi-user install (--daemon)."
|
echo "[init-nix] Host with systemd – using multi-user install (--daemon)."
|
||||||
|
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||||
|
# Prepare build-users-group for Nix daemon installs
|
||||||
|
ensure_nix_build_group
|
||||||
|
fi
|
||||||
sh <(curl -L https://nixos.org/nix/install) --daemon
|
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||||
else
|
else
|
||||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||||
echo "[init-nix] WARNING: Running as root without systemd on host."
|
echo "[init-nix] WARNING: Running as root without systemd on host."
|
||||||
echo "[init-nix] Falling back to single-user install (--no-daemon), but this is not recommended."
|
echo "[init-nix] Falling back to single-user install (--no-daemon), but this is not recommended."
|
||||||
|
|
||||||
|
# IMPORTANT: This is where Debian/Ubuntu inside your CI end up.
|
||||||
|
# We must ensure 'nixbld' exists before running the installer,
|
||||||
|
# otherwise modern Nix fails with: "the group 'nixbld' ... does not exist".
|
||||||
|
ensure_nix_build_group
|
||||||
|
|
||||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||||
else
|
else
|
||||||
echo "[init-nix] Non-root host without systemd – using single-user install (--no-daemon)."
|
echo "[init-nix] Non-root host without systemd – using single-user install (--no-daemon)."
|
||||||
|
# Non-root cannot create nixbld group; rely on upstream defaults
|
||||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -45,8 +45,42 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
|
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
|
||||||
|
|
||||||
if ! "${RUN_AS_AUR[@]}" 'command -v yay >/dev/null 2>&1'; then
|
if ! "${RUN_AS_AUR[@]}" 'command -v yay >/dev/null 2>&1'; then
|
||||||
"${RUN_AS_AUR[@]}" 'cd ~ && rm -rf yay && git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -si --noconfirm'
|
echo "[aur-builder-setup] yay not found – starting retry sequence for download..."
|
||||||
|
|
||||||
|
MAX_TIME=300 # 5 minutes
|
||||||
|
SLEEP_INTERVAL=20 # 20 seconds
|
||||||
|
ELAPSED=0
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if "${RUN_AS_AUR[@]}" '
|
||||||
|
set -euo pipefail
|
||||||
|
cd ~
|
||||||
|
rm -rf yay || true
|
||||||
|
git clone https://aur.archlinux.org/yay.git yay
|
||||||
|
'; then
|
||||||
|
echo "[aur-builder-setup] yay repository cloned successfully."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[aur-builder-setup] git clone failed (likely 504). Retrying in ${SLEEP_INTERVAL}s..."
|
||||||
|
sleep "${SLEEP_INTERVAL}"
|
||||||
|
ELAPSED=$((ELAPSED + SLEEP_INTERVAL))
|
||||||
|
|
||||||
|
if (( ELAPSED >= MAX_TIME )); then
|
||||||
|
echo "[aur-builder-setup] ERROR: Aborted after 5 minutes of retry attempts."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Now build yay after successful clone
|
||||||
|
"${RUN_AS_AUR[@]}" '
|
||||||
|
set -euo pipefail
|
||||||
|
cd ~/yay
|
||||||
|
makepkg -si --noconfirm
|
||||||
|
'
|
||||||
|
|
||||||
else
|
else
|
||||||
echo "[aur-builder-setup] yay already installed."
|
echo "[aur-builder-setup] yay already installed."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,10 +3,21 @@ set -euo pipefail
|
|||||||
|
|
||||||
echo "[arch/package] Building Arch package (makepkg --nodeps)..."
|
echo "[arch/package] Building Arch package (makepkg --nodeps)..."
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
PKG_DIR="${PROJECT_ROOT}/packaging/arch"
|
||||||
|
|
||||||
|
if [[ ! -f "${PKG_DIR}/PKGBUILD" ]]; then
|
||||||
|
echo "[arch/package] ERROR: PKGBUILD not found in ${PKG_DIR}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "${PKG_DIR}"
|
||||||
|
|
||||||
if id aur_builder >/dev/null 2>&1; then
|
if id aur_builder >/dev/null 2>&1; then
|
||||||
echo "[arch/package] Using 'aur_builder' user for makepkg..."
|
echo "[arch/package] Using 'aur_builder' user for makepkg..."
|
||||||
chown -R aur_builder:aur_builder "$(pwd)"
|
chown -R aur_builder:aur_builder "${PKG_DIR}"
|
||||||
su aur_builder -c "cd '$(pwd)' && rm -f package-manager-*.pkg.tar.* && makepkg --noconfirm --clean --nodeps"
|
su aur_builder -c "cd '${PKG_DIR}' && rm -f package-manager-*.pkg.tar.* && makepkg --noconfirm --clean --nodeps"
|
||||||
else
|
else
|
||||||
echo "[arch/package] WARNING: user 'aur_builder' not found, running makepkg as current user..."
|
echo "[arch/package] WARNING: user 'aur_builder' not found, running makepkg as current user..."
|
||||||
rm -f package-manager-*.pkg.tar.*
|
rm -f package-manager-*.pkg.tar.*
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ set -euo pipefail
|
|||||||
echo "[centos/package] Setting up rpmbuild directories..."
|
echo "[centos/package] Setting up rpmbuild directories..."
|
||||||
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
SPEC_PATH="${PROJECT_ROOT}/packaging/fedora/package-manager.spec"
|
||||||
|
|
||||||
|
if [[ ! -f "${SPEC_PATH}" ]]; then
|
||||||
|
echo "[centos/package] ERROR: SPEC file not found: ${SPEC_PATH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[centos/package] Extracting version from package-manager.spec..."
|
echo "[centos/package] Extracting version from package-manager.spec..."
|
||||||
version="$(grep -E '^Version:' package-manager.spec | awk '{print $2}')"
|
version="$(grep -E '^Version:' "${SPEC_PATH}" | awk '{print $2}')"
|
||||||
if [[ -z "${version}" ]]; then
|
if [[ -z "${version}" ]]; then
|
||||||
echo "ERROR: Version missing!"
|
echo "ERROR: Version missing!"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -15,13 +24,13 @@ srcdir="package-manager-${version}"
|
|||||||
echo "[centos/package] Preparing source tree: ${srcdir}"
|
echo "[centos/package] Preparing source tree: ${srcdir}"
|
||||||
rm -rf "/tmp/${srcdir}"
|
rm -rf "/tmp/${srcdir}"
|
||||||
mkdir -p "/tmp/${srcdir}"
|
mkdir -p "/tmp/${srcdir}"
|
||||||
cp -a . "/tmp/${srcdir}/"
|
cp -a "${PROJECT_ROOT}/." "/tmp/${srcdir}/"
|
||||||
|
|
||||||
echo "[centos/package] Creating source tarball..."
|
echo "[centos/package] Creating source tarball..."
|
||||||
tar czf "/root/rpmbuild/SOURCES/${srcdir}.tar.gz" -C /tmp "${srcdir}"
|
tar czf "/root/rpmbuild/SOURCES/${srcdir}.tar.gz" -C /tmp "${srcdir}"
|
||||||
|
|
||||||
echo "[centos/package] Copying SPEC..."
|
echo "[centos/package] Copying SPEC..."
|
||||||
cp package-manager.spec /root/rpmbuild/SPECS/
|
cp "${SPEC_PATH}" /root/rpmbuild/SPECS/
|
||||||
|
|
||||||
echo "[centos/package] Running rpmbuild..."
|
echo "[centos/package] Running rpmbuild..."
|
||||||
cd /root/rpmbuild/SPECS
|
cd /root/rpmbuild/SPECS
|
||||||
|
|||||||
@@ -3,6 +3,25 @@ set -euo pipefail
|
|||||||
|
|
||||||
echo "[debian/package] Building Debian package..."
|
echo "[debian/package] Building Debian package..."
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
|
||||||
|
BUILD_ROOT="/tmp/package-manager-debian-build"
|
||||||
|
rm -rf "${BUILD_ROOT}"
|
||||||
|
mkdir -p "${BUILD_ROOT}"
|
||||||
|
|
||||||
|
echo "[debian/package] Syncing project sources to ${BUILD_ROOT}..."
|
||||||
|
rsync -a \
|
||||||
|
--exclude 'packaging/debian' \
|
||||||
|
"${PROJECT_ROOT}/" "${BUILD_ROOT}/"
|
||||||
|
|
||||||
|
echo "[debian/package] Overlaying debian/ metadata from packaging/debian..."
|
||||||
|
mkdir -p "${BUILD_ROOT}/debian"
|
||||||
|
cp -a "${PROJECT_ROOT}/packaging/debian/." "${BUILD_ROOT}/debian/"
|
||||||
|
|
||||||
|
cd "${BUILD_ROOT}"
|
||||||
|
|
||||||
|
echo "[debian/package] Running dpkg-buildpackage..."
|
||||||
dpkg-buildpackage -us -uc -b
|
dpkg-buildpackage -us -uc -b
|
||||||
|
|
||||||
echo "[debian/package] Installing generated DEB package..."
|
echo "[debian/package] Installing generated DEB package..."
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ set -euo pipefail
|
|||||||
echo "[fedora/package] Setting up rpmbuild directories..."
|
echo "[fedora/package] Setting up rpmbuild directories..."
|
||||||
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
mkdir -p /root/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
SPEC_PATH="${PROJECT_ROOT}/packaging/fedora/package-manager.spec"
|
||||||
|
|
||||||
|
if [[ ! -f "${SPEC_PATH}" ]]; then
|
||||||
|
echo "[fedora/package] ERROR: SPEC file not found: ${SPEC_PATH}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[fedora/package] Extracting version from package-manager.spec..."
|
echo "[fedora/package] Extracting version from package-manager.spec..."
|
||||||
version="$(grep -E '^Version:' package-manager.spec | awk '{print $2}')"
|
version="$(grep -E '^Version:' "${SPEC_PATH}" | awk '{print $2}')"
|
||||||
if [[ -z "${version}" ]]; then
|
if [[ -z "${version}" ]]; then
|
||||||
echo "ERROR: Version missing!"
|
echo "ERROR: Version missing!"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -15,13 +24,13 @@ srcdir="package-manager-${version}"
|
|||||||
echo "[fedora/package] Preparing source tree: ${srcdir}"
|
echo "[fedora/package] Preparing source tree: ${srcdir}"
|
||||||
rm -rf "/tmp/${srcdir}"
|
rm -rf "/tmp/${srcdir}"
|
||||||
mkdir -p "/tmp/${srcdir}"
|
mkdir -p "/tmp/${srcdir}"
|
||||||
cp -a . "/tmp/${srcdir}/"
|
cp -a "${PROJECT_ROOT}/." "/tmp/${srcdir}/"
|
||||||
|
|
||||||
echo "[fedora/package] Creating source tarball..."
|
echo "[fedora/package] Creating source tarball..."
|
||||||
tar czf "/root/rpmbuild/SOURCES/${srcdir}.tar.gz" -C /tmp "${srcdir}"
|
tar czf "/root/rpmbuild/SOURCES/${srcdir}.tar.gz" -C /tmp "${srcdir}"
|
||||||
|
|
||||||
echo "[fedora/package] Copying SPEC..."
|
echo "[fedora/package] Copying SPEC..."
|
||||||
cp package-manager.spec /root/rpmbuild/SPECS/
|
cp "${SPEC_PATH}" /root/rpmbuild/SPECS/
|
||||||
|
|
||||||
echo "[fedora/package] Running rpmbuild..."
|
echo "[fedora/package] Running rpmbuild..."
|
||||||
cd /root/rpmbuild/SPECS
|
cd /root/rpmbuild/SPECS
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ source "${SCRIPT_DIR}/lib.sh"
|
|||||||
|
|
||||||
OS_ID="$(detect_os_id)"
|
OS_ID="$(detect_os_id)"
|
||||||
|
|
||||||
|
# Map Manjaro to Arch
|
||||||
|
if [[ "${OS_ID}" == "manjaro" ]]; then
|
||||||
|
echo "[run-package] Mapping OS 'manjaro' → 'arch'"
|
||||||
|
OS_ID="arch"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[run-package] Detected OS: ${OS_ID}"
|
echo "[run-package] Detected OS: ${OS_ID}"
|
||||||
|
|
||||||
case "${OS_ID}" in
|
case "${OS_ID}" in
|
||||||
|
|||||||
@@ -3,6 +3,25 @@ set -euo pipefail
|
|||||||
|
|
||||||
echo "[ubuntu/package] Building Ubuntu (Debian-style) package..."
|
echo "[ubuntu/package] Building Ubuntu (Debian-style) package..."
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
||||||
|
|
||||||
|
BUILD_ROOT="/tmp/package-manager-ubuntu-build"
|
||||||
|
rm -rf "${BUILD_ROOT}"
|
||||||
|
mkdir -p "${BUILD_ROOT}"
|
||||||
|
|
||||||
|
echo "[ubuntu/package] Syncing project sources to ${BUILD_ROOT}..."
|
||||||
|
rsync -a \
|
||||||
|
--exclude 'packaging/debian' \
|
||||||
|
"${PROJECT_ROOT}/" "${BUILD_ROOT}/"
|
||||||
|
|
||||||
|
echo "[ubuntu/package] Overlaying debian/ metadata from packaging/debian..."
|
||||||
|
mkdir -p "${BUILD_ROOT}/debian"
|
||||||
|
cp -a "${PROJECT_ROOT}/packaging/debian/." "${BUILD_ROOT}/debian/"
|
||||||
|
|
||||||
|
cd "${BUILD_ROOT}"
|
||||||
|
|
||||||
|
echo "[ubuntu/package] Running dpkg-buildpackage..."
|
||||||
dpkg-buildpackage -us -uc -b
|
dpkg-buildpackage -us -uc -b
|
||||||
|
|
||||||
echo "[ubuntu/package] Installing generated DEB package..."
|
echo "[ubuntu/package] Installing generated DEB package..."
|
||||||
|
|||||||
@@ -8,19 +8,18 @@ fi
|
|||||||
|
|
||||||
FLAKE_DIR="/usr/lib/package-manager"
|
FLAKE_DIR="/usr/lib/package-manager"
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Try to ensure that "nix" is on PATH
|
# Try to ensure that "nix" is on PATH (common locations + container user)
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
# Common locations for Nix installations
|
|
||||||
CANDIDATES=(
|
CANDIDATES=(
|
||||||
"/nix/var/nix/profiles/default/bin/nix"
|
"/nix/var/nix/profiles/default/bin/nix"
|
||||||
"${HOME:-/root}/.nix-profile/bin/nix"
|
"${HOME:-/root}/.nix-profile/bin/nix"
|
||||||
|
"/home/nix/.nix-profile/bin/nix"
|
||||||
)
|
)
|
||||||
|
|
||||||
for candidate in "${CANDIDATES[@]}"; do
|
for candidate in "${CANDIDATES[@]}"; do
|
||||||
if [[ -x "$candidate" ]]; then
|
if [[ -x "$candidate" ]]; then
|
||||||
# Prepend the directory of the candidate to PATH
|
|
||||||
PATH="$(dirname "$candidate"):${PATH}"
|
PATH="$(dirname "$candidate"):${PATH}"
|
||||||
export PATH
|
export PATH
|
||||||
break
|
break
|
||||||
@@ -28,13 +27,22 @@ if ! command -v nix >/dev/null 2>&1; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Primary (and only) path: use Nix flake if available
|
# If nix is still missing, try to run init-nix.sh once
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
|
if [[ -x "${FLAKE_DIR}/init-nix.sh" ]]; then
|
||||||
|
"${FLAKE_DIR}/init-nix.sh" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Primary path: use Nix flake if available
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
if command -v nix >/dev/null 2>&1; then
|
if command -v nix >/dev/null 2>&1; then
|
||||||
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH."
|
echo "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH after init."
|
||||||
echo "[pkgmgr-wrapper] Nix is required to run pkgmgr (no Python fallback)."
|
echo "[pkgmgr-wrapper] Nix is required to run pkgmgr (no Python fallback)."
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -1,41 +1,32 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "============================================================"
|
IMAGE="package-manager-test-$distro"
|
||||||
echo ">>> Running sanity test: verifying test containers start"
|
|
||||||
echo "============================================================"
|
|
||||||
|
|
||||||
for distro in $DISTROS; do
|
|
||||||
IMAGE="package-manager-test-$distro"
|
|
||||||
|
|
||||||
echo
|
|
||||||
echo "------------------------------------------------------------"
|
|
||||||
echo ">>> Testing container: $IMAGE"
|
|
||||||
echo "------------------------------------------------------------"
|
|
||||||
|
|
||||||
echo "[test-container] Running: docker run --rm --entrypoint pkgmgr $IMAGE --help"
|
|
||||||
echo
|
|
||||||
|
|
||||||
# Run the command and capture the output
|
|
||||||
if OUTPUT=$(docker run --rm \
|
|
||||||
-e PKGMGR_DEV=1 \
|
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
|
||||||
-v "$(pwd):/src" \
|
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
|
||||||
"$IMAGE" 2>&1); then
|
|
||||||
echo "$OUTPUT"
|
|
||||||
echo
|
|
||||||
echo "[test-container] SUCCESS: $IMAGE responded to 'pkgmgr --help'"
|
|
||||||
|
|
||||||
else
|
|
||||||
echo "$OUTPUT"
|
|
||||||
echo
|
|
||||||
echo "[test-container] ERROR: $IMAGE failed to run 'pkgmgr --help'"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "============================================================"
|
echo "------------------------------------------------------------"
|
||||||
echo ">>> All containers passed the sanity check"
|
echo ">>> Testing container: $IMAGE"
|
||||||
echo "============================================================"
|
echo "------------------------------------------------------------"
|
||||||
|
echo "[test-container] Inspect image metadata:"
|
||||||
|
docker image inspect "$IMAGE" | sed -n '1,40p'
|
||||||
|
|
||||||
|
echo "[test-container] Running: docker run --rm --entrypoint pkgmgr $IMAGE --help"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Run the command and capture the output
|
||||||
|
if OUTPUT=$(docker run --rm \
|
||||||
|
-e PKGMGR_DEV=1 \
|
||||||
|
-v pkgmgr_nix_store_${distro}:/nix \
|
||||||
|
-v "$(pwd):/src" \
|
||||||
|
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
||||||
|
"$IMAGE" 2>&1); then
|
||||||
|
echo "$OUTPUT"
|
||||||
|
echo
|
||||||
|
echo "[test-container] SUCCESS: $IMAGE responded to 'pkgmgr --help'"
|
||||||
|
|
||||||
|
else
|
||||||
|
echo "$OUTPUT"
|
||||||
|
echo
|
||||||
|
echo "[test-container] ERROR: $IMAGE failed to run 'pkgmgr --help'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -1,55 +1,60 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo ">>> Running E2E tests in all distros: $DISTROS"
|
echo "============================================================"
|
||||||
|
echo ">>> Running E2E tests: $distro"
|
||||||
|
echo "============================================================"
|
||||||
|
|
||||||
for distro in $DISTROS; do
|
docker run --rm \
|
||||||
echo "============================================================"
|
-v "$(pwd):/src" \
|
||||||
echo ">>> Running E2E tests: $distro"
|
-v "pkgmgr_nix_store_${distro}:/nix" \
|
||||||
echo "============================================================"
|
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
||||||
|
-e PKGMGR_DEV=1 \
|
||||||
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
|
--workdir /src \
|
||||||
|
"package-manager-test-${distro}" \
|
||||||
|
bash -lc '
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
docker run --rm \
|
# Load distro info
|
||||||
-v "$(pwd):/src" \
|
if [ -f /etc/os-release ]; then
|
||||||
-v pkgmgr_nix_store_${distro}:/nix \
|
. /etc/os-release
|
||||||
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
|
fi
|
||||||
-e PKGMGR_DEV=1 \
|
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
|
||||||
--workdir /src \
|
|
||||||
--entrypoint bash \
|
|
||||||
"package-manager-test-$distro" \
|
|
||||||
-c '
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Load distro info
|
echo "Running tests inside distro: ${ID:-unknown}"
|
||||||
if [ -f /etc/os-release ]; then
|
|
||||||
. /etc/os-release
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Running tests inside distro: $ID"
|
# Load Nix environment if available
|
||||||
|
if [ -f "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" ]; then
|
||||||
|
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
# Load nix environment if available
|
if [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
||||||
if [ -f "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh" ]; then
|
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
||||||
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
|
PATH="/nix/var/nix/profiles/default/bin:$HOME/.nix-profile/bin:$PATH"
|
||||||
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
|
|
||||||
fi
|
|
||||||
|
|
||||||
PATH="/nix/var/nix/profiles/default/bin:$HOME/.nix-profile/bin:$PATH"
|
command -v nix >/dev/null || {
|
||||||
|
echo "ERROR: nix not found."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
command -v nix >/dev/null || {
|
# Mark the mounted repository as safe to avoid Git ownership errors.
|
||||||
echo "ERROR: nix not found."
|
# Newer Git (e.g. on Ubuntu) complains about the gitdir (/src/.git),
|
||||||
exit 1
|
# older versions about the worktree (/src). Nix turns "." into the
|
||||||
}
|
# flake input "git+file:///src", which then uses Git under the hood.
|
||||||
|
if command -v git >/dev/null 2>&1; then
|
||||||
# Mark the mounted repository as safe to avoid Git ownership errors
|
# Worktree path
|
||||||
git config --global --add safe.directory /src || true
|
git config --global --add safe.directory /src || true
|
||||||
|
# Gitdir path shown in the "dubious ownership" error
|
||||||
|
git config --global --add safe.directory /src/.git || true
|
||||||
|
# Ephemeral CI containers: allow all paths as a last resort
|
||||||
|
git config --global --add safe.directory '*' || true
|
||||||
|
fi
|
||||||
|
|
||||||
# Run the E2E tests inside the Nix development shell
|
# Run the E2E tests inside the Nix development shell
|
||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python3 -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s /src/tests/e2e \
|
-s /src/tests/e2e \
|
||||||
-p "$TEST_PATTERN"
|
-p "$TEST_PATTERN"
|
||||||
'
|
'
|
||||||
done
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
: "${distro:=arch}"
|
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running INTEGRATION tests in ${distro} container"
|
echo ">>> Running INTEGRATION tests in ${distro} container"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
@@ -14,13 +12,12 @@ docker run --rm \
|
|||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e PKGMGR_DEV=1 \
|
-e PKGMGR_DEV=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--entrypoint bash \
|
|
||||||
"package-manager-test-${distro}" \
|
"package-manager-test-${distro}" \
|
||||||
-c '
|
bash -lc '
|
||||||
set -e;
|
set -e;
|
||||||
git config --global --add safe.directory /src || true;
|
git config --global --add safe.directory /src || true;
|
||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s tests/integration \
|
-s tests/integration \
|
||||||
-t /src \
|
-t /src \
|
||||||
-p "$TEST_PATTERN";
|
-p "$TEST_PATTERN";
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
: "${distro:=arch}"
|
|
||||||
|
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ">>> Running UNIT tests in ${distro} container"
|
echo ">>> Running UNIT tests in ${distro} container"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
@@ -14,13 +12,12 @@ docker run --rm \
|
|||||||
--workdir /src \
|
--workdir /src \
|
||||||
-e PKGMGR_DEV=1 \
|
-e PKGMGR_DEV=1 \
|
||||||
-e TEST_PATTERN="${TEST_PATTERN}" \
|
-e TEST_PATTERN="${TEST_PATTERN}" \
|
||||||
--entrypoint bash \
|
|
||||||
"package-manager-test-${distro}" \
|
"package-manager-test-${distro}" \
|
||||||
-c '
|
bash -lc '
|
||||||
set -e;
|
set -e;
|
||||||
git config --global --add safe.directory /src || true;
|
git config --global --add safe.directory /src || true;
|
||||||
nix develop .#default --no-write-lock-file -c \
|
nix develop .#default --no-write-lock-file -c \
|
||||||
python -m unittest discover \
|
python3 -m unittest discover \
|
||||||
-s tests/unit \
|
-s tests/unit \
|
||||||
-t /src \
|
-t /src \
|
||||||
-p "$TEST_PATTERN";
|
-p "$TEST_PATTERN";
|
||||||
|
|||||||
36
src/pkgmgr/__init__.py
Normal file
36
src/pkgmgr/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Top-level pkgmgr package.
|
||||||
|
|
||||||
|
We deliberately avoid importing heavy submodules (like the CLI)
|
||||||
|
on import to keep unit tests fast and to not require optional
|
||||||
|
dependencies (like PyYAML) unless they are actually used.
|
||||||
|
|
||||||
|
Accessing ``pkgmgr.cli`` will load the CLI module lazily via
|
||||||
|
``__getattr__``. This keeps patterns like
|
||||||
|
|
||||||
|
from pkgmgr import cli
|
||||||
|
|
||||||
|
working as expected in tests and entry points.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
__all__ = ["cli"]
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
"""
|
||||||
|
Lazily expose ``pkgmgr.cli`` as attribute on the top-level package.
|
||||||
|
|
||||||
|
This keeps ``import pkgmgr`` lightweight while still allowing
|
||||||
|
``from pkgmgr import cli`` in tests and entry points.
|
||||||
|
"""
|
||||||
|
if name == "cli":
|
||||||
|
return import_module("pkgmgr.cli")
|
||||||
|
raise AttributeError(f"module 'pkgmgr' has no attribute {name!r}")
|
||||||
@@ -139,22 +139,27 @@ class NixFlakeInstaller(BaseInstaller):
|
|||||||
|
|
||||||
for output, allow_failure in outputs:
|
for output, allow_failure in outputs:
|
||||||
cmd = f"nix profile install {ctx.repo_dir}#{output}"
|
cmd = f"nix profile install {ctx.repo_dir}#{output}"
|
||||||
|
print(f"[INFO] Running: {cmd}")
|
||||||
|
ret = os.system(cmd)
|
||||||
|
|
||||||
try:
|
# Extract real exit code from os.system() result
|
||||||
run_command(
|
if os.WIFEXITED(ret):
|
||||||
cmd,
|
exit_code = os.WEXITSTATUS(ret)
|
||||||
cwd=ctx.repo_dir,
|
else:
|
||||||
preview=ctx.preview,
|
# abnormal termination (signal etc.) – keep raw value
|
||||||
allow_failure=allow_failure,
|
exit_code = ret
|
||||||
)
|
|
||||||
|
if exit_code == 0:
|
||||||
print(f"Nix flake output '{output}' successfully installed.")
|
print(f"Nix flake output '{output}' successfully installed.")
|
||||||
except SystemExit as e:
|
continue
|
||||||
print(f"[Error] Failed to install Nix flake output '{output}': {e}")
|
|
||||||
if not allow_failure:
|
print(f"[Error] Failed to install Nix flake output '{output}'")
|
||||||
# Mandatory output failed → fatal for the pipeline.
|
print(f"[Error] Command exited with code {exit_code}")
|
||||||
raise
|
|
||||||
# Optional output failed → log and continue.
|
if not allow_failure:
|
||||||
print(
|
raise SystemExit(exit_code)
|
||||||
"[Warning] Continuing despite failure to install "
|
|
||||||
f"optional output '{output}'."
|
print(
|
||||||
)
|
"[Warning] Continuing despite failure to install "
|
||||||
|
f"optional output '{output}'."
|
||||||
|
)
|
||||||
26
src/pkgmgr/actions/mirror/__init__.py
Normal file
26
src/pkgmgr/actions/mirror/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
"""
|
||||||
|
High-level mirror actions.
|
||||||
|
|
||||||
|
Public API:
|
||||||
|
- list_mirrors
|
||||||
|
- diff_mirrors
|
||||||
|
- merge_mirrors
|
||||||
|
- setup_mirrors
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .types import Repository, MirrorMap
|
||||||
|
from .list_cmd import list_mirrors
|
||||||
|
from .diff_cmd import diff_mirrors
|
||||||
|
from .merge_cmd import merge_mirrors
|
||||||
|
from .setup_cmd import setup_mirrors
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Repository",
|
||||||
|
"MirrorMap",
|
||||||
|
"list_mirrors",
|
||||||
|
"diff_mirrors",
|
||||||
|
"merge_mirrors",
|
||||||
|
"setup_mirrors",
|
||||||
|
]
|
||||||
31
src/pkgmgr/actions/mirror/context.py
Normal file
31
src/pkgmgr/actions/mirror/context.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pkgmgr.core.repository.dir import get_repo_dir
|
||||||
|
from pkgmgr.core.repository.identifier import get_repo_identifier
|
||||||
|
|
||||||
|
from .io import load_config_mirrors, read_mirrors_file
|
||||||
|
from .types import MirrorMap, RepoMirrorContext, Repository
|
||||||
|
|
||||||
|
|
||||||
|
def build_context(
|
||||||
|
repo: Repository,
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
) -> RepoMirrorContext:
|
||||||
|
"""
|
||||||
|
Build a RepoMirrorContext for a single repository.
|
||||||
|
"""
|
||||||
|
identifier = get_repo_identifier(repo, all_repos)
|
||||||
|
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||||
|
|
||||||
|
config_mirrors: MirrorMap = load_config_mirrors(repo)
|
||||||
|
file_mirrors: MirrorMap = read_mirrors_file(repo_dir)
|
||||||
|
|
||||||
|
return RepoMirrorContext(
|
||||||
|
identifier=identifier,
|
||||||
|
repo_dir=repo_dir,
|
||||||
|
config_mirrors=config_mirrors,
|
||||||
|
file_mirrors=file_mirrors,
|
||||||
|
)
|
||||||
60
src/pkgmgr/actions/mirror/diff_cmd.py
Normal file
60
src/pkgmgr/actions/mirror/diff_cmd.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .context import build_context
|
||||||
|
from .printing import print_header
|
||||||
|
from .types import Repository
|
||||||
|
|
||||||
|
|
||||||
|
def diff_mirrors(
|
||||||
|
selected_repos: List[Repository],
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Show differences between config mirrors and MIRRORS file.
|
||||||
|
|
||||||
|
- Mirrors present only in config are reported as "ONLY IN CONFIG".
|
||||||
|
- Mirrors present only in MIRRORS file are reported as "ONLY IN FILE".
|
||||||
|
- Mirrors with same name but different URLs are reported as "URL MISMATCH".
|
||||||
|
"""
|
||||||
|
for repo in selected_repos:
|
||||||
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
|
|
||||||
|
print_header("[MIRROR DIFF]", ctx)
|
||||||
|
|
||||||
|
config_m = ctx.config_mirrors
|
||||||
|
file_m = ctx.file_mirrors
|
||||||
|
|
||||||
|
if not config_m and not file_m:
|
||||||
|
print(" No mirrors configured in config or MIRRORS file.")
|
||||||
|
print()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mirrors only in config
|
||||||
|
for name, url in sorted(config_m.items()):
|
||||||
|
if name not in file_m:
|
||||||
|
print(f" [ONLY IN CONFIG] {name}: {url}")
|
||||||
|
|
||||||
|
# Mirrors only in MIRRORS file
|
||||||
|
for name, url in sorted(file_m.items()):
|
||||||
|
if name not in config_m:
|
||||||
|
print(f" [ONLY IN FILE] {name}: {url}")
|
||||||
|
|
||||||
|
# Mirrors with same name but different URLs
|
||||||
|
shared = set(config_m) & set(file_m)
|
||||||
|
for name in sorted(shared):
|
||||||
|
url_cfg = config_m.get(name)
|
||||||
|
url_file = file_m.get(name)
|
||||||
|
if url_cfg != url_file:
|
||||||
|
print(
|
||||||
|
f" [URL MISMATCH] {name}:\n"
|
||||||
|
f" config: {url_cfg}\n"
|
||||||
|
f" file: {url_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if config_m and file_m and config_m == file_m:
|
||||||
|
print(" [OK] Mirrors in config and MIRRORS file are in sync.")
|
||||||
|
|
||||||
|
print()
|
||||||
179
src/pkgmgr/actions/mirror/git_remote.py
Normal file
179
src/pkgmgr/actions/mirror/git_remote.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
|
from pkgmgr.core.command.run import run_command
|
||||||
|
from pkgmgr.core.git import GitError, run_git
|
||||||
|
|
||||||
|
from .types import MirrorMap, RepoMirrorContext, Repository
|
||||||
|
|
||||||
|
|
||||||
|
def build_default_ssh_url(repo: Repository) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Build a simple SSH URL from repo config if no explicit mirror is defined.
|
||||||
|
|
||||||
|
Example: git@github.com:account/repository.git
|
||||||
|
"""
|
||||||
|
provider = repo.get("provider")
|
||||||
|
account = repo.get("account")
|
||||||
|
name = repo.get("repository")
|
||||||
|
port = repo.get("port")
|
||||||
|
|
||||||
|
if not provider or not account or not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
provider = str(provider)
|
||||||
|
account = str(account)
|
||||||
|
name = str(name)
|
||||||
|
|
||||||
|
if port:
|
||||||
|
return f"ssh://git@{provider}:{port}/{account}/{name}.git"
|
||||||
|
|
||||||
|
# GitHub-style shorthand
|
||||||
|
return f"git@{provider}:{account}/{name}.git"
|
||||||
|
|
||||||
|
|
||||||
|
def determine_primary_remote_url(
|
||||||
|
repo: Repository,
|
||||||
|
resolved_mirrors: MirrorMap,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Determine the primary remote URL in a consistent way:
|
||||||
|
|
||||||
|
1. resolved_mirrors["origin"]
|
||||||
|
2. any resolved mirror (first by name)
|
||||||
|
3. default SSH URL from provider/account/repository
|
||||||
|
"""
|
||||||
|
if "origin" in resolved_mirrors:
|
||||||
|
return resolved_mirrors["origin"]
|
||||||
|
|
||||||
|
if resolved_mirrors:
|
||||||
|
first_name = sorted(resolved_mirrors.keys())[0]
|
||||||
|
return resolved_mirrors[first_name]
|
||||||
|
|
||||||
|
return build_default_ssh_url(repo)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_git_output(args: List[str], cwd: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Run a Git command via run_git and return its stdout, or None on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return run_git(args, cwd=cwd)
|
||||||
|
except GitError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def current_origin_url(repo_dir: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Return the current URL for remote 'origin', or None if not present.
|
||||||
|
"""
|
||||||
|
output = _safe_git_output(["remote", "get-url", "origin"], cwd=repo_dir)
|
||||||
|
if not output:
|
||||||
|
return None
|
||||||
|
url = output.strip()
|
||||||
|
return url or None
|
||||||
|
|
||||||
|
|
||||||
|
def has_origin_remote(repo_dir: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether a remote called 'origin' exists in the repository.
|
||||||
|
"""
|
||||||
|
output = _safe_git_output(["remote"], cwd=repo_dir)
|
||||||
|
if not output:
|
||||||
|
return False
|
||||||
|
names = output.split()
|
||||||
|
return "origin" in names
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_push_urls_for_origin(
|
||||||
|
repo_dir: str,
|
||||||
|
mirrors: MirrorMap,
|
||||||
|
preview: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that all mirror URLs are present as push URLs on 'origin'.
|
||||||
|
"""
|
||||||
|
desired: Set[str] = {url for url in mirrors.values() if url}
|
||||||
|
if not desired:
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_output = _safe_git_output(
|
||||||
|
["remote", "get-url", "--push", "--all", "origin"],
|
||||||
|
cwd=repo_dir,
|
||||||
|
)
|
||||||
|
existing = set(existing_output.splitlines()) if existing_output else set()
|
||||||
|
|
||||||
|
missing = sorted(desired - existing)
|
||||||
|
for url in missing:
|
||||||
|
cmd = f"git remote set-url --add --push origin {url}"
|
||||||
|
if preview:
|
||||||
|
print(f"[PREVIEW] Would run in {repo_dir!r}: {cmd}")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Adding push URL to 'origin': {url}")
|
||||||
|
run_command(cmd, cwd=repo_dir, preview=False)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_origin_remote(
|
||||||
|
repo: Repository,
|
||||||
|
ctx: RepoMirrorContext,
|
||||||
|
preview: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that a usable 'origin' remote exists and has all push URLs.
|
||||||
|
"""
|
||||||
|
repo_dir = ctx.repo_dir
|
||||||
|
resolved_mirrors = ctx.resolved_mirrors
|
||||||
|
|
||||||
|
if not os.path.isdir(os.path.join(repo_dir, ".git")):
|
||||||
|
print(f"[WARN] {repo_dir} is not a Git repository (no .git directory).")
|
||||||
|
return
|
||||||
|
|
||||||
|
url = determine_primary_remote_url(repo, resolved_mirrors)
|
||||||
|
|
||||||
|
if not has_origin_remote(repo_dir):
|
||||||
|
if not url:
|
||||||
|
print(
|
||||||
|
"[WARN] Could not determine URL for 'origin' remote. "
|
||||||
|
"Please configure mirrors or provider/account/repository."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = f"git remote add origin {url}"
|
||||||
|
if preview:
|
||||||
|
print(f"[PREVIEW] Would run in {repo_dir!r}: {cmd}")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Adding 'origin' remote in {repo_dir}: {url}")
|
||||||
|
run_command(cmd, cwd=repo_dir, preview=False)
|
||||||
|
else:
|
||||||
|
current = current_origin_url(repo_dir)
|
||||||
|
if current == url or not url:
|
||||||
|
print(
|
||||||
|
f"[INFO] 'origin' already points to "
|
||||||
|
f"{current or '<unknown>'} (no change needed)."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# We do not auto-change origin here, only log the mismatch.
|
||||||
|
print(
|
||||||
|
"[INFO] 'origin' exists with URL "
|
||||||
|
f"{current or '<unknown>'}; not changing to {url}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure all mirrors are present as push URLs
|
||||||
|
_ensure_push_urls_for_origin(repo_dir, resolved_mirrors, preview)
|
||||||
|
|
||||||
|
|
||||||
|
def is_remote_reachable(url: str, cwd: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether a remote repository is reachable via `git ls-remote`.
|
||||||
|
|
||||||
|
This does NOT modify anything; it only probes the remote.
|
||||||
|
"""
|
||||||
|
workdir = cwd or os.getcwd()
|
||||||
|
try:
|
||||||
|
# --exit-code → non-zero exit code if the remote does not exist
|
||||||
|
run_git(["ls-remote", "--exit-code", url], cwd=workdir)
|
||||||
|
return True
|
||||||
|
except GitError:
|
||||||
|
return False
|
||||||
98
src/pkgmgr/actions/mirror/io.py
Normal file
98
src/pkgmgr/actions/mirror/io.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from typing import List, Mapping
|
||||||
|
|
||||||
|
from .types import MirrorMap, Repository
|
||||||
|
|
||||||
|
|
||||||
|
def load_config_mirrors(repo: Repository) -> MirrorMap:
|
||||||
|
mirrors = repo.get("mirrors") or {}
|
||||||
|
result: MirrorMap = {}
|
||||||
|
|
||||||
|
if isinstance(mirrors, dict):
|
||||||
|
for name, url in mirrors.items():
|
||||||
|
if url:
|
||||||
|
result[str(name)] = str(url)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if isinstance(mirrors, list):
|
||||||
|
for entry in mirrors:
|
||||||
|
if isinstance(entry, dict):
|
||||||
|
name = entry.get("name")
|
||||||
|
url = entry.get("url")
|
||||||
|
if name and url:
|
||||||
|
result[str(name)] = str(url)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
||||||
|
"""
|
||||||
|
Supports:
|
||||||
|
NAME URL
|
||||||
|
URL → auto name = hostname
|
||||||
|
"""
|
||||||
|
path = os.path.join(repo_dir, filename)
|
||||||
|
mirrors: MirrorMap = {}
|
||||||
|
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return mirrors
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
for line in fh:
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
parts = stripped.split(None, 1)
|
||||||
|
|
||||||
|
# Case 1: "name url"
|
||||||
|
if len(parts) == 2:
|
||||||
|
name, url = parts
|
||||||
|
# Case 2: "url" → auto-generate name
|
||||||
|
elif len(parts) == 1:
|
||||||
|
url = parts[0]
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = (parsed.netloc or "").split(":")[0]
|
||||||
|
base = host or "mirror"
|
||||||
|
name = base
|
||||||
|
i = 2
|
||||||
|
while name in mirrors:
|
||||||
|
name = f"{base}{i}"
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
mirrors[name] = url
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"[WARN] Could not read MIRRORS file at {path}: {exc}")
|
||||||
|
|
||||||
|
return mirrors
|
||||||
|
|
||||||
|
|
||||||
|
def write_mirrors_file(
|
||||||
|
repo_dir: str,
|
||||||
|
mirrors: Mapping[str, str],
|
||||||
|
filename: str = "MIRRORS",
|
||||||
|
preview: bool = False,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
path = os.path.join(repo_dir, filename)
|
||||||
|
lines = [f"{name} {url}" for name, url in sorted(mirrors.items())]
|
||||||
|
content = "\n".join(lines) + ("\n" if lines else "")
|
||||||
|
|
||||||
|
if preview:
|
||||||
|
print(f"[PREVIEW] Would write MIRRORS file at {path}:")
|
||||||
|
print(content or "(empty)")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(content)
|
||||||
|
print(f"[INFO] Wrote MIRRORS file at {path}")
|
||||||
|
except OSError as exc:
|
||||||
|
print(f"[ERROR] Failed to write MIRRORS file at {path}: {exc}")
|
||||||
46
src/pkgmgr/actions/mirror/list_cmd.py
Normal file
46
src/pkgmgr/actions/mirror/list_cmd.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .context import build_context
|
||||||
|
from .printing import print_header, print_named_mirrors
|
||||||
|
from .types import Repository
|
||||||
|
|
||||||
|
|
||||||
|
def list_mirrors(
|
||||||
|
selected_repos: List[Repository],
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
source: str = "all",
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
List mirrors for the selected repositories.
|
||||||
|
|
||||||
|
source:
|
||||||
|
- "config" → only mirrors from configuration
|
||||||
|
- "file" → only mirrors from MIRRORS file
|
||||||
|
- "resolved" → merged view (config + file, file wins)
|
||||||
|
- "all" → show config + file + resolved
|
||||||
|
"""
|
||||||
|
for repo in selected_repos:
|
||||||
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
|
resolved_m = ctx.resolved_mirrors
|
||||||
|
|
||||||
|
print_header("[MIRROR]", ctx)
|
||||||
|
|
||||||
|
if source in ("config", "all"):
|
||||||
|
print_named_mirrors("config mirrors", ctx.config_mirrors)
|
||||||
|
if source == "config":
|
||||||
|
print()
|
||||||
|
continue # next repo
|
||||||
|
|
||||||
|
if source in ("file", "all"):
|
||||||
|
print_named_mirrors("MIRRORS file", ctx.file_mirrors)
|
||||||
|
if source == "file":
|
||||||
|
print()
|
||||||
|
continue # next repo
|
||||||
|
|
||||||
|
if source in ("resolved", "all"):
|
||||||
|
print_named_mirrors("resolved mirrors", resolved_m)
|
||||||
|
|
||||||
|
print()
|
||||||
162
src/pkgmgr/actions/mirror/merge_cmd.py
Normal file
162
src/pkgmgr/actions/mirror/merge_cmd.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict, List, Tuple, Optional
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from pkgmgr.core.config.save import save_user_config
|
||||||
|
|
||||||
|
from .context import build_context
|
||||||
|
from .io import write_mirrors_file
|
||||||
|
from .types import MirrorMap, Repository
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _repo_key(repo: Repository) -> Tuple[str, str, str]:
|
||||||
|
"""
|
||||||
|
Normalised key for identifying a repository in config files.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
str(repo.get("provider", "")),
|
||||||
|
str(repo.get("account", "")),
|
||||||
|
str(repo.get("repository", "")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_user_config(path: str) -> Dict[str, object]:
|
||||||
|
"""
|
||||||
|
Load a user config YAML file as dict.
|
||||||
|
Non-dicts yield {}.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Main merge command
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def merge_mirrors(
|
||||||
|
selected_repos: List[Repository],
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
source: str,
|
||||||
|
target: str,
|
||||||
|
preview: bool = False,
|
||||||
|
user_config_path: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Merge mirrors between config and MIRRORS file.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- source, target ∈ {"config", "file"}.
|
||||||
|
- merged = (target_mirrors overridden by source_mirrors)
|
||||||
|
- If target == "file" → write MIRRORS file.
|
||||||
|
- If target == "config":
|
||||||
|
* update the user config YAML directly
|
||||||
|
* write it using save_user_config()
|
||||||
|
|
||||||
|
The merge strategy is:
|
||||||
|
dst + src (src wins on same name)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Load user config once if we intend to write to it.
|
||||||
|
user_cfg: Optional[Dict[str, object]] = None
|
||||||
|
user_cfg_path_expanded: Optional[str] = None
|
||||||
|
|
||||||
|
if target == "config" and user_config_path and not preview:
|
||||||
|
user_cfg_path_expanded = os.path.expanduser(user_config_path)
|
||||||
|
user_cfg = _load_user_config(user_cfg_path_expanded)
|
||||||
|
if not isinstance(user_cfg.get("repositories"), list):
|
||||||
|
user_cfg["repositories"] = []
|
||||||
|
|
||||||
|
for repo in selected_repos:
|
||||||
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
|
|
||||||
|
print("============================================================")
|
||||||
|
print(f"[MIRROR MERGE] Repository: {ctx.identifier}")
|
||||||
|
print(f"[MIRROR MERGE] Directory: {ctx.repo_dir}")
|
||||||
|
print(f"[MIRROR MERGE] {source} → {target}")
|
||||||
|
print("============================================================")
|
||||||
|
|
||||||
|
# Pick the correct source/target maps
|
||||||
|
if source == "config":
|
||||||
|
src = ctx.config_mirrors
|
||||||
|
dst = ctx.file_mirrors
|
||||||
|
else: # source == "file"
|
||||||
|
src = ctx.file_mirrors
|
||||||
|
dst = ctx.config_mirrors
|
||||||
|
|
||||||
|
# Merge (src overrides dst)
|
||||||
|
merged: MirrorMap = dict(dst)
|
||||||
|
merged.update(src)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# WRITE TO FILE
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if target == "file":
|
||||||
|
write_mirrors_file(ctx.repo_dir, merged, preview=preview)
|
||||||
|
print()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# WRITE TO CONFIG
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
if target == "config":
|
||||||
|
# If preview or no config path → show intended output
|
||||||
|
if preview or not user_cfg:
|
||||||
|
print("[INFO] The following mirrors would be written to config:")
|
||||||
|
if not merged:
|
||||||
|
print(" (no mirrors)")
|
||||||
|
else:
|
||||||
|
for name, url in sorted(merged.items()):
|
||||||
|
print(f" - {name}: {url}")
|
||||||
|
print(" (Config not modified due to preview or missing path.)")
|
||||||
|
print()
|
||||||
|
continue
|
||||||
|
|
||||||
|
repos = user_cfg.get("repositories")
|
||||||
|
target_key = _repo_key(repo)
|
||||||
|
existing_repo: Optional[Repository] = None
|
||||||
|
|
||||||
|
# Find existing repo entry
|
||||||
|
for entry in repos:
|
||||||
|
if isinstance(entry, dict) and _repo_key(entry) == target_key:
|
||||||
|
existing_repo = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
# Create entry if missing
|
||||||
|
if existing_repo is None:
|
||||||
|
existing_repo = {
|
||||||
|
"provider": repo.get("provider"),
|
||||||
|
"account": repo.get("account"),
|
||||||
|
"repository": repo.get("repository"),
|
||||||
|
}
|
||||||
|
repos.append(existing_repo)
|
||||||
|
|
||||||
|
# Write or delete mirrors
|
||||||
|
if merged:
|
||||||
|
existing_repo["mirrors"] = dict(merged)
|
||||||
|
else:
|
||||||
|
existing_repo.pop("mirrors", None)
|
||||||
|
|
||||||
|
print(" [OK] Updated repo['mirrors'] in user config.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# SAVE CONFIG (once at the end)
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
if user_cfg is not None and user_cfg_path_expanded is not None and not preview:
|
||||||
|
save_user_config(user_cfg, user_cfg_path_expanded)
|
||||||
|
print(f"[OK] Saved updated config: {user_cfg_path_expanded}")
|
||||||
35
src/pkgmgr/actions/mirror/printing.py
Normal file
35
src/pkgmgr/actions/mirror/printing.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .types import MirrorMap, RepoMirrorContext
|
||||||
|
|
||||||
|
|
||||||
|
def print_header(
|
||||||
|
title_prefix: str,
|
||||||
|
ctx: RepoMirrorContext,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Print a standard header for mirror-related output.
|
||||||
|
|
||||||
|
title_prefix examples:
|
||||||
|
- "[MIRROR]"
|
||||||
|
- "[MIRROR DIFF]"
|
||||||
|
- "[MIRROR MERGE]"
|
||||||
|
- "[MIRROR SETUP:LOCAL]"
|
||||||
|
- "[MIRROR SETUP:REMOTE]"
|
||||||
|
"""
|
||||||
|
print("============================================================")
|
||||||
|
print(f"{title_prefix} Repository: {ctx.identifier}")
|
||||||
|
print(f"{title_prefix} Directory: {ctx.repo_dir}")
|
||||||
|
print("============================================================")
|
||||||
|
|
||||||
|
|
||||||
|
def print_named_mirrors(label: str, mirrors: MirrorMap) -> None:
|
||||||
|
"""
|
||||||
|
Print a labeled mirror block (e.g. '[config mirrors]').
|
||||||
|
"""
|
||||||
|
print(f" [{label}]")
|
||||||
|
if mirrors:
|
||||||
|
for name, url in sorted(mirrors.items()):
|
||||||
|
print(f" - {name}: {url}")
|
||||||
|
else:
|
||||||
|
print(" (none)")
|
||||||
165
src/pkgmgr/actions/mirror/setup_cmd.py
Normal file
165
src/pkgmgr/actions/mirror/setup_cmd.py
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from pkgmgr.core.git import run_git, GitError
|
||||||
|
|
||||||
|
from .context import build_context
|
||||||
|
from .git_remote import determine_primary_remote_url, ensure_origin_remote
|
||||||
|
from .types import Repository
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_local_mirrors_for_repo(
|
||||||
|
repo: Repository,
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
preview: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure local Git state is sane (currently: 'origin' remote).
|
||||||
|
"""
|
||||||
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
|
|
||||||
|
print("------------------------------------------------------------")
|
||||||
|
print(f"[MIRROR SETUP:LOCAL] {ctx.identifier}")
|
||||||
|
print(f"[MIRROR SETUP:LOCAL] dir: {ctx.repo_dir}")
|
||||||
|
print("------------------------------------------------------------")
|
||||||
|
|
||||||
|
ensure_origin_remote(repo, ctx, preview=preview)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def _probe_mirror(url: str, repo_dir: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Probe a remote mirror by running `git ls-remote <url>`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(True, "") on success,
|
||||||
|
(False, error_message) on failure.
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
- Wir werten ausschließlich den Exit-Code aus.
|
||||||
|
- STDERR kann Hinweise/Warnings enthalten und ist NICHT automatisch ein Fehler.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Wir ignorieren stdout komplett; wichtig ist nur, dass der Befehl ohne
|
||||||
|
# GitError (also Exit-Code 0) durchläuft.
|
||||||
|
run_git(["ls-remote", url], cwd=repo_dir)
|
||||||
|
return True, ""
|
||||||
|
except GitError as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_remote_mirrors_for_repo(
|
||||||
|
repo: Repository,
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
preview: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Remote-side setup / validation.
|
||||||
|
|
||||||
|
Aktuell werden nur **nicht-destruktive Checks** gemacht:
|
||||||
|
|
||||||
|
- Für jeden Mirror (aus config + MIRRORS-Datei, file gewinnt):
|
||||||
|
* `git ls-remote <url>` wird ausgeführt.
|
||||||
|
* Bei Exit-Code 0 → [OK]
|
||||||
|
* Bei Fehler → [WARN] + Details aus der GitError-Exception
|
||||||
|
|
||||||
|
Es werden **keine** Provider-APIs aufgerufen und keine Repos angelegt.
|
||||||
|
"""
|
||||||
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
|
resolved_m = ctx.resolved_mirrors
|
||||||
|
|
||||||
|
print("------------------------------------------------------------")
|
||||||
|
print(f"[MIRROR SETUP:REMOTE] {ctx.identifier}")
|
||||||
|
print(f"[MIRROR SETUP:REMOTE] dir: {ctx.repo_dir}")
|
||||||
|
print("------------------------------------------------------------")
|
||||||
|
|
||||||
|
if not resolved_m:
|
||||||
|
# Optional: Fallback auf eine heuristisch bestimmte URL, falls wir
|
||||||
|
# irgendwann "automatisch anlegen" implementieren wollen.
|
||||||
|
primary_url = determine_primary_remote_url(repo, resolved_m)
|
||||||
|
if not primary_url:
|
||||||
|
print(
|
||||||
|
"[INFO] No mirrors configured (config or MIRRORS file), and no "
|
||||||
|
"primary URL could be derived from provider/account/repository."
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
ok, error_message = _probe_mirror(primary_url, ctx.repo_dir)
|
||||||
|
if ok:
|
||||||
|
print(f"[OK] Remote mirror (primary) is reachable: {primary_url}")
|
||||||
|
else:
|
||||||
|
print("[WARN] Primary remote URL is NOT reachable:")
|
||||||
|
print(f" {primary_url}")
|
||||||
|
if error_message:
|
||||||
|
print(" Details:")
|
||||||
|
for line in error_message.splitlines():
|
||||||
|
print(f" {line}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
"[INFO] Remote checks are non-destructive and only use `git ls-remote` "
|
||||||
|
"to probe mirror URLs."
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Normaler Fall: wir haben benannte Mirrors aus config/MIRRORS
|
||||||
|
for name, url in sorted(resolved_m.items()):
|
||||||
|
ok, error_message = _probe_mirror(url, ctx.repo_dir)
|
||||||
|
if ok:
|
||||||
|
print(f"[OK] Remote mirror '{name}' is reachable: {url}")
|
||||||
|
else:
|
||||||
|
print(f"[WARN] Remote mirror '{name}' is NOT reachable:")
|
||||||
|
print(f" {url}")
|
||||||
|
if error_message:
|
||||||
|
print(" Details:")
|
||||||
|
for line in error_message.splitlines():
|
||||||
|
print(f" {line}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
"[INFO] Remote checks are non-destructive and only use `git ls-remote` "
|
||||||
|
"to probe mirror URLs."
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_mirrors(
|
||||||
|
selected_repos: List[Repository],
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Repository],
|
||||||
|
preview: bool = False,
|
||||||
|
local: bool = True,
|
||||||
|
remote: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Setup mirrors for the selected repositories.
|
||||||
|
|
||||||
|
local:
|
||||||
|
- Configure local Git remotes (currently: ensure 'origin' is present and
|
||||||
|
points to a reasonable URL).
|
||||||
|
|
||||||
|
remote:
|
||||||
|
- Non-destructive remote checks using `git ls-remote` for each mirror URL.
|
||||||
|
Es werden keine Repositories auf dem Provider angelegt.
|
||||||
|
"""
|
||||||
|
for repo in selected_repos:
|
||||||
|
if local:
|
||||||
|
_setup_local_mirrors_for_repo(
|
||||||
|
repo,
|
||||||
|
repositories_base_dir=repositories_base_dir,
|
||||||
|
all_repos=all_repos,
|
||||||
|
preview=preview,
|
||||||
|
)
|
||||||
|
|
||||||
|
if remote:
|
||||||
|
_setup_remote_mirrors_for_repo(
|
||||||
|
repo,
|
||||||
|
repositories_base_dir=repositories_base_dir,
|
||||||
|
all_repos=all_repos,
|
||||||
|
preview=preview,
|
||||||
|
)
|
||||||
32
src/pkgmgr/actions/mirror/types.py
Normal file
32
src/pkgmgr/actions/mirror/types.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
Repository = Dict[str, Any]
|
||||||
|
MirrorMap = Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class RepoMirrorContext:
|
||||||
|
"""
|
||||||
|
Bundle mirror-related information for a single repository.
|
||||||
|
"""
|
||||||
|
|
||||||
|
identifier: str
|
||||||
|
repo_dir: str
|
||||||
|
config_mirrors: MirrorMap
|
||||||
|
file_mirrors: MirrorMap
|
||||||
|
|
||||||
|
@property
|
||||||
|
def resolved_mirrors(self) -> MirrorMap:
|
||||||
|
"""
|
||||||
|
Combined mirrors from config and MIRRORS file.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Start from config mirrors
|
||||||
|
- Overlay MIRRORS file (file wins on same name)
|
||||||
|
"""
|
||||||
|
merged: MirrorMap = dict(self.config_mirrors)
|
||||||
|
merged.update(self.file_mirrors)
|
||||||
|
return merged
|
||||||
@@ -6,6 +6,7 @@ from .version import handle_version
|
|||||||
from .make import handle_make
|
from .make import handle_make
|
||||||
from .changelog import handle_changelog
|
from .changelog import handle_changelog
|
||||||
from .branch import handle_branch
|
from .branch import handle_branch
|
||||||
|
from .mirror import handle_mirror_command
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"handle_repos_command",
|
"handle_repos_command",
|
||||||
@@ -16,4 +17,5 @@ __all__ = [
|
|||||||
"handle_make",
|
"handle_make",
|
||||||
"handle_changelog",
|
"handle_changelog",
|
||||||
"handle_branch",
|
"handle_branch",
|
||||||
|
"handle_mirror_command",
|
||||||
]
|
]
|
||||||
118
src/pkgmgr/cli/commands/mirror.py
Normal file
118
src/pkgmgr/cli/commands/mirror.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from pkgmgr.actions.mirror import (
|
||||||
|
diff_mirrors,
|
||||||
|
list_mirrors,
|
||||||
|
merge_mirrors,
|
||||||
|
setup_mirrors,
|
||||||
|
)
|
||||||
|
from pkgmgr.cli.context import CLIContext
|
||||||
|
|
||||||
|
Repository = Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_mirror_command(
|
||||||
|
args,
|
||||||
|
ctx: CLIContext,
|
||||||
|
selected: List[Repository],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Entry point for 'pkgmgr mirror' subcommands.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
- mirror list → list configured mirrors
|
||||||
|
- mirror diff → compare config vs MIRRORS file
|
||||||
|
- mirror merge → merge mirrors between config and MIRRORS file
|
||||||
|
- mirror setup → configure local Git + remote placeholders
|
||||||
|
"""
|
||||||
|
if not selected:
|
||||||
|
print("[INFO] No repositories selected for 'mirror' command.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
subcommand = getattr(args, "subcommand", None)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# mirror list
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if subcommand == "list":
|
||||||
|
source = getattr(args, "source", "all")
|
||||||
|
list_mirrors(
|
||||||
|
selected_repos=selected,
|
||||||
|
repositories_base_dir=ctx.repositories_base_dir,
|
||||||
|
all_repos=ctx.all_repositories,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# mirror diff
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if subcommand == "diff":
|
||||||
|
diff_mirrors(
|
||||||
|
selected_repos=selected,
|
||||||
|
repositories_base_dir=ctx.repositories_base_dir,
|
||||||
|
all_repos=ctx.all_repositories,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# mirror merge
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if subcommand == "merge":
|
||||||
|
source = getattr(args, "source", None)
|
||||||
|
target = getattr(args, "target", None)
|
||||||
|
preview = getattr(args, "preview", False)
|
||||||
|
|
||||||
|
if source == target:
|
||||||
|
print(
|
||||||
|
"[ERROR] For 'mirror merge', source and target "
|
||||||
|
"must differ (one of: config, file)."
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# Config file path can be passed explicitly via --config-path.
|
||||||
|
# If not given, fall back to the global context (if available).
|
||||||
|
explicit_config_path = getattr(args, "config_path", None)
|
||||||
|
user_config_path = explicit_config_path or getattr(
|
||||||
|
ctx, "user_config_path", None
|
||||||
|
)
|
||||||
|
|
||||||
|
merge_mirrors(
|
||||||
|
selected_repos=selected,
|
||||||
|
repositories_base_dir=ctx.repositories_base_dir,
|
||||||
|
all_repos=ctx.all_repositories,
|
||||||
|
source=source,
|
||||||
|
target=target,
|
||||||
|
preview=preview,
|
||||||
|
user_config_path=user_config_path,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# mirror setup
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
if subcommand == "setup":
|
||||||
|
local = getattr(args, "local", False)
|
||||||
|
remote = getattr(args, "remote", False)
|
||||||
|
preview = getattr(args, "preview", False)
|
||||||
|
|
||||||
|
# If neither flag is set → default to both.
|
||||||
|
if not local and not remote:
|
||||||
|
local = True
|
||||||
|
remote = True
|
||||||
|
|
||||||
|
setup_mirrors(
|
||||||
|
selected_repos=selected,
|
||||||
|
repositories_base_dir=ctx.repositories_base_dir,
|
||||||
|
all_repos=ctx.all_repositories,
|
||||||
|
preview=preview,
|
||||||
|
local=local,
|
||||||
|
remote=remote,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[ERROR] Unknown mirror subcommand: {subcommand}")
|
||||||
|
sys.exit(2)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user