Compare commits

...

9 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
6c116a029e Release version 0.10.2
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-11 20:16:59 +01:00
Kevin Veen-Birkenbach
3eb7c81fa1 **Mark stable only on highest version tag**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Updated the `mark-stable` workflow so that the `stable` tag is only moved when:

* the current push is a version tag (`v*`)
* all tests have passed
* the pushed version tag is the highest semantic version among all existing tags

This ensures that `stable` always reflects the latest valid release and prevents older version tags from overwriting it.

https://chatgpt.com/share/693b163b-0c34-800f-adcb-12cf4744dbe2
2025-12-11 20:06:22 +01:00
Kevin Veen-Birkenbach
0334f477fd Release version 0.10.2
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-11 20:01:29 +01:00
Kevin Veen-Birkenbach
8bb99c99b7 refactor(init-nix): unify installer logic and add robust retry handling
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Refactored the Nix initialization script to reduce duplicated code and
centralize the installation workflow. The core functionality remains
unchanged, but all installer calls now use a unified function with retry
support to ensure resilient downloads in CI and container environments.

Key improvements:
- Added download retry logic (5 minutes total, 20-second intervals)
- Consolidated installer invocation into `install_nix_with_retry`
- Reduced code duplication across container/host install paths
- Preserved existing installation behavior for all environments
- Maintained `nixbld` group and build-user handling
- Improved consistency and readability without altering semantics

This prevents intermittent failures such as:
“curl: (6) Could not resolve host: nixos.org”
and ensures stable, deterministic Nix setup in CI pipelines.

https://chatgpt.com/share/693b13ce-fdcc-800f-a7bc-81c67478edff
2025-12-11 19:56:10 +01:00
Kevin Veen-Birkenbach
587cb2e516 Removed comments
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-11 19:44:36 +01:00
Kevin Veen-Birkenbach
fcf9d4b59b **Aur builder: add retry logic for yay clone to recover from GitHub 504 errors**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Implemented a robust retry mechanism for cloning the yay AUR helper during Arch dependency installation.
The new logic retries the git clone operation for up to 5 minutes with a 20-second pause between attempts, allowing the build to proceed even when GitHub intermittently returns HTTP 504 errors.

This improves the stability of Arch container builds, especially under network pressure or transient upstream outages.
The yay build process now only starts once the clone step completes successfully.

https://chatgpt.com/share/693b102b-fdb0-800f-9f2e-d4840f14d329
2025-12-11 19:40:25 +01:00
Kevin Veen-Birkenbach
b483dbfaad **fix(init-nix): ensure nixbld group/users exist on Ubuntu root-without-systemd installs**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Implement `ensure_nix_build_group()` and use it in all code paths where Nix is installed as root.
This resolves Nix installation failures on Ubuntu containers (root, no systemd) where the installer aborts with:

```
error: the group 'nixbld' specified in 'build-users-group' does not exist
```

The fix standardizes creation of the `nixbld` group and `nixbld1..10` build users across:

* container root mode
* systemd host daemon installs
* root-on-host without systemd (Debian/Ubuntu CI case)

This makes Nix initialization deterministic across all test distros and fixes failing Ubuntu E2E runs.

https://chatgpt.com/share/693b0e1a-e5d4-800f-8a89-7d91108b0368
2025-12-11 19:31:25 +01:00
Kevin Veen-Birkenbach
9630917570 **refactor(nix-flake): replace run_command wrapper with direct os.system execution and extend test coverage**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
This commit removes the `run_command`-based execution model for Nix flake
installations and replaces it with a direct `os.system` invocation.
This ensures that *all* Nix diagnostics (stdout/stderr) are fully visible and
no longer suppressed by wrapper logic.

Key changes:

* Directly run `nix profile install` via `os.system` for full error output
* Correctly decode real exit codes via `os.WIFEXITED` / `os.WEXITSTATUS`
* Preserve mandatory/optional behavior for flake outputs
* Update unit tests to the new execution model using `unittest`
* Add complete coverage for:

  * successful installs
  * mandatory failures → raise SystemExit(code)
  * optional failures → warn and continue
  * environment-based disabling via `PKGMGR_DISABLE_NIX_FLAKE_INSTALLER`
* Remove obsolete mocks and legacy test logic that assumed `run_command`

Overall, this improves transparency, debuggability, and correctness of the
Nix flake installer while maintaining full backward compatibility at the
interface level.

https://chatgpt.com/share/693b0a20-99f4-800f-b789-b00a50413612
2025-12-11 19:14:25 +01:00
Kevin Veen-Birkenbach
6a4432dd04 Added required sudo to debian
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-container (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-11 18:42:33 +01:00
9 changed files with 446 additions and 291 deletions

View File

@@ -3,7 +3,9 @@ name: Mark stable commit
on: on:
push: push:
branches: branches:
- main - main # still run tests for main
tags:
- 'v*' # run tests for version tags (e.g. v0.9.1)
jobs: jobs:
test-unit: test-unit:
@@ -34,31 +36,63 @@ jobs:
- test-virgin-root - test-virgin-root
runs-on: ubuntu-latest 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: permissions:
contents: write # to move the tag contents: write # Required to move/update the tag
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 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: | run: |
set -euo pipefail set -euo pipefail
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com" 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 git tag -d stable 2>/dev/null || true
# delete remote tag if exists
git push origin :refs/tags/stable || true git push origin :refs/tags/stable || true
# create new tag on this commit # Create new stable tag
git tag stable "$GITHUB_SHA" git tag stable "$GITHUB_SHA"
git push origin stable git push origin stable
echo "✅ Stable tag updated." echo "✅ Stable tag updated to ${VERSION}."

View File

@@ -1,3 +1,12 @@
## [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 ## [0.10.1] - 2025-12-11
* Fixed Debian\Ubuntu to pass container e2e tests * Fixed Debian\Ubuntu to pass container e2e tests
@@ -5,8 +14,6 @@
## [0.10.0] - 2025-12-11 ## [0.10.0] - 2025-12-11
* **Changes since v0.9.1**
**Mirror System** **Mirror System**
* Added SSH mirror support including multi-push and remote probing * Added SSH mirror support including multi-push and remote probing

View File

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

View File

@@ -9,7 +9,7 @@ Homepage: https://github.com/kevinveenbirkenbach/package-manager
Package: package-manager Package: package-manager
Architecture: any Architecture: any
Depends: ${misc:Depends} Depends: sudo, ${misc:Depends}
Description: Wrapper that runs Kevin's package-manager via Nix flake Description: Wrapper that runs Kevin's package-manager via Nix flake
This package provides the `pkgmgr` command, which runs Kevin's package This package provides the `pkgmgr` command, which runs Kevin's package
manager via a local Nix flake manager via a local Nix flake

View File

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

View File

@@ -3,21 +3,22 @@ set -euo pipefail
echo "[init-nix] Starting Nix initialization..." 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() { is_container() {
# Docker / Podman markers
if [[ -f /.dockerenv ]] || [[ -f /run/.containerenv ]]; then if [[ -f /.dockerenv ]] || [[ -f /run/.containerenv ]]; then
return 0 return 0
fi fi
# cgroup hints
if grep -qiE 'docker|container|podman|lxc' /proc/1/cgroup 2>/dev/null; then if grep -qiE 'docker|container|podman|lxc' /proc/1/cgroup 2>/dev/null; then
return 0 return 0
fi fi
# Environment variable used by some runtimes
if [[ -n "${container:-}" ]]; then if [[ -n "${container:-}" ]]; then
return 0 return 0
fi 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() { ensure_nix_on_path() {
# Multi-user profile (daemon install)
if [[ -x /nix/var/nix/profiles/default/bin/nix ]]; then if [[ -x /nix/var/nix/profiles/default/bin/nix ]]; then
export PATH="/nix/var/nix/profiles/default/bin:${PATH}" export PATH="/nix/var/nix/profiles/default/bin:${PATH}"
fi fi
# Single-user profile (current user)
if [[ -x "${HOME}/.nix-profile/bin/nix" ]]; then if [[ -x "${HOME}/.nix-profile/bin/nix" ]]; then
export PATH="${HOME}/.nix-profile/bin:${PATH}" export PATH="${HOME}/.nix-profile/bin:${PATH}"
fi fi
# Single-user profile for dedicated "nix" user (container case)
if [[ -x /home/nix/.nix-profile/bin/nix ]]; then if [[ -x /home/nix/.nix-profile/bin/nix ]]; then
export PATH="/home/nix/.nix-profile/bin:${PATH}" export PATH="/home/nix/.nix-profile/bin:${PATH}"
fi 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 ensure_nix_build_group() {
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)
if ! getent group nixbld >/dev/null 2>&1; then if ! getent group nixbld >/dev/null 2>&1; then
echo "[init-nix] Creating group 'nixbld'..." echo "[init-nix] Creating group 'nixbld'..."
groupadd -r nixbld groupadd -r nixbld
fi fi
# Ensure Nix build users (nixbld1..nixbld10) as members of nixbld
for i in $(seq 1 10); do for i in $(seq 1 10); do
if ! id "nixbld$i" >/dev/null 2>&1; then if ! id "nixbld$i" >/dev/null 2>&1; then
echo "[init-nix] Creating build user nixbld$i..." 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" useradd -r -g nixbld -G nixbld -s /usr/sbin/nologin "nixbld$i"
fi fi
done done
}
# Ensure "nix" user (home at /home/nix) # ---------------------------------------------------------------------------
if ! id nix >/dev/null 2>&1; then # Download and run Nix installer with retry
echo "[init-nix] Creating user 'nix'..." # Usage: install_nix_with_retry daemon|no-daemon [run_as_user]
# Resolve a valid shell path across distros: # ---------------------------------------------------------------------------
# - Debian/Ubuntu: /bin/bash install_nix_with_retry() {
# - Arch: /usr/bin/bash (often symlinked) local mode="$1"
# Fall back to /bin/sh on ultra-minimal systems. local run_as="${2:-}"
BASH_SHELL="$(command -v bash || true)" local installer elapsed=0 mode_flag
if [[ -z "${BASH_SHELL}" ]]; then
BASH_SHELL="/bin/sh" case "${mode}" in
fi daemon) mode_flag="--daemon" ;;
useradd -m -r -g nixbld -s "${BASH_SHELL}" nix 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 fi
# Ensure /nix exists and is writable by the "nix" user. local curl_exit=$?
# echo "[init-nix] WARNING: Failed to download Nix installer (curl exit code ${curl_exit})."
# 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: elapsed=$((elapsed + NIX_DOWNLOAD_SLEEP_INTERVAL))
# if (( elapsed >= NIX_DOWNLOAD_MAX_TIME )); then
# "directory /nix exists, but is not writable by you" echo "[init-nix] ERROR: Giving up after ${elapsed}s trying to download Nix installer."
# rm -f "${installer}"
# To keep container runs idempotent and robust, we always enforce exit 1
# 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
else
echo "[init-nix] /nix already exists with correct owner nix:nixbld."
fi fi
if [[ ! -w /nix ]]; then echo "[init-nix] Retrying in ${NIX_DOWNLOAD_SLEEP_INTERVAL}s (elapsed: ${elapsed}s/${NIX_DOWNLOAD_MAX_TIME}s)..."
echo "[init-nix] WARNING: /nix is still not writable after chown; Nix installer may fail." sleep "${NIX_DOWNLOAD_SLEEP_INTERVAL}"
fi done
fi
# Run Nix single-user installer as "nix" if [[ -n "${run_as}" ]]; then
echo "[init-nix] Installing Nix as user 'nix' (single-user, --no-daemon)..." echo "[init-nix] Running installer as user '${run_as}' with mode '${mode}'..."
if command -v sudo >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then
sudo -u nix bash -lc 'sh <(curl -L https://nixos.org/nix/install) --no-daemon' sudo -u "${run_as}" bash -lc "sh '${installer}' ${mode_flag}"
else else
su - nix -c 'sh <(curl -L https://nixos.org/nix/install) --no-daemon' su - "${run_as}" -c "sh '${installer}' ${mode_flag}"
fi
else
echo "[init-nix] Running installer as current user with mode '${mode}'..."
sh "${installer}" "${mode_flag}"
fi fi
# After installation, expose nix to root via PATH and symlink rm -f "${installer}"
ensure_nix_on_path }
if [[ -x /home/nix/.nix-profile/bin/nix ]]; then # ---------------------------------------------------------------------------
if [[ ! -e /usr/local/bin/nix ]]; then # Main
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 main() {
fi # 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 fi
ensure_nix_on_path ensure_nix_on_path
if command -v nix >/dev/null 2>&1; then if command -v nix >/dev/null 2>&1; then
echo "[init-nix] Nix successfully installed (container mode) at: $(command -v nix)" echo "[init-nix] Nix found after adjusting PATH: $(command -v nix)"
return 0
fi
echo "[init-nix] Nix not found, starting installation logic..."
local IN_CONTAINER=0
if is_container; then
IN_CONTAINER=1
echo "[init-nix] Detected container environment."
else else
echo "[init-nix] WARNING: Nix installation finished in container, but 'nix' is still not on PATH." echo "[init-nix] No container detected."
fi fi
# Optionally add PATH hints to /etc/profile (best effort) # -------------------------------------------------------------------------
if [[ -w /etc/profile ]]; then # Container + root: dedicated "nix" user, single-user install
if ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then # -------------------------------------------------------------------------
cat <<'EOF' >> /etc/profile if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
echo "[init-nix] Container + root installing as 'nix' user (single-user)."
# Nix profiles (added by package-manager init-nix.sh) ensure_nix_build_group
if [ -d /nix/var/nix/profiles/default/bin ]; then
PATH="/nix/var/nix/profiles/default/bin:$PATH" if ! id nix >/dev/null 2>&1; then
fi echo "[init-nix] Creating user 'nix'..."
if [ -d "$HOME/.nix-profile/bin" ]; then local BASH_SHELL
PATH="$HOME/.nix-profile/bin:$PATH" BASH_SHELL="$(command -v bash || true)"
fi [[ -z "${BASH_SHELL}" ]] && BASH_SHELL="/bin/sh"
EOF useradd -m -r -g nixbld -s "${BASH_SHELL}" nix
echo "[init-nix] Appended Nix PATH setup to /etc/profile (container mode)." fi
if [[ ! -d /nix ]]; then
echo "[init-nix] Creating /nix with owner nix:nixbld..."
mkdir -m 0755 /nix
chown nix:nixbld /nix
else
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 fi
echo "[init-nix] Nix initialization complete (container root mode)." install_nix_with_retry "no-daemon" "nix"
exit 0
fi
# --------------------------------------------------------------------------- ensure_nix_on_path
# Non-container or non-root container: normal installer paths
# --------------------------------------------------------------------------- if [[ -x /home/nix/.nix-profile/bin/nix && ! -e /usr/local/bin/nix ]]; then
if [[ "${IN_CONTAINER}" -eq 0 ]]; then echo "[init-nix] Creating /usr/local/bin/nix symlink -> /home/nix/.nix-profile/bin/nix"
# Real host 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 if command -v systemctl >/dev/null 2>&1; then
echo "[init-nix] Host with systemd using multi-user install (--daemon)." echo "[init-nix] Host with systemd using multi-user install (--daemon)."
sh <(curl -L https://nixos.org/nix/install) --daemon if [[ "${EUID:-0}" -eq 0 ]]; then
ensure_nix_build_group
fi
install_nix_with_retry "daemon"
else else
if [[ "${EUID:-0}" -eq 0 ]]; then if [[ "${EUID:-0}" -eq 0 ]]; then
echo "[init-nix] WARNING: Running as root without systemd on host." echo "[init-nix] Host without systemd as root using single-user install (--no-daemon)."
echo "[init-nix] Falling back to single-user install (--no-daemon), but this is not recommended." ensure_nix_build_group
sh <(curl -L https://nixos.org/nix/install) --no-daemon
else else
echo "[init-nix] Non-root host without systemd using single-user install (--no-daemon)." echo "[init-nix] Host without systemd as non-root using single-user install (--no-daemon)."
sh <(curl -L https://nixos.org/nix/install) --no-daemon
fi fi
install_nix_with_retry "no-daemon"
fi fi
else
# -------------------------------------------------------------------------
# Container, but not root (rare) # 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 else
fi 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) # After installation: PATH + /etc/profile
# --------------------------------------------------------------------------- # -------------------------------------------------------------------------
ensure_nix_on_path ensure_nix_on_path
if ! command -v nix >/dev/null 2>&1; then 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] WARNING: Nix installation finished, but 'nix' is still not on PATH."
echo "[init-nix] You may need to source your shell profile manually." echo "[init-nix] You may need to source your shell profile manually."
exit 0 else
fi echo "[init-nix] Nix successfully installed at: $(command -v nix)"
fi
echo "[init-nix] Nix successfully installed at: $(command -v nix)" if [[ -w /etc/profile ]] && ! grep -q 'Nix profiles' /etc/profile 2>/dev/null; then
# 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
cat <<'EOF' >> /etc/profile cat <<'EOF' >> /etc/profile
# Nix profiles (added by package-manager init-nix.sh) # Nix profiles (added by package-manager init-nix.sh)
@@ -232,6 +239,8 @@ fi
EOF EOF
echo "[init-nix] Appended Nix PATH setup to /etc/profile" echo "[init-nix] Appended Nix PATH setup to /etc/profile"
fi fi
fi
echo "[init-nix] Nix initialization complete." echo "[init-nix] Nix initialization complete."
}
main "$@"

View File

@@ -45,8 +45,42 @@ else
fi fi
echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..." echo "[aur-builder-setup] Ensuring yay is installed for aur_builder..."
if ! "${RUN_AS_AUR[@]}" 'command -v yay >/dev/null 2>&1'; then if ! "${RUN_AS_AUR[@]}" 'command -v yay >/dev/null 2>&1'; then
"${RUN_AS_AUR[@]}" 'cd ~ && rm -rf yay && git clone https://aur.archlinux.org/yay.git && cd yay && makepkg -si --noconfirm' echo "[aur-builder-setup] yay not found starting retry sequence for download..."
MAX_TIME=300
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 else
echo "[aur-builder-setup] yay already installed." echo "[aur-builder-setup] yay already installed."
fi fi

View File

@@ -139,21 +139,26 @@ class NixFlakeInstaller(BaseInstaller):
for output, allow_failure in outputs: for output, allow_failure in outputs:
cmd = f"nix profile install {ctx.repo_dir}#{output}" cmd = f"nix profile install {ctx.repo_dir}#{output}"
print(f"[INFO] Running: {cmd}")
ret = os.system(cmd)
try: # Extract real exit code from os.system() result
run_command( if os.WIFEXITED(ret):
cmd, exit_code = os.WEXITSTATUS(ret)
cwd=ctx.repo_dir, else:
preview=ctx.preview, # abnormal termination (signal etc.) keep raw value
allow_failure=allow_failure, exit_code = ret
)
if exit_code == 0:
print(f"Nix flake output '{output}' successfully installed.") print(f"Nix flake output '{output}' successfully installed.")
except SystemExit as e: continue
print(f"[Error] Failed to install Nix flake output '{output}': {e}")
print(f"[Error] Failed to install Nix flake output '{output}'")
print(f"[Error] Command exited with code {exit_code}")
if not allow_failure: if not allow_failure:
# Mandatory output failed → fatal for the pipeline. raise SystemExit(exit_code)
raise
# Optional output failed → log and continue.
print( print(
"[Warning] Continuing despite failure to install " "[Warning] Continuing despite failure to install "
f"optional output '{output}'." f"optional output '{output}'."

View File

@@ -1,140 +1,206 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os """
import unittest Unit tests for NixFlakeInstaller using unittest (no pytest).
from unittest import mock
from unittest.mock import MagicMock, patch 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 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): class TestNixFlakeInstaller(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.repo = {"repository": "package-manager"} # Create a temporary repository directory with a flake.nix file
# Important: identifier "pkgmgr" triggers both "pkgmgr" and "default" self._tmpdir = tempfile.mkdtemp(prefix="nix_flake_test_")
self.ctx = RepoContext( self.repo_dir = self._tmpdir
repo=self.repo, flake_path = os.path.join(self.repo_dir, "flake.nix")
identifier="pkgmgr", with open(flake_path, "w", encoding="utf-8") as f:
repo_dir="/tmp/repo", f.write("{}\n")
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()
@patch("pkgmgr.actions.install.installers.nix_flake.os.path.exists") # Ensure the disable env var is not set by default
@patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") os.environ.pop("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER", None)
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
with patch.dict(os.environ, {"PKGMGR_DISABLE_NIX_FLAKE_INSTALLER": ""}, clear=False): def tearDown(self) -> None:
self.assertTrue(self.installer.supports(self.ctx)) # Cleanup temporary directory
if os.path.isdir(self._tmpdir):
shutil.rmtree(self._tmpdir, ignore_errors=True)
mock_which.assert_called_once_with("nix") def _enable_nix_in_module(self, which_patch):
mock_exists.assert_called_once_with( """Ensure shutil.which('nix') in nix_flake module returns a path."""
os.path.join(self.ctx.repo_dir, self.installer.FLAKE_FILE) which_patch.return_value = "/usr/bin/nix"
)
@patch("pkgmgr.actions.install.installers.nix_flake.os.path.exists") def test_nix_flake_run_success(self):
@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:
""" """
run() should: When os.system returns a successful exit code, the installer
- remove the old profile should report success and not raise.
- install both 'pkgmgr' and 'default' outputs for identifier 'pkgmgr'
- call commands in the correct order
""" """
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: buf = io.StringIO()
commands.append(cmd) 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): # Sanity: supports() must be True
self.installer.run(self.ctx) self.assertTrue(installer.supports(ctx))
remove_cmd = f"nix profile remove {self.installer.PROFILE_NAME} || true" installer.run(ctx)
install_pkgmgr_cmd = f"nix profile install {self.ctx.repo_dir}#pkgmgr"
install_default_cmd = f"nix profile install {self.ctx.repo_dir}#default"
self.assertIn(remove_cmd, commands) out = buf.getvalue()
self.assertIn(install_pkgmgr_cmd, commands) self.assertIn("[INFO] Running: nix profile install", out)
self.assertIn(install_default_cmd, commands) self.assertIn("Nix flake output 'default' successfully installed.", out)
self.assertEqual(commands[0], remove_cmd) # Ensure the nix command was actually invoked
system_mock.assert_called_with(
@patch("pkgmgr.actions.install.installers.nix_flake.shutil.which") f"nix profile install {self.repo_dir}#default"
@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,
) )
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__": if __name__ == "__main__":
unittest.main() unittest.main()