Compare commits

...

8 Commits

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

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

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

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

https://chatgpt.com/share/6939c98a-d428-800f-8bb8-cf72e80ba80c
2025-12-10 20:27:03 +01:00
Kevin Veen-Birkenbach
37ac22e0b4 test: isolate Nix store/cache per distro to fix cross-distro manifest conflicts
- Replace shared Nix volumes with distro-specific volumes
  (pkgmgr_nix_store_<distro>, pkgmgr_nix_cache_<distro>)
- Prevent incompatible profile manifest versions between Ubuntu and Debian
- Update all test scripts (unit, integration, container, e2e)
- Remove unused global Nix volume variables from Makefile
- Improve consistency of test-e2e.sh formatting and environment handling
- Add Git safe.directory configuration for mounted /src to avoid ownership warnings
2025-12-10 20:07:41 +01:00
Kevin Veen-Birkenbach
bcea440e40 Fix path and shell repo directory resolution + add unit/E2E tests
- Introduce `_resolve_repository_directory()` to unify directory lookup
  (explicit `directory` key → fallback to `get_repo_dir()` using base dir)
- Fix `pkgmgr path` to avoid KeyError and behave consistently with
  other commands using lazy directory resolution
- Fix `pkgmgr shell` to use resolved directory and correctly emit cwd
- Add full E2E tests for `pkgmgr path --all` and `pkgmgr path pkgmgr`
- Add unit tests covering:
    * explicit directory usage
    * fallback resolution via get_repo_dir()
    * empty selection behavior
    * shell command cwd resolution
    * missing shell command error handling
2025-12-10 19:47:26 +01:00
24 changed files with 611 additions and 138 deletions

View File

@@ -1,4 +1,4 @@
name: Test Virgin
name: Test Virgin Root
on:
push:
@@ -10,7 +10,7 @@ on:
pull_request:
jobs:
test-virgin:
test-virgin-root:
runs-on: ubuntu-latest
timeout-minutes: 45
@@ -21,16 +21,14 @@ jobs:
- name: Show Docker version
run: docker version
- name: Virgin Arch pkgmgr flake test
- name: Virgin Arch pkgmgr flake test (root)
run: |
set -euo pipefail
echo ">>> Starting virgin ArchLinux container test (with shared caches)..."
echo ">>> Starting virgin ArchLinux container test (root, with shared caches)..."
docker run --rm \
-v "$PWD":/src \
-v pkgmgr_nix_store:/nix \
-v pkgmgr_nix_cache:/root/.cache/nix \
-v pkgmgr_repos:/root/Repositories \
-v pkgmgr_pip_cache:/root/.cache/pip \
-w /src \
@@ -59,8 +57,8 @@ jobs:
echo ">>> Running: pkgmgr update pkgmgr --clone-mode shallow --no-verification"
pkgmgr update pkgmgr --clone-mode shallow --no-verification
echo ">>> Running: pkgmgr version"
echo ">>> Running: pkgmgr version pkgmgr"
pkgmgr version pkgmgr
echo ">>> Virgin Arch test completed successfully."
echo ">>> Virgin Arch (root) test completed successfully."
'

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

@@ -0,0 +1,79 @@
name: Test Virgin User
on:
push:
branches:
- main
- master
- develop
- "*"
pull_request:
jobs:
test-virgin-user:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Show Docker version
run: docker version
- name: Virgin Arch pkgmgr user test (non-root with sudo)
run: |
set -euo pipefail
echo ">>> Starting virgin ArchLinux container test (non-root user with sudo)..."
docker run --rm \
-v "$PWD":/src \
archlinux:latest \
bash -lc '
set -euo pipefail
echo ">>> [root] Updating and upgrading Arch system..."
pacman -Syu --noconfirm git python python-pip sudo base-devel debugedit
echo ">>> [root] Creating non-root user dev..."
useradd -m dev
echo ">>> [root] Allowing passwordless sudo for dev..."
echo "dev ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/dev
chmod 0440 /etc/sudoers.d/dev
echo ">>> [root] Adjusting ownership of /src for dev..."
chown -R dev:dev /src
echo ">>> [root] Running pkgmgr flow as non-root user dev..."
sudo -u dev env PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 bash -lc "
set -euo pipefail
cd /src
echo \">>> [dev] Using user: \$(whoami)\"
echo \">>> [dev] Running scripts/installation/main.sh...\"
bash scripts/installation/main.sh
echo \">>> [dev] Activating venv...\"
. \"\$HOME/.venvs/pkgmgr/bin/activate\"
echo \">>> [dev] Installing pkgmgr into venv via pip...\"
python -m pip install /src >/dev/null
echo \">>> [dev] PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=\$PKGMGR_DISABLE_NIX_FLAKE_INSTALLER\"
echo \">>> [dev] Updating managed repo package-manager via pkgmgr...\"
pkgmgr update pkgmgr --clone-mode shallow --no-verification
echo \">>> [dev] PATH:\"
echo \"\$PATH\"
echo \">>> [dev] which pkgmgr:\"
which pkgmgr || echo \">>> [dev] pkgmgr not found in PATH\"
echo \">>> [dev] Running: pkgmgr version pkgmgr\"
pkgmgr version pkgmgr
"
echo ">>> [root] Container flow finished."
'

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ dist/
build/*
*.egg-info/
pkg
src/source
package-manager-*
# debian

View File

@@ -1,3 +1,11 @@
## [0.9.1] - 2025-12-10
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`.
## [0.9.0] - 2025-12-10
* Introduce a virgin Arch-based Nix flake E2E workflow that validates pkgmgrs full flake installation path using shared caches for faster and reproducible CI runs.

View File

@@ -2,12 +2,6 @@
test build build-no-cache test-unit test-e2e test-integration \
test-container
# ------------------------------------------------------------
# Local Nix cache directories in the repo
# ------------------------------------------------------------
NIX_STORE_VOLUME := pkgmgr_nix_store
NIX_CACHE_VOLUME := pkgmgr_nix_cache
# ------------------------------------------------------------
# Distro list and base images
# (kept for documentation/reference; actual build logic is in scripts/build)

View File

@@ -1,7 +1,7 @@
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
pkgname=package-manager
pkgver=0.9.0
pkgver=0.9.1
pkgrel=1
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
arch=('any')

View File

@@ -24,6 +24,15 @@
- **Custom Aliases:**
Generate and manage custom aliases for easy command invocation.
## Architecture & Setup Map 🗺️
The following diagram provides a full overview of PKGMGRs package structure,
installation layers, and setup controller flow:
![PKGMGR Architecture](assets/map.png)
**Diagram status:** *Stand: 10. Dezember 2025*
**Always-up-to-date version:** https://s.veen.world/pkgmgrmp
## Installation ⚙️
@@ -51,55 +60,6 @@ The `make setup` command will:
- Install required packages from `requirements.txt`.
- Execute `python main.py install` to complete the installation.
## Docker Quickstart 🐳
Alternatively to installing locally, you can use Docker: build the image with
```bash
docker build --no-cache -t pkgmgr .
```
or alternativ pull it via
```bash
docker pull kevinveenbirkenbach/pkgmgr:latest
```
and then run
```bash
docker run --rm pkgmgr --help
```
## Usage 📖
Run the script with different commands. For example:
- **Install all packages:**
```bash
pkgmgr install --all
```
- **Pull updates for a specific repository:**
```bash
pkgmgr pull pkgmgr
```
- **Commit changes with extra Git parameters:**
```bash
pkgmgr commit pkgmgr -- -m "Your commit message"
```
- **List all configured packages:**
```bash
pkgmgr config show
```
- **Manage configuration:**
```bash
pkgmgr config init
pkgmgr config add
pkgmgr config edit
pkgmgr config delete <identifier>
pkgmgr config ignore <identifier> --set true
```
## License 📄
This project is licensed under the MIT License.
@@ -108,9 +68,3 @@ This project is licensed under the MIT License.
Kevin Veen-Birkenbach
[https://www.veen.world](https://www.veen.world)
---
**Repository:** [github.com/kevinveenbirkenbach/package-manager](https://github.com/kevinveenbirkenbach/package-manager)
*Created with AI 🤖 - [View conversation](https://chatgpt.com/share/67c728c4-92d0-800f-8945-003fa9bf27c6)*

BIN
assets/map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

9
debian/changelog vendored
View File

@@ -1,3 +1,12 @@
package-manager (0.9.1-1) unstable; urgency=medium
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`.
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 22:56:01 +0100
package-manager (0.9.0-1) unstable; urgency=medium
* Introduce a virgin Arch-based Nix flake E2E workflow that validates pkgmgrs full flake installation path using shared caches for faster and reproducible CI runs.

View File

@@ -31,7 +31,7 @@
rec {
pkgmgr = pyPkgs.buildPythonApplication {
pname = "package-manager";
version = "0.9.0";
version = "0.9.1";
# Use the git repo as source
src = ./.;

View File

@@ -1,5 +1,5 @@
Name: package-manager
Version: 0.9.0
Version: 0.9.1
Release: 1%{?dist}
Summary: Wrapper that runs Kevin's package-manager via Nix flake
@@ -77,6 +77,12 @@ echo ">>> package-manager removed. Nix itself was not removed."
/usr/lib/package-manager/
%changelog
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.1-1
- * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
* Split virgin tests into root/user workflows; stabilized Nix installer across distros; improved test scripts with dynamic distro selection and isolated Nix stores.
* Fixed repository directory resolution; improved `pkgmgr path` and `pkgmgr shell`; added full unit/E2E coverage.
* Removed deprecated files and updated `.gitignore`.
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.9.0-1
- Introduce a virgin Arch-based Nix flake E2E workflow that validates pkgmgrs full flake installation path using shared caches for faster and reproducible CI runs.

View File

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

View File

@@ -34,7 +34,6 @@ dependency formats, including:
\033[1;33mNix:\033[0m flake.nix
\033[1;33mArch Linux:\033[0m PKGBUILD
\033[1;33mAnsible:\033[0m requirements.yml
\033[1;33mpkgmgr-native:\033[0m pkgmgr.yml
This allows pkgmgr to perform installation, updates, verification, dependency
resolution, and synchronization across complex multi-repo environments — with a

View File

@@ -16,10 +16,36 @@ from pkgmgr.actions.repository.list import list_repositories
from pkgmgr.core.command.run import run_command
from pkgmgr.actions.repository.create import create_repo
from pkgmgr.core.repository.selected import get_selected_repos
from pkgmgr.core.repository.dir import get_repo_dir
Repository = Dict[str, Any]
def _resolve_repository_directory(repository: Repository, ctx: CLIContext) -> str:
"""
Resolve the local filesystem directory for a repository.
Priority:
1. Use repository["directory"] if present.
2. Fallback to get_repo_dir(...) using the repositories base directory
from the CLI context.
"""
repo_dir = repository.get("directory")
if repo_dir:
return repo_dir
base_dir = (
getattr(ctx, "repositories_base_dir", None)
or getattr(ctx, "repositories_dir", None)
)
if not base_dir:
raise RuntimeError(
"Cannot resolve repositories base directory from context; "
"expected ctx.repositories_base_dir or ctx.repositories_dir."
)
return get_repo_dir(base_dir, repository)
def handle_repos_command(
args,
ctx: CLIContext,
@@ -108,8 +134,25 @@ def handle_repos_command(
# path
# ------------------------------------------------------------
if args.command == "path":
if not selected:
print("[pkgmgr] No repositories selected for path.")
return
for repository in selected:
print(repository["directory"])
try:
repo_dir = _resolve_repository_directory(repository, ctx)
except Exception as exc:
ident = (
f"{repository.get('provider', '?')}/"
f"{repository.get('account', '?')}/"
f"{repository.get('repository', '?')}"
)
print(
f"[WARN] Could not resolve directory for {ident}: {exc}"
)
continue
print(repo_dir)
return
# ------------------------------------------------------------
@@ -119,14 +162,14 @@ def handle_repos_command(
if not args.shell_command:
print("[ERROR] 'shell' requires a command via -c/--command.")
sys.exit(2)
command_to_run = " ".join(args.shell_command)
for repository in selected:
print(
f"Executing in '{repository['directory']}': {command_to_run}"
)
repo_dir = _resolve_repository_directory(repository, ctx)
print(f"Executing in '{repo_dir}': {command_to_run}")
run_command(
command_to_run,
cwd=repository["directory"],
cwd=repo_dir,
preview=args.preview,
)
return

View File

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

View File

@@ -94,7 +94,15 @@ if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
# Ensure "nix" user (home at /home/nix)
if ! id nix >/dev/null 2>&1; then
echo "[init-nix] Creating user 'nix'..."
useradd -m -r -g nixbld -s /usr/bin/bash nix
# Resolve a valid shell path across distros:
# - Debian/Ubuntu: /bin/bash
# - Arch: /usr/bin/bash (often symlinked)
# Fall back to /bin/sh on ultra-minimal systems.
BASH_SHELL="$(command -v bash || true)"
if [[ -z "${BASH_SHELL}" ]]; then
BASH_SHELL="/bin/sh"
fi
useradd -m -r -g nixbld -s "${BASH_SHELL}" nix
fi
# Ensure /nix exists and is writable by the "nix" user.

View File

@@ -4,20 +4,22 @@ set -euo pipefail
# ------------------------------------------------------------
# main.sh
#
# Developer setup entrypoint.
# Developer / system setup entrypoint.
#
# Responsibilities:
# - If inside a Nix shell (IN_NIX_SHELL=1):
# * Skip venv creation and dependency installation
# * Run `python3 main.py install`
# - Otherwise:
# - If running as root (EUID=0):
# * Run system-level installer (run-package.sh)
# - Otherwise (normal user):
# * Create ~/.venvs/pkgmgr virtual environment if missing
# * Install Python dependencies into that venv
# * Append auto-activation to ~/.bashrc and ~/.zshrc
# * Run `main.py install` using the venv Python
# ------------------------------------------------------------
echo "[installation/main] Starting developer setup..."
echo "[installation/main] Starting setup..."
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${PROJECT_ROOT}"
@@ -26,20 +28,34 @@ VENV_DIR="${HOME}/.venvs/pkgmgr"
RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'
# ------------------------------------------------------------
# Nix shell mode: do not touch venv, only run main.py install
# 1) Nix shell mode: do not touch venv, only run main.py install
# ------------------------------------------------------------
if [[ -n "${IN_NIX_SHELL:-}" ]]; then
echo "[installation/main] Nix shell detected (IN_NIX_SHELL=1)."
echo "[installation/main] Skipping virtualenv creation and dependency installation."
echo "[installation/main] Running main.py install via system python3..."
python3 main.py install
echo "[installation/main] Developer setup finished (Nix mode)."
echo "[installation/main] Setup finished (Nix mode)."
exit 0
fi
# ------------------------------------------------------------
# Normal host mode: create/update venv and run main.py install
# 2) Root mode: system / distro-level installation
# ------------------------------------------------------------
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
echo "[installation/main] Running as root (EUID=0)."
echo "[installation/main] Skipping user virtualenv and shell RC modifications."
echo "[installation/main] Delegating to scripts/installation/run-package.sh..."
bash scripts/installation/run-package.sh
echo "[installation/main] Root/system setup complete."
exit 0
fi
# ------------------------------------------------------------
# 3) Normal user mode: dev setup with venv
# ------------------------------------------------------------
echo "[installation/main] Running in normal user mode (developer setup)."
echo "[installation/main] Ensuring main.py is executable..."
chmod +x main.py || true
@@ -47,26 +63,8 @@ chmod +x main.py || true
echo "[installation/main] Ensuring global virtualenv root: ${HOME}/.venvs"
mkdir -p "${HOME}/.venvs"
if [[ ! -d "${VENV_DIR}" ]]; then
echo "[installation/main] Creating virtual environment at: ${VENV_DIR}"
python3 -m venv "${VENV_DIR}"
else
echo "[installation/main] Virtual environment already exists at: ${VENV_DIR}"
fi
echo "[installation/main] Installing Python tooling into venv..."
"${VENV_DIR}/bin/python" -m ensurepip --upgrade
"${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel
if [[ -f "requirements.txt" ]]; then
echo "[installation/main] Installing dependencies from requirements.txt..."
"${VENV_DIR}/bin/pip" install -r requirements.txt
elif [[ -f "_requirements.txt" ]]; then
echo "[installation/main] Installing dependencies from _requirements.txt..."
"${VENV_DIR}/bin/pip" install -r _requirements.txt
else
echo "[installation/main] No requirements.txt or _requirements.txt found. Skipping dependency installation."
fi
echo "[installation/main] Creating/updating virtualenv via helper..."
PKGMGR_VENV_DIR="${VENV_DIR}" bash scripts/installation/venv-create.sh
echo "[installation/main] Ensuring ~/.bashrc and ~/.zshrc exist..."
touch "${HOME}/.bashrc" "${HOME}/.zshrc"

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
# venv-create.sh
#
# Small helper to create/update a Python virtual environment for pkgmgr.
#
# Usage:
# PKGMGR_VENV_DIR=/home/dev/.venvs/pkgmgr bash scripts/installation/venv-create.sh
# or
# bash scripts/installation/venv-create.sh /home/dev/.venvs/pkgmgr
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "${PROJECT_ROOT}"
VENV_DIR="${PKGMGR_VENV_DIR:-${1:-${HOME}/.venvs/pkgmgr}}"
echo "[venv-create] Using VENV_DIR=${VENV_DIR}"
echo "[venv-create] Ensuring virtualenv parent directory exists..."
mkdir -p "$(dirname "${VENV_DIR}")"
if [[ ! -d "${VENV_DIR}" ]]; then
echo "[venv-create] Creating virtual environment at: ${VENV_DIR}"
python3 -m venv "${VENV_DIR}"
else
echo "[venv-create] Virtual environment already exists at: ${VENV_DIR}"
fi
echo "[venv-create] Installing Python tooling into venv..."
"${VENV_DIR}/bin/python" -m ensurepip --upgrade
"${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel
if [[ -f "requirements.txt" ]]; then
echo "[venv-create] Installing dependencies from requirements.txt..."
"${VENV_DIR}/bin/pip" install -r requirements.txt
elif [[ -f "_requirements.txt" ]]; then
echo "[venv-create] Installing dependencies from _requirements.txt..."
"${VENV_DIR}/bin/pip" install -r _requirements.txt
else
echo "[venv-create] No requirements.txt or _requirements.txt found. Skipping dependency installation."
fi
echo "[venv-create] Done."

View File

@@ -19,9 +19,9 @@ for distro in $DISTROS; do
# Run the command and capture the output
if OUTPUT=$(docker run --rm \
-e PKGMGR_DEV=1 \
-v pkgmgr_nix_store:/nix \
-v pkgmgr_nix_store_${distro}:/nix \
-v "$(pwd):/src" \
-v "pkgmgr_nix_cache:/root/.cache/nix" \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
"$IMAGE" 2>&1); then
echo "$OUTPUT"
echo

View File

@@ -10,43 +10,46 @@ for distro in $DISTROS; do
docker run --rm \
-v "$(pwd):/src" \
-v pkgmgr_nix_store:/nix \
-v "pkgmgr_nix_cache:/root/.cache/nix" \
-v pkgmgr_nix_store_${distro}:/nix \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-e PKGMGR_DEV=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
--workdir /src \
--entrypoint bash \
"package-manager-test-$distro" \
-c '
set -e;
set -e
# Load distro info
if [ -f /etc/os-release ]; then
. /etc/os-release;
fi;
. /etc/os-release
fi
echo "Running tests inside distro: $ID";
echo "Running tests inside distro: $ID"
# Try to load nix environment
# 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";
. "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
fi
if [ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]; then
. "$HOME/.nix-profile/etc/profile.d/nix.sh";
. "$HOME/.nix-profile/etc/profile.d/nix.sh"
fi
PATH="/nix/var/nix/profiles/default/bin:$HOME/.nix-profile/bin:$PATH";
PATH="/nix/var/nix/profiles/default/bin:$HOME/.nix-profile/bin:$PATH"
command -v nix >/dev/null || {
echo "ERROR: nix not found.";
exit 1;
echo "ERROR: nix not found."
exit 1
}
git config --global --add safe.directory /src || true;
# Mark the mounted repository as safe to avoid Git ownership errors
git config --global --add safe.directory /src || true
# Run the E2E tests inside the Nix development shell
nix develop .#default --no-write-lock-file -c \
python3 -m unittest discover \
-s /src/tests/e2e \
-p "$TEST_PATTERN";
-p "$TEST_PATTERN"
'
done

View File

@@ -1,19 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
: "${distro:=arch}"
echo "============================================================"
echo ">>> Running INTEGRATION tests in Arch container"
echo ">>> Running INTEGRATION tests in ${distro} container"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v pkgmgr_nix_store:/nix \
-v "pkgmgr_nix_cache:/root/.cache/nix" \
-v pkgmgr_nix_store_${distro}:/nix \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
--workdir /src \
-e PKGMGR_DEV=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
--entrypoint bash \
"package-manager-test-arch" \
"package-manager-test-${distro}" \
-c '
set -e;
git config --global --add safe.directory /src || true;

View File

@@ -1,19 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
: "${distro:=arch}"
echo "============================================================"
echo ">>> Running UNIT tests in Arch container"
echo ">>> Running UNIT tests in ${distro} container"
echo "============================================================"
docker run --rm \
-v "$(pwd):/src" \
-v "pkgmgr_nix_cache:/root/.cache/nix" \
-v pkgmgr_nix_store:/nix \
-v "pkgmgr_nix_cache_${distro}:/root/.cache/nix" \
-v pkgmgr_nix_store_${distro}:/nix \
--workdir /src \
-e PKGMGR_DEV=1 \
-e TEST_PATTERN="${TEST_PATTERN}" \
--entrypoint bash \
"package-manager-test-arch" \
"package-manager-test-${distro}" \
-c '
set -e;
git config --global --add safe.directory /src || true;

View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
End-to-end tests for the `pkgmgr path` command.
We verify two usage patterns:
1) pkgmgr path --all
- Should print the paths of all configured repositories.
2) pkgmgr path pkgmgr
- Should print the path for the repository identified as "pkgmgr".
Both tests are considered successful if the command completes without
raising an exception and exits with code 0 (or no explicit exit code).
"""
from __future__ import annotations
import io
import runpy
import sys
import unittest
from contextlib import redirect_stdout
class TestPathCommandsE2E(unittest.TestCase):
def _run_pkgmgr_path(self, argv_tail: list[str]) -> str:
"""
Helper to run `pkgmgr path ...` via main.py and return stdout.
Args:
argv_tail: List of arguments that follow the "pkgmgr" executable,
e.g. ["path", "--all"] or ["path", "pkgmgr"].
Returns:
The captured stdout produced by the command.
Raises:
AssertionError if the command exits with a non-zero exit code.
"""
original_argv = sys.argv
cmd_repr = "pkgmgr " + " ".join(argv_tail)
buffer = io.StringIO()
try:
sys.argv = ["pkgmgr"] + argv_tail
try:
# Capture stdout while running the CLI entry point.
with redirect_stdout(buffer):
runpy.run_module("main", run_name="__main__")
except SystemExit as exc:
# Determine the exit code (int or string)
exit_code = exc.code
if isinstance(exit_code, int):
numeric_code = exit_code
else:
try:
numeric_code = int(exit_code)
except (TypeError, ValueError):
numeric_code = None
# Treat SystemExit(0) as success.
if numeric_code == 0 or numeric_code is None:
return buffer.getvalue()
# Non-zero exit code → fail with helpful message.
raise AssertionError(
f"{cmd_repr!r} failed with exit code {exit_code!r}. "
"Scroll up to see the full pkgmgr output inside the container."
) from exc
finally:
sys.argv = original_argv
# No SystemExit raised → also treat as success.
return buffer.getvalue()
def test_path_all_repositories(self) -> None:
"""
Run: pkgmgr path --all
The test succeeds if the command exits successfully and prints
at least one non-empty line.
"""
output = self._run_pkgmgr_path(["path", "--all"])
lines = [line for line in output.splitlines() if line.strip()]
# We only assert that something was printed; we do not assume
# that repositories are already cloned on disk.
self.assertGreater(
len(lines),
0,
msg="Expected `pkgmgr path --all` to print at least one path.",
)
def test_path_single_pkgmgr(self) -> None:
"""
Run: pkgmgr path pkgmgr
The test succeeds if the command exits successfully and prints
at least one non-empty line (the resolved directory).
"""
output = self._run_pkgmgr_path(["path", "pkgmgr"])
lines = [line for line in output.splitlines() if line.strip()]
self.assertGreater(
len(lines),
0,
msg="Expected `pkgmgr path pkgmgr` to print at least one path.",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Unit tests for pkgmgr.cli.commands.repos
We focus on the behaviour of:
- _resolve_repository_directory(...)
- handle_repos_command(...) for the "path" and "shell" commands
Goals:
* "path" should:
- print repo["directory"] if present
- fall back to get_repo_dir(ctx.repositories_base_dir, repo) otherwise
- handle "no selected repos" gracefully
* "shell" should:
- resolve the directory via _resolve_repository_directory(...)
- call run_command(...) with cwd set to the resolved directory
"""
from __future__ import annotations
import io
import sys
import unittest
from contextlib import redirect_stdout
from types import SimpleNamespace
from typing import Any, Dict, List
from unittest.mock import MagicMock, patch
from pkgmgr.cli.context import CLIContext
from pkgmgr.cli.commands.repos import handle_repos_command
Repository = Dict[str, Any]
class TestReposCommand(unittest.TestCase):
def _make_ctx(self, repositories: List[Repository]) -> CLIContext:
"""
Helper to build a minimal CLIContext for tests.
"""
return CLIContext(
config_merged={},
repositories_base_dir="/base/dir",
all_repositories=repositories,
binaries_dir="/bin/dir",
user_config_path="~/.config/pkgmgr/config.yaml",
)
# ------------------------------------------------------------------
# "path" command tests
# ------------------------------------------------------------------
def test_path_uses_explicit_directory_if_present(self) -> None:
"""
When repository["directory"] is present, handle_repos_command("path")
should print this value directly without calling get_repo_dir().
"""
repos: List[Repository] = [
{
"provider": "github.com",
"account": "kevinveenbirkenbach",
"repository": "package-manager",
"directory": "/custom/path/pkgmgr",
}
]
ctx = self._make_ctx(repos)
args = SimpleNamespace(
command="path",
preview=False,
list=False,
system=False,
extra_args=[],
)
buf = io.StringIO()
with patch(
"pkgmgr.cli.commands.repos.get_repo_dir"
) as mock_get_repo_dir, redirect_stdout(buf):
handle_repos_command(args, ctx, selected=repos)
output = buf.getvalue().strip().splitlines()
self.assertIn("/custom/path/pkgmgr", output)
mock_get_repo_dir.assert_not_called()
def test_path_falls_back_to_get_repo_dir_if_directory_missing(self) -> None:
"""
When repository["directory"] is missing, handle_repos_command("path")
should call get_repo_dir(ctx.repositories_base_dir, repo) and print
the returned value.
"""
repos: List[Repository] = [
{
"provider": "github.com",
"account": "kevinveenbirkenbach",
"repository": "package-manager",
}
]
ctx = self._make_ctx(repos)
args = SimpleNamespace(
command="path",
preview=False,
list=False,
system=False,
extra_args=[],
)
buf = io.StringIO()
with patch(
"pkgmgr.cli.commands.repos.get_repo_dir",
return_value="/resolved/from/get_repo_dir",
) as mock_get_repo_dir, redirect_stdout(buf):
handle_repos_command(args, ctx, selected=repos)
output = buf.getvalue().strip().splitlines()
self.assertIn("/resolved/from/get_repo_dir", output)
mock_get_repo_dir.assert_called_once_with("/base/dir", repos[0])
def test_path_with_no_selected_repos_prints_message(self) -> None:
"""
When 'selected' is empty, the 'path' command should print a friendly
message and not raise.
"""
ctx = self._make_ctx(repositories=[])
args = SimpleNamespace(
command="path",
preview=False,
list=False,
system=False,
extra_args=[],
)
buf = io.StringIO()
with redirect_stdout(buf):
handle_repos_command(args, ctx, selected=[])
output = buf.getvalue()
self.assertIn("No repositories selected for path", output)
# ------------------------------------------------------------------
# "shell" command tests
# ------------------------------------------------------------------
def test_shell_resolves_directory_and_calls_run_command(self) -> None:
"""
'shell' should resolve the repository directory and pass it as cwd
to run_command(), along with the full shell command string.
"""
repos: List[Repository] = [
{
"provider": "github.com",
"account": "kevinveenbirkenbach",
"repository": "package-manager",
}
]
ctx = self._make_ctx(repos)
args = SimpleNamespace(
command="shell",
preview=False,
shell_command=["echo", "hello"],
)
with patch(
"pkgmgr.cli.commands.repos.get_repo_dir",
return_value="/resolved/for/shell",
) as mock_get_repo_dir, patch(
"pkgmgr.cli.commands.repos.run_command"
) as mock_run_command:
buf = io.StringIO()
with redirect_stdout(buf):
handle_repos_command(args, ctx, selected=repos)
# _resolve_repository_directory should have called get_repo_dir
mock_get_repo_dir.assert_called_once_with("/base/dir", repos[0])
# run_command should be invoked with cwd set to the resolved path
mock_run_command.assert_called_once()
called_args, called_kwargs = mock_run_command.call_args
self.assertEqual("echo hello", called_args[0]) # command string
self.assertEqual("/resolved/for/shell", called_kwargs["cwd"])
self.assertFalse(called_kwargs["preview"])
def test_shell_without_command_exits_with_error(self) -> None:
"""
'shell' without -c/--command should print an error and exit with code 2.
"""
repos: List[Repository] = []
ctx = self._make_ctx(repos)
args = SimpleNamespace(
command="shell",
preview=False,
shell_command=[],
)
buf = io.StringIO()
with redirect_stdout(buf), self.assertRaises(SystemExit) as cm:
handle_repos_command(args, ctx, selected=repos)
self.assertEqual(cm.exception.code, 2)
output = buf.getvalue()
self.assertIn("'shell' requires a command via -c/--command", output)
if __name__ == "__main__":
unittest.main()