Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72fc69c2f8 | ||
|
|
6d8c6deae8 | ||
|
|
6c116a029e | ||
|
|
3eb7c81fa1 | ||
|
|
0334f477fd | ||
|
|
8bb99c99b7 | ||
|
|
587cb2e516 | ||
|
|
fcf9d4b59b | ||
|
|
b483dbfaad | ||
|
|
9630917570 | ||
|
|
6a4432dd04 |
50
.github/workflows/mark-stable.yml
vendored
50
.github/workflows/mark-stable.yml
vendored
@@ -3,7 +3,9 @@ name: Mark stable commit
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- main # still run tests for main
|
||||
tags:
|
||||
- 'v*' # run tests for version tags (e.g. v0.9.1)
|
||||
|
||||
jobs:
|
||||
test-unit:
|
||||
@@ -34,31 +36,63 @@ jobs:
|
||||
- test-virgin-root
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Only run this job if the push is for a version tag (v*)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
permissions:
|
||||
contents: write # to move the tag
|
||||
contents: write # Required to move/update the tag
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true # We need all tags for version comparison
|
||||
|
||||
- name: Move 'stable' tag to this commit
|
||||
- name: Move 'stable' tag only if this version is the highest
|
||||
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…"
|
||||
echo "Ref: $GITHUB_REF"
|
||||
echo "SHA: $GITHUB_SHA"
|
||||
|
||||
# delete local tag if exists
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "Current version tag: ${VERSION}"
|
||||
|
||||
echo "Collecting all version tags..."
|
||||
ALL_V_TAGS="$(git tag --list 'v*' || true)"
|
||||
|
||||
if [[ -z "${ALL_V_TAGS}" ]]; then
|
||||
echo "No version tags found. Skipping stable update."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "All version tags:"
|
||||
echo "${ALL_V_TAGS}"
|
||||
|
||||
# Determine highest version using natural version sorting
|
||||
LATEST_TAG="$(printf '%s\n' ${ALL_V_TAGS} | sort -V | tail -n1)"
|
||||
|
||||
echo "Highest version tag: ${LATEST_TAG}"
|
||||
|
||||
if [[ "${VERSION}" != "${LATEST_TAG}" ]]; then
|
||||
echo "Current version ${VERSION} is NOT the highest version."
|
||||
echo "Stable tag will NOT be updated."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Current version ${VERSION} IS the highest version."
|
||||
echo "Updating 'stable' tag..."
|
||||
|
||||
# Delete existing stable tag (local + remote)
|
||||
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
|
||||
# Create new stable tag
|
||||
git tag stable "$GITHUB_SHA"
|
||||
git push origin stable
|
||||
|
||||
echo "✅ Stable tag updated."
|
||||
echo "✅ Stable tag updated to ${VERSION}."
|
||||
|
||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -1,3 +1,51 @@
|
||||
## [1.0.0] - 2025-12-11
|
||||
|
||||
* **1.0.0 – Official Stable Release 🎉**
|
||||
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
||||
|
||||
---
|
||||
|
||||
**Key Features**
|
||||
|
||||
**Core Functionality**
|
||||
|
||||
* Manage many repositories with one CLI: `clone`, `update`, `install`, `list`, `path`, `config`
|
||||
* Proxy wrappers for Git, Docker/Compose and Make
|
||||
* Multi-repo execution with safe *preview mode*
|
||||
* Mirror management: `mirror list/diff/merge/setup`
|
||||
|
||||
**Releases & Versioning**
|
||||
|
||||
* Automated SemVer bumps, tagging and changelog generation
|
||||
* Supports PKGBUILD, Debian, RPM, pyproject.toml, flake.nix
|
||||
|
||||
**Developer Tools**
|
||||
|
||||
* Open repositories in VS Code, file manager or terminal
|
||||
* Unified workflows across all major Linux distros
|
||||
|
||||
**Nix Integration**
|
||||
|
||||
* Cross-distro reproducible builds via Nix flakes
|
||||
* CI-tested across all supported environments
|
||||
|
||||
---
|
||||
|
||||
**Summary**
|
||||
PKGMGR 1.0.0 unifies repository management, build tooling, release automation and reproducible multi-distro workflows into one cohesive CLI tool.
|
||||
|
||||
*This is the first official stable release.*
|
||||
|
||||
|
||||
## [0.10.2] - 2025-12-11
|
||||
|
||||
* * Stable tag now updates only when a new highest version is released.
|
||||
* Debian package now includes sudo to ensure privilege escalation works reliably.
|
||||
* Nix setup is significantly more resilient with retries, correct permissions, and better environment handling.
|
||||
* AUR builder setup uses retries so yay installs succeed even under network instability.
|
||||
* Nix flake installation now fails only on mandatory parts; optional outputs no longer block installation.
|
||||
|
||||
|
||||
## [0.10.1] - 2025-12-11
|
||||
|
||||
* Fixed Debian\Ubuntu to pass container e2e tests
|
||||
@@ -5,8 +53,6 @@
|
||||
|
||||
## [0.10.0] - 2025-12-11
|
||||
|
||||
* **Changes since v0.9.1**
|
||||
|
||||
**Mirror System**
|
||||
|
||||
* Added SSH mirror support including multi-push and remote probing
|
||||
|
||||
178
README.md
178
README.md
@@ -1,70 +1,186 @@
|
||||
# Package Manager🤖📦
|
||||
# Package Manager 🤖📦
|
||||
|
||||
[](https://github.com/sponsors/kevinveenbirkenbach)
|
||||
[](https://www.patreon.com/c/kevinveenbirkenbach)
|
||||
[](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
||||
[](https://www.patreon.com/c/kevinveenbirkenbach)
|
||||
[](https://buymeacoffee.com/kevinveenbirkenbach)
|
||||
[](https://s.veen.world/paypaldonate)
|
||||
[](LICENSE)
|
||||
[](https://github.com/kevinveenbirkenbach/package-manager)
|
||||
|
||||
*Kevins's* Package Manager is a configurable Python tool designed to manage multiple repositories via Bash. It automates common Git operations such as clone, pull, push, status, and more. Additionally, it handles the creation of executable wrappers and alias links for your repositories.
|
||||
**Kevin's Package Manager (PKGMGR)** is a *multi-distro* package manager and workflow orchestrator.
|
||||
It helps you **develop, package, release and manage projects across multiple Linux-based
|
||||
operating systems** (Arch, Debian, Ubuntu, Fedora, CentOS, …).
|
||||
|
||||
PKGMGR is implemented in **Python** and uses **Nix (flakes)** as a foundation for
|
||||
distribution-independent builds and tooling. On top of that it provides a rich
|
||||
CLI that proxies common developer tools (Git, Docker, Make, …) and glues them
|
||||
together into repeatable development workflows.
|
||||
|
||||
---
|
||||
|
||||
## Why PKGMGR? 🧠
|
||||
|
||||
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
||||
single operating system. PKGMGR instead focuses on **your repositories and
|
||||
development lifecycle**:
|
||||
|
||||
* one configuration for all your repos,
|
||||
* one CLI to interact with them,
|
||||
* one Nix-based layer to keep tooling reproducible across distros.
|
||||
|
||||
You keep using your native package manager where it makes sense – PKGMGR
|
||||
coordinates the *development and release flow* around it.
|
||||
|
||||
---
|
||||
|
||||
## Features 🚀
|
||||
|
||||
- **Installation & Setup:**
|
||||
Create executable wrappers with auto-detected commands (e.g. `main.sh` or `main.py`).
|
||||
|
||||
- **Git Operations:**
|
||||
Easily perform `git pull`, `push`, `status`, `commit`, `diff`, `add`, `show`, and `checkout` with extra parameters passed through.
|
||||
|
||||
- **Configuration Management:**
|
||||
Manage repository configurations via a default file (`config/defaults.yaml`) and a user-specific file (`config/config.yaml`). Initialize, add, delete, or ignore entries using subcommands.
|
||||
|
||||
- **Path & Listing:**
|
||||
Display repository paths or list all configured packages with their details.
|
||||
|
||||
- **Custom Aliases:**
|
||||
Generate and manage custom aliases for easy command invocation.
|
||||
### Multi-distro development & packaging
|
||||
|
||||
* Manage **many repositories at once** from a single `config/config.yaml`.
|
||||
* Drive full **release pipelines** across Linux distributions using:
|
||||
|
||||
* Nix flakes (`flake.nix`)
|
||||
* PyPI style builds (`pyproject.toml`)
|
||||
* OS packages (PKGBUILD, Debian control/changelog, RPM spec)
|
||||
* Ansible Galaxy metadata and more.
|
||||
|
||||
### Rich CLI for daily work
|
||||
|
||||
All commands are exposed via the `pkgmgr` CLI and are available on every distro:
|
||||
|
||||
* **Repository management**
|
||||
|
||||
* `clone`, `update`, `install`, `delete`, `deinstall`, `path`, `list`, `config`
|
||||
* **Git proxies**
|
||||
|
||||
* `pull`, `push`, `status`, `diff`, `add`, `show`, `checkout`,
|
||||
`reset`, `revert`, `rebase`, `commit`, `branch`
|
||||
* **Docker & Compose orchestration**
|
||||
|
||||
* `build`, `up`, `down`, `exec`, `ps`, `start`, `stop`, `restart`
|
||||
* **Release toolchain**
|
||||
|
||||
* `version`, `release`, `changelog`, `make`
|
||||
* **Mirror & workflow helpers**
|
||||
|
||||
* `mirror` (list/diff/merge/setup), `shell`, `terminal`, `code`, `explore`
|
||||
|
||||
Many of these commands support `--preview` mode so you can inspect the
|
||||
underlying Git or Docker calls without executing them.
|
||||
|
||||
### Full development workflows
|
||||
|
||||
PKGMGR is not just a helper around Git commands. Combined with its release and
|
||||
versioning features it can drive **end-to-end workflows**:
|
||||
|
||||
1. Clone and mirror repositories.
|
||||
2. Run tests and builds through `make` or Nix.
|
||||
3. Bump versions, update changelogs and tags.
|
||||
4. Build distro-specific packages.
|
||||
5. Keep all mirrors and working copies in sync.
|
||||
|
||||
The extensive E2E tests (`tests/e2e/`) and GitHub Actions workflows (including
|
||||
“virgin user” and “virgin root” Arch tests) validate these flows across
|
||||
different Linux environments.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Setup Map 🗺️
|
||||
|
||||
The following diagram provides a full overview of PKGMGR’s package structure,
|
||||
installation layers, and setup controller flow:
|
||||
The following diagram gives a full overview of:
|
||||
|
||||
* PKGMGR’s package structure,
|
||||
* the layered installers (OS, foundation, Python, Makefile),
|
||||
* and the setup controller that decides which layer to use on a given system.
|
||||
|
||||

|
||||
|
||||
**Diagram status:** *Stand: 11. Dezember 2025*
|
||||
**Always-up-to-date version:** https://s.veen.world/pkgmgrmp
|
||||
**Diagram status:** 11 December 2025
|
||||
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
||||
|
||||
---
|
||||
|
||||
## Installation ⚙️
|
||||
|
||||
Clone the repository and ensure your `~/.local/bin` is in your system PATH:
|
||||
### 1. Get the latest stable version
|
||||
|
||||
For a stable setup, use the **latest tagged release** (the tag pointed to by
|
||||
`latest`):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||
cd package-manager
|
||||
|
||||
# Optional but recommended: checkout the latest stable tag
|
||||
git fetch --tags
|
||||
git checkout "$(git describe --tags --abbrev=0)"
|
||||
```
|
||||
|
||||
Install make and pip if not installed yet:
|
||||
### 2. Install via Make
|
||||
|
||||
The project ships with a Makefile that encapsulates the typical installation
|
||||
flow. On most systems you only need:
|
||||
|
||||
```bash
|
||||
pacman -S make python-pip
|
||||
# Ensure make, Python and pip are installed via your distro package manager
|
||||
# (e.g. pacman -S make python python-pip, apt install make python3-pip, ...)
|
||||
|
||||
make install
|
||||
```
|
||||
|
||||
Then, run the following command to set up the project:
|
||||
This will:
|
||||
|
||||
* create or reuse a Python virtual environment,
|
||||
* install PKGMGR (and its Python dependencies) into that environment,
|
||||
* expose the `pkgmgr` executable on your PATH (usually via `~/.local/bin`),
|
||||
* prepare Nix-based integration where available so PKGMGR can build and manage
|
||||
packages distribution-independently.
|
||||
|
||||
For development use, you can also run:
|
||||
|
||||
```bash
|
||||
make setup
|
||||
```
|
||||
|
||||
The `make setup` command will:
|
||||
- Make `main.py` executable.
|
||||
- Install required packages from `requirements.txt`.
|
||||
- Execute `python main.py install` to complete the installation.
|
||||
which prepares the environment and leaves you with a fully wired development
|
||||
workspace (including Nix, tests and scripts).
|
||||
|
||||
---
|
||||
|
||||
## Usage 🧰
|
||||
|
||||
After installation, the main entry point is:
|
||||
|
||||
```bash
|
||||
pkgmgr --help
|
||||
```
|
||||
|
||||
This prints a list of all available subcommands, for example:
|
||||
|
||||
* `pkgmgr list --all` – show all repositories in the config
|
||||
* `pkgmgr update --all --clone-mode https` – update every repository
|
||||
* `pkgmgr release patch --preview` – simulate a patch release
|
||||
* `pkgmgr version --all` – show version information for all repositories
|
||||
* `pkgmgr mirror setup --preview --all` – prepare Git mirrors (no changes in preview)
|
||||
* `pkgmgr make install --preview pkgmgr` – preview make install for the pkgmgr repo
|
||||
|
||||
The help for each command is available via:
|
||||
|
||||
```bash
|
||||
pkgmgr <command> --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License 📄
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## Author 👤
|
||||
|
||||
Kevin Veen-Birkenbach
|
||||
Kevin Veen-Birkenbach
|
||||
[https://www.veen.world](https://www.veen.world)
|
||||
|
||||
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 |
@@ -36,7 +36,7 @@
|
||||
rec {
|
||||
pkgmgr = pyPkgs.buildPythonApplication {
|
||||
pname = "package-manager";
|
||||
version = "0.10.1";
|
||||
version = "1.0.0";
|
||||
|
||||
# Use the git repo as source
|
||||
src = ./.;
|
||||
|
||||
@@ -9,7 +9,7 @@ Homepage: https://github.com/kevinveenbirkenbach/package-manager
|
||||
|
||||
Package: package-manager
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}
|
||||
Depends: sudo, ${misc:Depends}
|
||||
Description: Wrapper that runs Kevin's package-manager via Nix flake
|
||||
This package provides the `pkgmgr` command, which runs Kevin's package
|
||||
manager via a local Nix flake
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "package-manager"
|
||||
version = "0.10.1"
|
||||
version = "1.0.0"
|
||||
description = "Kevin's package-manager tool (pkgmgr)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@@ -3,21 +3,22 @@ set -euo pipefail
|
||||
|
||||
echo "[init-nix] Starting Nix initialization..."
|
||||
|
||||
NIX_INSTALL_URL="${NIX_INSTALL_URL:-https://nixos.org/nix/install}"
|
||||
NIX_DOWNLOAD_MAX_TIME=300 # 5 minutes
|
||||
NIX_DOWNLOAD_SLEEP_INTERVAL=20 # 20 seconds
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: detect whether we are inside a container (Docker/Podman/etc.)
|
||||
# Detect whether we are inside a container (Docker/Podman/etc.)
|
||||
# ---------------------------------------------------------------------------
|
||||
is_container() {
|
||||
# Docker / Podman markers
|
||||
if [[ -f /.dockerenv ]] || [[ -f /run/.containerenv ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# cgroup hints
|
||||
if grep -qiE 'docker|container|podman|lxc' /proc/1/cgroup 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Environment variable used by some runtimes
|
||||
if [[ -n "${container:-}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
@@ -26,200 +27,206 @@ is_container() {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: ensure Nix binaries are on PATH (multi-user or single-user)
|
||||
# Ensure Nix binaries are on PATH (multi-user or single-user)
|
||||
# ---------------------------------------------------------------------------
|
||||
ensure_nix_on_path() {
|
||||
# Multi-user profile (daemon install)
|
||||
if [[ -x /nix/var/nix/profiles/default/bin/nix ]]; then
|
||||
export PATH="/nix/var/nix/profiles/default/bin:${PATH}"
|
||||
fi
|
||||
|
||||
# Single-user profile (current user)
|
||||
if [[ -x "${HOME}/.nix-profile/bin/nix" ]]; then
|
||||
export PATH="${HOME}/.nix-profile/bin:${PATH}"
|
||||
fi
|
||||
|
||||
# Single-user profile for dedicated "nix" user (container case)
|
||||
if [[ -x /home/nix/.nix-profile/bin/nix ]]; then
|
||||
export PATH="/home/nix/.nix-profile/bin:${PATH}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fast path: Nix already available
|
||||
# Ensure Nix build group and users exist (build-users-group = nixbld)
|
||||
# ---------------------------------------------------------------------------
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] Nix already available on PATH: $(command -v nix)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ensure_nix_on_path
|
||||
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] Nix found after adjusting PATH: $(command -v nix)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[init-nix] Nix not found, starting installation logic..."
|
||||
|
||||
IN_CONTAINER=0
|
||||
if is_container; then
|
||||
IN_CONTAINER=1
|
||||
echo "[init-nix] Detected container environment."
|
||||
else
|
||||
echo "[init-nix] No container detected."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Container + root: install Nix as dedicated "nix" user (single-user)
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
||||
echo "[init-nix] Running as root inside a container – using dedicated 'nix' user."
|
||||
|
||||
# Ensure nixbld group (required by Nix)
|
||||
ensure_nix_build_group() {
|
||||
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
|
||||
}
|
||||
|
||||
# Ensure "nix" user (home at /home/nix)
|
||||
if ! id nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] Creating user '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"
|
||||
# ---------------------------------------------------------------------------
|
||||
# Download and run Nix installer with retry
|
||||
# Usage: install_nix_with_retry daemon|no-daemon [run_as_user]
|
||||
# ---------------------------------------------------------------------------
|
||||
install_nix_with_retry() {
|
||||
local mode="$1"
|
||||
local run_as="${2:-}"
|
||||
local installer elapsed=0 mode_flag
|
||||
|
||||
case "${mode}" in
|
||||
daemon) mode_flag="--daemon" ;;
|
||||
no-daemon) mode_flag="--no-daemon" ;;
|
||||
*)
|
||||
echo "[init-nix] ERROR: Invalid mode '${mode}', expected 'daemon' or 'no-daemon'."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
installer="$(mktemp -t nix-installer.XXXXXX)"
|
||||
|
||||
echo "[init-nix] Downloading Nix installer from ${NIX_INSTALL_URL} with retry (max ${NIX_DOWNLOAD_MAX_TIME}s)..."
|
||||
|
||||
while true; do
|
||||
if curl -fL "${NIX_INSTALL_URL}" -o "${installer}"; then
|
||||
echo "[init-nix] Successfully downloaded Nix installer to ${installer}"
|
||||
break
|
||||
fi
|
||||
useradd -m -r -g nixbld -s "${BASH_SHELL}" nix
|
||||
fi
|
||||
|
||||
# Ensure /nix exists and is writable by the "nix" user.
|
||||
#
|
||||
# In some base images (or previous runs), /nix may already exist and be
|
||||
# owned by root. In that case the Nix single-user installer will abort with:
|
||||
#
|
||||
# "directory /nix exists, but is not writable by you"
|
||||
#
|
||||
# To keep container runs idempotent and robust, we always enforce
|
||||
# ownership nix:nixbld here.
|
||||
if [[ ! -d /nix ]]; then
|
||||
echo "[init-nix] Creating /nix with owner nix:nixbld..."
|
||||
mkdir -m 0755 /nix
|
||||
chown nix:nixbld /nix
|
||||
else
|
||||
current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')"
|
||||
current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')"
|
||||
if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then
|
||||
echo "[init-nix] /nix already exists with owner ${current_owner}:${current_group} – fixing to nix:nixbld..."
|
||||
chown -R nix:nixbld /nix
|
||||
local curl_exit=$?
|
||||
echo "[init-nix] WARNING: Failed to download Nix installer (curl exit code ${curl_exit})."
|
||||
|
||||
elapsed=$((elapsed + NIX_DOWNLOAD_SLEEP_INTERVAL))
|
||||
if (( elapsed >= NIX_DOWNLOAD_MAX_TIME )); then
|
||||
echo "[init-nix] ERROR: Giving up after ${elapsed}s trying to download Nix installer."
|
||||
rm -f "${installer}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[init-nix] Retrying in ${NIX_DOWNLOAD_SLEEP_INTERVAL}s (elapsed: ${elapsed}s/${NIX_DOWNLOAD_MAX_TIME}s)..."
|
||||
sleep "${NIX_DOWNLOAD_SLEEP_INTERVAL}"
|
||||
done
|
||||
|
||||
if [[ -n "${run_as}" ]]; then
|
||||
echo "[init-nix] Running installer as user '${run_as}' with mode '${mode}'..."
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo -u "${run_as}" bash -lc "sh '${installer}' ${mode_flag}"
|
||||
else
|
||||
echo "[init-nix] /nix already exists with correct owner nix:nixbld."
|
||||
su - "${run_as}" -c "sh '${installer}' ${mode_flag}"
|
||||
fi
|
||||
|
||||
if [[ ! -w /nix ]]; then
|
||||
echo "[init-nix] WARNING: /nix is still not writable after chown; Nix installer may fail."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run Nix single-user installer as "nix"
|
||||
echo "[init-nix] Installing Nix as user 'nix' (single-user, --no-daemon)..."
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
sudo -u nix bash -lc 'sh <(curl -L https://nixos.org/nix/install) --no-daemon'
|
||||
else
|
||||
su - nix -c 'sh <(curl -L https://nixos.org/nix/install) --no-daemon'
|
||||
echo "[init-nix] Running installer as current user with mode '${mode}'..."
|
||||
sh "${installer}" "${mode_flag}"
|
||||
fi
|
||||
|
||||
# After installation, expose nix to root via PATH and symlink
|
||||
ensure_nix_on_path
|
||||
rm -f "${installer}"
|
||||
}
|
||||
|
||||
if [[ -x /home/nix/.nix-profile/bin/nix ]]; then
|
||||
if [[ ! -e /usr/local/bin/nix ]]; then
|
||||
echo "[init-nix] Creating /usr/local/bin/nix symlink -> /home/nix/.nix-profile/bin/nix"
|
||||
ln -s /home/nix/.nix-profile/bin/nix /usr/local/bin/nix
|
||||
fi
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
main() {
|
||||
# Fast path: Nix already available
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] Nix already available on PATH: $(command -v nix)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ensure_nix_on_path
|
||||
|
||||
if command -v nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] Nix successfully installed (container mode) at: $(command -v nix)"
|
||||
else
|
||||
echo "[init-nix] WARNING: Nix installation finished in container, but 'nix' is still not on PATH."
|
||||
echo "[init-nix] Nix found after adjusting PATH: $(command -v nix)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Optionally add PATH hints to /etc/profile (best effort)
|
||||
if [[ -w /etc/profile ]]; then
|
||||
if ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then
|
||||
cat <<'EOF' >> /etc/profile
|
||||
echo "[init-nix] Nix not found, starting installation logic..."
|
||||
|
||||
# Nix profiles (added by package-manager init-nix.sh)
|
||||
if [ -d /nix/var/nix/profiles/default/bin ]; then
|
||||
PATH="/nix/var/nix/profiles/default/bin:$PATH"
|
||||
fi
|
||||
if [ -d "$HOME/.nix-profile/bin" ]; then
|
||||
PATH="$HOME/.nix-profile/bin:$PATH"
|
||||
fi
|
||||
EOF
|
||||
echo "[init-nix] Appended Nix PATH setup to /etc/profile (container mode)."
|
||||
local IN_CONTAINER=0
|
||||
if is_container; then
|
||||
IN_CONTAINER=1
|
||||
echo "[init-nix] Detected container environment."
|
||||
else
|
||||
echo "[init-nix] No container detected."
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Container + root: dedicated "nix" user, single-user install
|
||||
# -------------------------------------------------------------------------
|
||||
if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
||||
echo "[init-nix] Container + root – installing as 'nix' user (single-user)."
|
||||
|
||||
ensure_nix_build_group
|
||||
|
||||
if ! id nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] Creating user 'nix'..."
|
||||
local BASH_SHELL
|
||||
BASH_SHELL="$(command -v bash || true)"
|
||||
[[ -z "${BASH_SHELL}" ]] && BASH_SHELL="/bin/sh"
|
||||
useradd -m -r -g nixbld -s "${BASH_SHELL}" nix
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[init-nix] Nix initialization complete (container root mode)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Non-container or non-root container: normal installer paths
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ "${IN_CONTAINER}" -eq 0 ]]; then
|
||||
# Real host
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
echo "[init-nix] Host with systemd – using multi-user install (--daemon)."
|
||||
sh <(curl -L https://nixos.org/nix/install) --daemon
|
||||
else
|
||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||
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."
|
||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||
if [[ ! -d /nix ]]; then
|
||||
echo "[init-nix] Creating /nix with owner nix:nixbld..."
|
||||
mkdir -m 0755 /nix
|
||||
chown nix:nixbld /nix
|
||||
else
|
||||
echo "[init-nix] Non-root host without systemd – using single-user install (--no-daemon)."
|
||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||
local current_owner current_group
|
||||
current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')"
|
||||
current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')"
|
||||
if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then
|
||||
echo "[init-nix] Fixing /nix ownership from ${current_owner}:${current_group} to nix:nixbld..."
|
||||
chown -R nix:nixbld /nix
|
||||
fi
|
||||
if [[ ! -w /nix ]]; then
|
||||
echo "[init-nix] WARNING: /nix is not writable after chown; Nix installer may fail."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
|
||||
install_nix_with_retry "no-daemon" "nix"
|
||||
|
||||
ensure_nix_on_path
|
||||
|
||||
if [[ -x /home/nix/.nix-profile/bin/nix && ! -e /usr/local/bin/nix ]]; then
|
||||
echo "[init-nix] Creating /usr/local/bin/nix symlink -> /home/nix/.nix-profile/bin/nix"
|
||||
ln -s /home/nix/.nix-profile/bin/nix /usr/local/bin/nix
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Host (no container)
|
||||
# -------------------------------------------------------------------------
|
||||
elif [[ "${IN_CONTAINER}" -eq 0 ]]; then
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
echo "[init-nix] Host with systemd – using multi-user install (--daemon)."
|
||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||
ensure_nix_build_group
|
||||
fi
|
||||
install_nix_with_retry "daemon"
|
||||
else
|
||||
if [[ "${EUID:-0}" -eq 0 ]]; then
|
||||
echo "[init-nix] Host without systemd as root – using single-user install (--no-daemon)."
|
||||
ensure_nix_build_group
|
||||
else
|
||||
echo "[init-nix] Host without systemd as non-root – using single-user install (--no-daemon)."
|
||||
fi
|
||||
install_nix_with_retry "no-daemon"
|
||||
fi
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Container, but not root (rare)
|
||||
echo "[init-nix] Container as non-root user – using single-user install (--no-daemon)."
|
||||
sh <(curl -L https://nixos.org/nix/install) --no-daemon
|
||||
fi
|
||||
# -------------------------------------------------------------------------
|
||||
else
|
||||
echo "[init-nix] Container as non-root – using single-user install (--no-daemon)."
|
||||
install_nix_with_retry "no-daemon"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# After installation: fix PATH (runtime + shell profiles)
|
||||
# ---------------------------------------------------------------------------
|
||||
ensure_nix_on_path
|
||||
# -------------------------------------------------------------------------
|
||||
# After installation: PATH + /etc/profile
|
||||
# -------------------------------------------------------------------------
|
||||
ensure_nix_on_path
|
||||
|
||||
if ! command -v nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] WARNING: Nix installation finished, but 'nix' is still not on PATH."
|
||||
echo "[init-nix] You may need to source your shell profile manually."
|
||||
exit 0
|
||||
fi
|
||||
if ! command -v nix >/dev/null 2>&1; then
|
||||
echo "[init-nix] WARNING: Nix installation finished, but 'nix' is still not on PATH."
|
||||
echo "[init-nix] You may need to source your shell profile manually."
|
||||
else
|
||||
echo "[init-nix] Nix successfully installed at: $(command -v nix)"
|
||||
fi
|
||||
|
||||
echo "[init-nix] Nix successfully installed at: $(command -v nix)"
|
||||
|
||||
# Update global /etc/profile if writable (helps especially on minimal systems)
|
||||
if [[ -w /etc/profile ]]; then
|
||||
if ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then
|
||||
if [[ -w /etc/profile ]] && ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then
|
||||
cat <<'EOF' >> /etc/profile
|
||||
|
||||
# Nix profiles (added by package-manager init-nix.sh)
|
||||
@@ -232,6 +239,8 @@ fi
|
||||
EOF
|
||||
echo "[init-nix] Appended Nix PATH setup to /etc/profile"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[init-nix] Nix initialization complete."
|
||||
echo "[init-nix] Nix initialization complete."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -45,8 +45,42 @@ else
|
||||
fi
|
||||
|
||||
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
|
||||
|
||||
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
|
||||
SLEEP_INTERVAL=20
|
||||
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
|
||||
echo "[aur-builder-setup] yay already installed."
|
||||
fi
|
||||
|
||||
@@ -139,22 +139,27 @@ class NixFlakeInstaller(BaseInstaller):
|
||||
|
||||
for output, allow_failure in outputs:
|
||||
cmd = f"nix profile install {ctx.repo_dir}#{output}"
|
||||
print(f"[INFO] Running: {cmd}")
|
||||
ret = os.system(cmd)
|
||||
|
||||
try:
|
||||
run_command(
|
||||
cmd,
|
||||
cwd=ctx.repo_dir,
|
||||
preview=ctx.preview,
|
||||
allow_failure=allow_failure,
|
||||
)
|
||||
# Extract real exit code from os.system() result
|
||||
if os.WIFEXITED(ret):
|
||||
exit_code = os.WEXITSTATUS(ret)
|
||||
else:
|
||||
# abnormal termination (signal etc.) – keep raw value
|
||||
exit_code = ret
|
||||
|
||||
if exit_code == 0:
|
||||
print(f"Nix flake output '{output}' successfully installed.")
|
||||
except SystemExit as e:
|
||||
print(f"[Error] Failed to install Nix flake output '{output}': {e}")
|
||||
if not allow_failure:
|
||||
# Mandatory output failed → fatal for the pipeline.
|
||||
raise
|
||||
# Optional output failed → log and continue.
|
||||
print(
|
||||
"[Warning] Continuing despite failure to install "
|
||||
f"optional output '{output}'."
|
||||
)
|
||||
continue
|
||||
|
||||
print(f"[Error] Failed to install Nix flake output '{output}'")
|
||||
print(f"[Error] Command exited with code {exit_code}")
|
||||
|
||||
if not allow_failure:
|
||||
raise SystemExit(exit_code)
|
||||
|
||||
print(
|
||||
"[Warning] Continuing despite failure to install "
|
||||
f"optional output '{output}'."
|
||||
)
|
||||
|
||||
@@ -18,52 +18,17 @@ USER_CONFIG_PATH = os.path.expanduser("~/.config/pkgmgr/config.yaml")
|
||||
|
||||
DESCRIPTION_TEXT = """\
|
||||
\033[1;32mPackage Manager 🤖📦\033[0m
|
||||
\033[3mKevin's Package Manager is a multi-repository, multi-package, and multi-format
|
||||
development tool crafted by and designed for:\033[0m
|
||||
\033[1;34mKevin Veen-Birkenbach\033[0m
|
||||
\033[4mhttps://www.veen.world/\033[0m
|
||||
\033[3mKevin's multi-distro package and workflow manager.\033[0m
|
||||
\033[1;34mKevin Veen-Birkenbach\033[0m – \033[4mhttps://www.veen.world/\033[0m
|
||||
|
||||
\033[1mOverview:\033[0m
|
||||
A powerful toolchain that unifies and automates workflows across heterogeneous
|
||||
project ecosystems. pkgmgr is not only a package manager — it is a full
|
||||
developer-oriented orchestration tool.
|
||||
Built in \033[1;33mPython\033[0m on top of \033[1;33mNix flakes\033[0m to manage many
|
||||
repositories and packaging formats (pyproject.toml, flake.nix,
|
||||
PKGBUILD, debian, Ansible, …) with one CLI.
|
||||
|
||||
It automatically detects, merges, and processes metadata from multiple
|
||||
dependency formats, including:
|
||||
• \033[1;33mPython:\033[0m pyproject.toml, requirements.txt
|
||||
• \033[1;33mNix:\033[0m flake.nix
|
||||
• \033[1;33mArch Linux:\033[0m PKGBUILD
|
||||
• \033[1;33mAnsible:\033[0m requirements.yml
|
||||
|
||||
This allows pkgmgr to perform installation, updates, verification, dependency
|
||||
resolution, and synchronization across complex multi-repo environments — with a
|
||||
single unified command-line interface.
|
||||
|
||||
\033[1mDeveloper Tools:\033[0m
|
||||
pkgmgr includes an integrated toolbox to enhance daily development workflows:
|
||||
|
||||
• \033[1;33mVS Code integration:\033[0m Auto-generate and open multi-repo workspaces
|
||||
• \033[1;33mTerminal integration:\033[0m Open repositories in new GNOME Terminal tabs
|
||||
• \033[1;33mExplorer integration:\033[0m Open repositories in your file manager
|
||||
• \033[1;33mRelease automation:\033[0m Version bumping, changelog updates, and tagging
|
||||
• \033[1;33mBatch operations:\033[0m Execute shell commands across multiple repositories
|
||||
• \033[1;33mGit/Docker/Make wrappers:\033[0m Unified command proxying for many tools
|
||||
|
||||
\033[1mCapabilities:\033[0m
|
||||
• Clone, pull, verify, update, and manage many repositories at once
|
||||
• Resolve dependencies across languages and ecosystems
|
||||
• Standardize install/update workflows
|
||||
• Create symbolic executable wrappers for any project
|
||||
• Merge configuration from default + user config layers
|
||||
|
||||
Use pkgmgr as both a robust package management framework and a versatile
|
||||
development orchestration tool.
|
||||
|
||||
For detailed help on each command, use:
|
||||
\033[1mpkgmgr <command> --help\033[0m
|
||||
For details on any command, run:
|
||||
\033[1mpkgmgr <command> --help\033[0m
|
||||
"""
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Entry point for the pkgmgr CLI.
|
||||
|
||||
@@ -1,140 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock, patch
|
||||
"""
|
||||
Unit tests for NixFlakeInstaller using unittest (no pytest).
|
||||
|
||||
Covers:
|
||||
- Successful installation (exit_code == 0)
|
||||
- Mandatory failure → SystemExit with correct code
|
||||
- Optional failure (pkgmgr default) → no raise, but warning
|
||||
- supports() behavior incl. PKGMGR_DISABLE_NIX_FLAKE_INSTALLER
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from unittest.mock import patch
|
||||
|
||||
from pkgmgr.actions.install.context import RepoContext
|
||||
from pkgmgr.actions.install.installers.nix_flake import NixFlakeInstaller
|
||||
|
||||
|
||||
class DummyCtx:
|
||||
"""Minimal context object to satisfy NixFlakeInstaller.run() / supports()."""
|
||||
|
||||
def __init__(self, identifier: str, repo_dir: str, preview: bool = False):
|
||||
self.identifier = identifier
|
||||
self.repo_dir = repo_dir
|
||||
self.preview = preview
|
||||
|
||||
|
||||
class TestNixFlakeInstaller(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.repo = {"repository": "package-manager"}
|
||||
# Important: identifier "pkgmgr" triggers both "pkgmgr" and "default"
|
||||
self.ctx = RepoContext(
|
||||
repo=self.repo,
|
||||
identifier="pkgmgr",
|
||||
repo_dir="/tmp/repo",
|
||||
repositories_base_dir="/tmp",
|
||||
bin_dir="/bin",
|
||||
all_repos=[self.repo],
|
||||
no_verification=False,
|
||||
preview=False,
|
||||
quiet=False,
|
||||
clone_mode="ssh",
|
||||
update_dependencies=False,
|
||||
)
|
||||
self.installer = NixFlakeInstaller()
|
||||
# Create a temporary repository directory with a flake.nix file
|
||||
self._tmpdir = tempfile.mkdtemp(prefix="nix_flake_test_")
|
||||
self.repo_dir = self._tmpdir
|
||||
flake_path = os.path.join(self.repo_dir, "flake.nix")
|
||||
with open(flake_path, "w", encoding="utf-8") as f:
|
||||
f.write("{}\n")
|
||||
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.os.path.exists")
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.shutil.which")
|
||||
def test_supports_true_when_nix_and_flake_exist(
|
||||
self,
|
||||
mock_which: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
) -> None:
|
||||
mock_which.return_value = "/usr/bin/nix"
|
||||
mock_exists.return_value = True
|
||||
# Ensure the disable env var is not set by default
|
||||
os.environ.pop("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER", None)
|
||||
|
||||
with patch.dict(os.environ, {"PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""}, clear=False):
|
||||
self.assertTrue(self.installer.supports(self.ctx))
|
||||
def tearDown(self) -> None:
|
||||
# Cleanup temporary directory
|
||||
if os.path.isdir(self._tmpdir):
|
||||
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
||||
|
||||
mock_which.assert_called_once_with("nix")
|
||||
mock_exists.assert_called_once_with(
|
||||
os.path.join(self.ctx.repo_dir, self.installer.FLAKE_FILE)
|
||||
)
|
||||
def _enable_nix_in_module(self, which_patch):
|
||||
"""Ensure shutil.which('nix') in nix_flake module returns a path."""
|
||||
which_patch.return_value = "/usr/bin/nix"
|
||||
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.os.path.exists")
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.shutil.which")
|
||||
def test_supports_false_when_nix_missing(
|
||||
self,
|
||||
mock_which: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
) -> None:
|
||||
mock_which.return_value = None
|
||||
mock_exists.return_value = True # flake exists but nix is missing
|
||||
|
||||
with patch.dict(os.environ, {"PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""}, clear=False):
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.os.path.exists")
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.shutil.which")
|
||||
def test_supports_false_when_disabled_via_env(
|
||||
self,
|
||||
mock_which: MagicMock,
|
||||
mock_exists: MagicMock,
|
||||
) -> None:
|
||||
mock_which.return_value = "/usr/bin/nix"
|
||||
mock_exists.return_value = True
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": "1"},
|
||||
clear=False,
|
||||
):
|
||||
self.assertFalse(self.installer.supports(self.ctx))
|
||||
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.NixFlakeInstaller.supports")
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.run_command")
|
||||
def test_run_removes_old_profile_and_installs_outputs(
|
||||
self,
|
||||
mock_run_command: MagicMock,
|
||||
mock_supports: MagicMock,
|
||||
) -> None:
|
||||
def test_nix_flake_run_success(self):
|
||||
"""
|
||||
run() should:
|
||||
- remove the old profile
|
||||
- install both 'pkgmgr' and 'default' outputs for identifier 'pkgmgr'
|
||||
- call commands in the correct order
|
||||
When os.system returns a successful exit code, the installer
|
||||
should report success and not raise.
|
||||
"""
|
||||
mock_supports.return_value = True
|
||||
ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir)
|
||||
|
||||
commands: list[str] = []
|
||||
installer = NixFlakeInstaller()
|
||||
|
||||
def side_effect(cmd: str, cwd: str | None = None, preview: bool = False, **_: object) -> None:
|
||||
commands.append(cmd)
|
||||
buf = io.StringIO()
|
||||
with patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.shutil.which"
|
||||
) as which_mock, patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.os.system"
|
||||
) as system_mock, redirect_stdout(buf):
|
||||
self._enable_nix_in_module(which_mock)
|
||||
|
||||
mock_run_command.side_effect = side_effect
|
||||
# Simulate os.system returning success (exit code 0)
|
||||
system_mock.return_value = 0
|
||||
|
||||
with patch.dict(os.environ, {"PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""}, clear=False):
|
||||
self.installer.run(self.ctx)
|
||||
# Sanity: supports() must be True
|
||||
self.assertTrue(installer.supports(ctx))
|
||||
|
||||
remove_cmd = f"nix profile remove {self.installer.PROFILE_NAME} || true"
|
||||
install_pkgmgr_cmd = f"nix profile install {self.ctx.repo_dir}#pkgmgr"
|
||||
install_default_cmd = f"nix profile install {self.ctx.repo_dir}#default"
|
||||
installer.run(ctx)
|
||||
|
||||
self.assertIn(remove_cmd, commands)
|
||||
self.assertIn(install_pkgmgr_cmd, commands)
|
||||
self.assertIn(install_default_cmd, commands)
|
||||
out = buf.getvalue()
|
||||
self.assertIn("[INFO] Running: nix profile install", out)
|
||||
self.assertIn("Nix flake output 'default' successfully installed.", out)
|
||||
|
||||
self.assertEqual(commands[0], remove_cmd)
|
||||
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.shutil.which")
|
||||
@patch("pkgmgr.actions.install.installers.nix_flake.run_command")
|
||||
def test_ensure_old_profile_removed_ignores_systemexit(
|
||||
self,
|
||||
mock_run_command: MagicMock,
|
||||
mock_which: MagicMock,
|
||||
) -> None:
|
||||
mock_which.return_value = "/usr/bin/nix"
|
||||
|
||||
def side_effect(cmd: str, cwd: str | None = None, preview: bool = False, **_: object) -> None:
|
||||
raise SystemExit(1)
|
||||
|
||||
mock_run_command.side_effect = side_effect
|
||||
|
||||
self.installer._ensure_old_profile_removed(self.ctx)
|
||||
|
||||
remove_cmd = f"nix profile remove {self.installer.PROFILE_NAME} || true"
|
||||
mock_run_command.assert_called_with(
|
||||
remove_cmd,
|
||||
cwd=self.ctx.repo_dir,
|
||||
preview=self.ctx.preview,
|
||||
# Ensure the nix command was actually invoked
|
||||
system_mock.assert_called_with(
|
||||
f"nix profile install {self.repo_dir}#default"
|
||||
)
|
||||
|
||||
def test_nix_flake_run_mandatory_failure_raises(self):
|
||||
"""
|
||||
For a generic repository (identifier not pkgmgr/package-manager),
|
||||
`default` is mandatory and a non-zero exit code should raise SystemExit
|
||||
with the real exit code (e.g. 1, not 256).
|
||||
"""
|
||||
ctx = DummyCtx(identifier="some-lib", repo_dir=self.repo_dir)
|
||||
installer = NixFlakeInstaller()
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.shutil.which"
|
||||
) as which_mock, patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.os.system"
|
||||
) as system_mock, redirect_stdout(buf):
|
||||
self._enable_nix_in_module(which_mock)
|
||||
|
||||
# Simulate os.system returning encoded status for exit code 1
|
||||
# os.system encodes exit code as (exit_code << 8)
|
||||
system_mock.return_value = 1 << 8
|
||||
|
||||
self.assertTrue(installer.supports(ctx))
|
||||
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
installer.run(ctx)
|
||||
|
||||
# The real exit code should be 1 (not 256)
|
||||
self.assertEqual(cm.exception.code, 1)
|
||||
|
||||
out = buf.getvalue()
|
||||
self.assertIn("[INFO] Running: nix profile install", out)
|
||||
self.assertIn("[Error] Failed to install Nix flake output 'default'", out)
|
||||
self.assertIn("[Error] Command exited with code 1", out)
|
||||
|
||||
def test_nix_flake_run_optional_failure_does_not_raise(self):
|
||||
"""
|
||||
For the package-manager repository, the 'default' output is optional.
|
||||
Failure to install it must not raise, but should log a warning instead.
|
||||
"""
|
||||
ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir)
|
||||
installer = NixFlakeInstaller()
|
||||
|
||||
calls = []
|
||||
|
||||
def fake_system(cmd: str) -> int:
|
||||
calls.append(cmd)
|
||||
# First call (pkgmgr) → success
|
||||
if len(calls) == 1:
|
||||
return 0
|
||||
# Second call (default) → failure (exit code 1 encoded)
|
||||
return 1 << 8
|
||||
|
||||
buf = io.StringIO()
|
||||
with patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.shutil.which"
|
||||
) as which_mock, patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.os.system",
|
||||
side_effect=fake_system,
|
||||
), redirect_stdout(buf):
|
||||
self._enable_nix_in_module(which_mock)
|
||||
|
||||
self.assertTrue(installer.supports(ctx))
|
||||
|
||||
# Optional failure must NOT raise
|
||||
installer.run(ctx)
|
||||
|
||||
out = buf.getvalue()
|
||||
|
||||
# Both outputs should have been mentioned
|
||||
self.assertIn(
|
||||
"attempting to install profile outputs: pkgmgr, default", out
|
||||
)
|
||||
|
||||
# First output ("pkgmgr") succeeded
|
||||
self.assertIn(
|
||||
"Nix flake output 'pkgmgr' successfully installed.", out
|
||||
)
|
||||
|
||||
# Second output ("default") failed but did not raise
|
||||
self.assertIn(
|
||||
"[Error] Failed to install Nix flake output 'default'", out
|
||||
)
|
||||
self.assertIn("[Error] Command exited with code 1", out)
|
||||
self.assertIn(
|
||||
"Continuing despite failure to install optional output 'default'.",
|
||||
out,
|
||||
)
|
||||
|
||||
# Ensure we actually called os.system twice (pkgmgr and default)
|
||||
self.assertEqual(len(calls), 2)
|
||||
self.assertIn(
|
||||
f"nix profile install {self.repo_dir}#pkgmgr",
|
||||
calls[0],
|
||||
)
|
||||
self.assertIn(
|
||||
f"nix profile install {self.repo_dir}#default",
|
||||
calls[1],
|
||||
)
|
||||
|
||||
def test_nix_flake_supports_respects_disable_env(self):
|
||||
"""
|
||||
PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 must disable the installer,
|
||||
even if flake.nix exists and nix is available.
|
||||
"""
|
||||
ctx = DummyCtx(identifier="pkgmgr", repo_dir=self.repo_dir)
|
||||
installer = NixFlakeInstaller()
|
||||
|
||||
with patch(
|
||||
"pkgmgr.actions.install.installers.nix_flake.shutil.which"
|
||||
) as which_mock:
|
||||
self._enable_nix_in_module(which_mock)
|
||||
os.environ["PKGMGR_DISABLE_NIX_FLAKE_INSTALLER"] = "1"
|
||||
|
||||
self.assertFalse(installer.supports(ctx))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user