Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a6c2f2988 | ||
|
|
0c90e984ad | ||
|
|
0a0cbbfe6d | ||
|
|
15c44cd484 | ||
|
|
6d7ee6fc04 | ||
|
|
5a022db0db | ||
|
|
37ac22e0b4 | ||
|
|
bcea440e40 | ||
|
|
6edde2d65b | ||
|
|
74189c1e14 |
64
.github/workflows/test-virgin-root.yml
vendored
Normal file
64
.github/workflows/test-virgin-root.yml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Test Virgin Root
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
- "*"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test-virgin-root:
|
||||
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 flake test (root)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo ">>> Starting virgin ArchLinux container test (root, with shared caches)..."
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD":/src \
|
||||
-v pkgmgr_repos:/root/Repositories \
|
||||
-v pkgmgr_pip_cache:/root/.cache/pip \
|
||||
-w /src \
|
||||
archlinux:latest \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
|
||||
echo ">>> Updating and upgrading Arch system..."
|
||||
pacman -Syu --noconfirm git python python-pip nix >/dev/null
|
||||
|
||||
echo ">>> Creating isolated virtual environment for pkgmgr..."
|
||||
python -m venv /tmp/pkgmgr-venv
|
||||
|
||||
echo ">>> Activating virtual environment..."
|
||||
source /tmp/pkgmgr-venv/bin/activate
|
||||
|
||||
echo ">>> Upgrading pip (cached)..."
|
||||
python -m pip install --upgrade pip >/dev/null
|
||||
|
||||
echo ">>> Installing pkgmgr from current source tree (cached pip)..."
|
||||
python -m pip install /src >/dev/null
|
||||
|
||||
echo ">>> Enabling Nix experimental features..."
|
||||
export NIX_CONFIG="experimental-features = nix-command flakes"
|
||||
|
||||
echo ">>> Running: pkgmgr update pkgmgr --clone-mode shallow --no-verification"
|
||||
pkgmgr update pkgmgr --clone-mode shallow --no-verification
|
||||
|
||||
echo ">>> Running: pkgmgr version pkgmgr"
|
||||
pkgmgr version pkgmgr
|
||||
|
||||
echo ">>> Virgin Arch (root) test completed successfully."
|
||||
'
|
||||
79
.github/workflows/test-virgin-user.yml
vendored
Normal file
79
.github/workflows/test-virgin-user.yml
vendored
Normal 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."
|
||||
'
|
||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -1,9 +1,6 @@
|
||||
|
||||
# Prevents unwanted files from being committed to version control.
|
||||
|
||||
# Custom Config file
|
||||
config/config.yaml
|
||||
|
||||
# Python bytecode
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -17,6 +14,16 @@ venv/
|
||||
dist/
|
||||
build/*
|
||||
*.egg-info/
|
||||
pkg
|
||||
src/source
|
||||
package-manager-*
|
||||
|
||||
# debian
|
||||
debian/package-manager/
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/.debhelper/
|
||||
debian/package-manager.substvars
|
||||
|
||||
# Editor files
|
||||
.vscode/
|
||||
@@ -31,11 +38,3 @@ Thumbs.db
|
||||
|
||||
# Ignore logs
|
||||
*.log
|
||||
package-manager-*
|
||||
|
||||
# debian
|
||||
debian/package-manager/
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/.debhelper/
|
||||
debian/package-manager.substvars
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
## [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 pkgmgr’s full flake installation path using shared caches for faster and reproducible CI runs.
|
||||
|
||||
|
||||
## [0.8.0] - 2025-12-10
|
||||
|
||||
* **v0.7.15 — Installer & Command Resolution Improvements**
|
||||
|
||||
6
Makefile
6
Makefile
@@ -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)
|
||||
|
||||
2
PKGBUILD
2
PKGBUILD
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Kevin Veen-Birkenbach <info@veen.world>
|
||||
|
||||
pkgname=package-manager
|
||||
pkgver=0.8.0
|
||||
pkgver=0.9.1
|
||||
pkgrel=1
|
||||
pkgdesc="Local-flake wrapper for Kevin's package-manager (Nix-based)."
|
||||
arch=('any')
|
||||
|
||||
64
README.md
64
README.md
@@ -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 PKGMGR’s package structure,
|
||||
installation layers, and setup controller flow:
|
||||
|
||||

|
||||
|
||||
**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
BIN
assets/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
15
debian/changelog
vendored
15
debian/changelog
vendored
@@ -1,3 +1,18 @@
|
||||
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 pkgmgr’s full flake installation path using shared caches for faster and reproducible CI runs.
|
||||
|
||||
-- Kevin Veen-Birkenbach <kevin@veen.world> Wed, 10 Dec 2025 18:38:07 +0100
|
||||
|
||||
package-manager (0.8.0-1) unstable; urgency=medium
|
||||
|
||||
* **v0.7.15 — Installer & Command Resolution Improvements**
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "0.8.0";
|
||||
version = "0.9.1";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: package-manager
|
||||
Version: 0.8.0
|
||||
Version: 0.9.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
|
||||
@@ -77,6 +77,15 @@ 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 pkgmgr’s full flake installation path using shared caches for faster and reproducible CI runs.
|
||||
|
||||
* Wed Dec 10 2025 Kevin Veen-Birkenbach <kevin@veen.world> - 0.8.0-1
|
||||
- **v0.7.15 — Installer & Command Resolution Improvements**
|
||||
|
||||
|
||||
@@ -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: []
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "package-manager"
|
||||
version = "0.8.0"
|
||||
version = "0.9.1"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
44
scripts/installation/venv-create.sh
Normal file
44
scripts/installation/venv-create.sh
Normal 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."
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
117
tests/e2e/test_path_commands.py
Normal file
117
tests/e2e/test_path_commands.py
Normal 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()
|
||||
216
tests/unit/pkgmgr/cli/commands/test_repos.py
Normal file
216
tests/unit/pkgmgr/cli/commands/test_repos.py
Normal 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()
|
||||
Reference in New Issue
Block a user