Compare commits
14 Commits
39b16b87a8
...
v0.10.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c116a029e | ||
|
|
3eb7c81fa1 | ||
|
|
0334f477fd | ||
|
|
8bb99c99b7 | ||
|
|
587cb2e516 | ||
|
|
fcf9d4b59b | ||
|
|
b483dbfaad | ||
|
|
9630917570 | ||
|
|
6a4432dd04 | ||
|
|
cfb91d825a | ||
|
|
a3b21f23fc | ||
|
|
e49dd85200 | ||
|
|
c9dec5ecd6 | ||
|
|
f3c5460e48 |
50
.github/workflows/mark-stable.yml
vendored
50
.github/workflows/mark-stable.yml
vendored
@@ -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}."
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,3 +1,57 @@
|
|||||||
|
## [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
|
||||||
|
|
||||||
|
|
||||||
|
## [0.10.0] - 2025-12-11
|
||||||
|
|
||||||
|
**Mirror System**
|
||||||
|
|
||||||
|
* Added SSH mirror support including multi-push and remote probing
|
||||||
|
* Introduced mirror management commands and refactored the CLI parser into modules
|
||||||
|
|
||||||
|
**CI/CD**
|
||||||
|
|
||||||
|
* Migrated to reusable workflows with improved debugging instrumentation
|
||||||
|
* Made stable-tag automation reliable for workflow_run events and permissions
|
||||||
|
* Ensured deterministic test results by rebuilding all test containers with no-cache
|
||||||
|
|
||||||
|
**E2E and Container Tests**
|
||||||
|
|
||||||
|
* Fixed Git safe.directory handling across all containers
|
||||||
|
* Restored Dockerfile ENTRYPOINT to resolve Nix TLS issues
|
||||||
|
* Fixed missing volume errors and hardened the E2E runner
|
||||||
|
* Added full Nix flake E2E test matrix across all distro containers
|
||||||
|
* Disabled Nix sandboxing for cross-distro builds where required
|
||||||
|
|
||||||
|
**Nix and Python Environment**
|
||||||
|
|
||||||
|
* Unified Nix Python environment and introduced lazy CLI imports
|
||||||
|
* Ensured PyYAML availability and improved Python 3.13 compatibility
|
||||||
|
* Refactored flake.nix to remove side effects and rely on generic python3
|
||||||
|
|
||||||
|
**Packaging**
|
||||||
|
|
||||||
|
* Removed Debian’s hard dependency on Nix
|
||||||
|
* Restructured packaging layout and refined build paths
|
||||||
|
* Excluded assets from Arch PKGBUILD rsync
|
||||||
|
* Cleaned up obsolete ignore files
|
||||||
|
|
||||||
|
**Repository Layout**
|
||||||
|
|
||||||
|
* Restructured repository to align local, Nix-based, and distro-based build workflows
|
||||||
|
* Added Arch support and refined build/purge scripts
|
||||||
|
|
||||||
|
|
||||||
## [0.9.1] - 2025-12-10
|
## [0.9.1] - 2025-12-10
|
||||||
|
|
||||||
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
* * Refactored installer: new `venv-create.sh`, cleaner root/user setup flow, updated README with architecture map.
|
||||||
|
|||||||
6
MIRRORS
6
MIRRORS
@@ -1,3 +1,3 @@
|
|||||||
https://github.com/kevinveenbirkenbach/package-manager
|
git@github.com:kevinveenbirkenbach/package-manager.git
|
||||||
https://git.veen.world/kevinveenbirkenbach/package-manager
|
ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git
|
||||||
https://code.infinito.nexus/kevinveenbirkenbach/package-manager
|
ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "0.9.1";
|
version = "0.10.2";
|
||||||
|
|
||||||
# Use the git repo as source
|
# Use the git repo as source
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "package-manager"
|
name = "package-manager"
|
||||||
version = "0.9.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"
|
||||||
|
|||||||
@@ -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,43 +27,116 @@ 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)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
ensure_nix_build_group() {
|
||||||
|
if ! getent group nixbld >/dev/null 2>&1; then
|
||||||
|
echo "[init-nix] Creating group 'nixbld'..."
|
||||||
|
groupadd -r nixbld
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
if ! id "nixbld$i" >/dev/null 2>&1; then
|
||||||
|
echo "[init-nix] Creating build user nixbld$i..."
|
||||||
|
useradd -r -g nixbld -G nixbld -s /usr/sbin/nologin "nixbld$i"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
rm -f "${installer}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
# Fast path: Nix already available
|
||||||
if command -v nix >/dev/null 2>&1; then
|
if command -v nix >/dev/null 2>&1; then
|
||||||
echo "[init-nix] Nix already available on PATH: $(command -v nix)"
|
echo "[init-nix] Nix already available on PATH: $(command -v nix)"
|
||||||
exit 0
|
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 found after adjusting PATH: $(command -v nix)"
|
echo "[init-nix] Nix found after adjusting PATH: $(command -v nix)"
|
||||||
exit 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[init-nix] Nix not found, starting installation logic..."
|
echo "[init-nix] Nix not found, starting installation logic..."
|
||||||
|
|
||||||
IN_CONTAINER=0
|
local IN_CONTAINER=0
|
||||||
if is_container; then
|
if is_container; then
|
||||||
IN_CONTAINER=1
|
IN_CONTAINER=1
|
||||||
echo "[init-nix] Detected container environment."
|
echo "[init-nix] Detected container environment."
|
||||||
@@ -70,156 +144,89 @@ else
|
|||||||
echo "[init-nix] No container detected."
|
echo "[init-nix] No container detected."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# Container + root: install Nix as dedicated "nix" user (single-user)
|
# Container + root: dedicated "nix" user, single-user install
|
||||||
# ---------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
if [[ "${IN_CONTAINER}" -eq 1 && "${EUID:-0}" -eq 0 ]]; then
|
||||||
echo "[init-nix] Running as root inside a container – using dedicated 'nix' user."
|
echo "[init-nix] Container + root – installing as 'nix' user (single-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
|
if ! id nix >/dev/null 2>&1; then
|
||||||
echo "[init-nix] Creating user 'nix'..."
|
echo "[init-nix] Creating user 'nix'..."
|
||||||
# Resolve a valid shell path across distros:
|
local BASH_SHELL
|
||||||
# - 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)"
|
BASH_SHELL="$(command -v bash || true)"
|
||||||
if [[ -z "${BASH_SHELL}" ]]; then
|
[[ -z "${BASH_SHELL}" ]] && BASH_SHELL="/bin/sh"
|
||||||
BASH_SHELL="/bin/sh"
|
|
||||||
fi
|
|
||||||
useradd -m -r -g nixbld -s "${BASH_SHELL}" nix
|
useradd -m -r -g nixbld -s "${BASH_SHELL}" nix
|
||||||
fi
|
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
|
if [[ ! -d /nix ]]; then
|
||||||
echo "[init-nix] Creating /nix with owner nix:nixbld..."
|
echo "[init-nix] Creating /nix with owner nix:nixbld..."
|
||||||
mkdir -m 0755 /nix
|
mkdir -m 0755 /nix
|
||||||
chown nix:nixbld /nix
|
chown nix:nixbld /nix
|
||||||
else
|
else
|
||||||
|
local current_owner current_group
|
||||||
current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')"
|
current_owner="$(stat -c '%U' /nix 2>/dev/null || echo '?')"
|
||||||
current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')"
|
current_group="$(stat -c '%G' /nix 2>/dev/null || echo '?')"
|
||||||
if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then
|
if [[ "${current_owner}" != "nix" || "${current_group}" != "nixbld" ]]; then
|
||||||
echo "[init-nix] /nix already exists with owner ${current_owner}:${current_group} – fixing to nix:nixbld..."
|
echo "[init-nix] Fixing /nix ownership from ${current_owner}:${current_group} to nix:nixbld..."
|
||||||
chown -R nix:nixbld /nix
|
chown -R nix:nixbld /nix
|
||||||
else
|
|
||||||
echo "[init-nix] /nix already exists with correct owner nix:nixbld."
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -w /nix ]]; then
|
if [[ ! -w /nix ]]; then
|
||||||
echo "[init-nix] WARNING: /nix is still not writable after chown; Nix installer may fail."
|
echo "[init-nix] WARNING: /nix is not writable after chown; Nix installer may fail."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run Nix single-user installer as "nix"
|
install_nix_with_retry "no-daemon" "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'
|
|
||||||
fi
|
|
||||||
|
|
||||||
# After installation, expose nix to root via PATH and symlink
|
|
||||||
ensure_nix_on_path
|
ensure_nix_on_path
|
||||||
|
|
||||||
if [[ -x /home/nix/.nix-profile/bin/nix ]]; then
|
if [[ -x /home/nix/.nix-profile/bin/nix && ! -e /usr/local/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"
|
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
|
ln -s /home/nix/.nix-profile/bin/nix /usr/local/bin/nix
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_nix_on_path
|
# -------------------------------------------------------------------------
|
||||||
|
# Host (no container)
|
||||||
if command -v nix >/dev/null 2>&1; then
|
# -------------------------------------------------------------------------
|
||||||
echo "[init-nix] Nix successfully installed (container mode) at: $(command -v nix)"
|
elif [[ "${IN_CONTAINER}" -eq 0 ]]; then
|
||||||
else
|
|
||||||
echo "[init-nix] WARNING: Nix installation finished in container, but 'nix' is still not on PATH."
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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)."
|
|
||||||
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
|
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
|
||||||
fi
|
install_nix_with_retry "no-daemon"
|
||||||
else
|
|
||||||
# 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
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# After installation: fix PATH (runtime + shell profiles)
|
# Container, but not root (rare)
|
||||||
# ---------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
|
else
|
||||||
|
echo "[init-nix] Container as non-root – using single-user install (--no-daemon)."
|
||||||
|
install_nix_with_retry "no-daemon"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
|
echo "[init-nix] Nix successfully installed at: $(command -v nix)"
|
||||||
fi
|
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 "$@"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -8,19 +8,18 @@ fi
|
|||||||
|
|
||||||
FLAKE_DIR="/usr/lib/package-manager"
|
FLAKE_DIR="/usr/lib/package-manager"
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Try to ensure that "nix" is on PATH
|
# Try to ensure that "nix" is on PATH (common locations + container user)
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if ! command -v nix >/dev/null 2>&1; then
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
# Common locations for Nix installations
|
|
||||||
CANDIDATES=(
|
CANDIDATES=(
|
||||||
"/nix/var/nix/profiles/default/bin/nix"
|
"/nix/var/nix/profiles/default/bin/nix"
|
||||||
"${HOME:-/root}/.nix-profile/bin/nix"
|
"${HOME:-/root}/.nix-profile/bin/nix"
|
||||||
|
"/home/nix/.nix-profile/bin/nix"
|
||||||
)
|
)
|
||||||
|
|
||||||
for candidate in "${CANDIDATES[@]}"; do
|
for candidate in "${CANDIDATES[@]}"; do
|
||||||
if [[ -x "$candidate" ]]; then
|
if [[ -x "$candidate" ]]; then
|
||||||
# Prepend the directory of the candidate to PATH
|
|
||||||
PATH="$(dirname "$candidate"):${PATH}"
|
PATH="$(dirname "$candidate"):${PATH}"
|
||||||
export PATH
|
export PATH
|
||||||
break
|
break
|
||||||
@@ -28,13 +27,22 @@ if ! command -v nix >/dev/null 2>&1; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Primary (and only) path: use Nix flake if available
|
# If nix is still missing, try to run init-nix.sh once
|
||||||
# ------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
if ! command -v nix >/dev/null 2>&1; then
|
||||||
|
if [[ -x "${FLAKE_DIR}/init-nix.sh" ]]; then
|
||||||
|
"${FLAKE_DIR}/init-nix.sh" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Primary path: use Nix flake if available
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
if command -v nix >/dev/null 2>&1; then
|
if command -v nix >/dev/null 2>&1; then
|
||||||
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
exec nix run "${FLAKE_DIR}#pkgmgr" -- "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH."
|
echo "[pkgmgr-wrapper] ERROR: 'nix' binary not found on PATH after init."
|
||||||
echo "[pkgmgr-wrapper] Nix is required to run pkgmgr (no Python fallback)."
|
echo "[pkgmgr-wrapper] Nix is required to run pkgmgr (no Python fallback)."
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -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}'."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Set
|
||||||
|
|
||||||
from pkgmgr.core.command.run import run_command
|
from pkgmgr.core.command.run import run_command
|
||||||
from pkgmgr.core.git import GitError, run_git
|
from pkgmgr.core.git import GitError, run_git
|
||||||
@@ -87,18 +87,41 @@ def has_origin_remote(repo_dir: str) -> bool:
|
|||||||
return "origin" in names
|
return "origin" in names
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_push_urls_for_origin(
|
||||||
|
repo_dir: str,
|
||||||
|
mirrors: MirrorMap,
|
||||||
|
preview: bool,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that all mirror URLs are present as push URLs on 'origin'.
|
||||||
|
"""
|
||||||
|
desired: Set[str] = {url for url in mirrors.values() if url}
|
||||||
|
if not desired:
|
||||||
|
return
|
||||||
|
|
||||||
|
existing_output = _safe_git_output(
|
||||||
|
["remote", "get-url", "--push", "--all", "origin"],
|
||||||
|
cwd=repo_dir,
|
||||||
|
)
|
||||||
|
existing = set(existing_output.splitlines()) if existing_output else set()
|
||||||
|
|
||||||
|
missing = sorted(desired - existing)
|
||||||
|
for url in missing:
|
||||||
|
cmd = f"git remote set-url --add --push origin {url}"
|
||||||
|
if preview:
|
||||||
|
print(f"[PREVIEW] Would run in {repo_dir!r}: {cmd}")
|
||||||
|
else:
|
||||||
|
print(f"[INFO] Adding push URL to 'origin': {url}")
|
||||||
|
run_command(cmd, cwd=repo_dir, preview=False)
|
||||||
|
|
||||||
|
|
||||||
def ensure_origin_remote(
|
def ensure_origin_remote(
|
||||||
repo: Repository,
|
repo: Repository,
|
||||||
ctx: RepoMirrorContext,
|
ctx: RepoMirrorContext,
|
||||||
preview: bool,
|
preview: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure that a usable 'origin' remote exists.
|
Ensure that a usable 'origin' remote exists and has all push URLs.
|
||||||
|
|
||||||
Priority for choosing URL:
|
|
||||||
1. resolved_mirrors["origin"]
|
|
||||||
2. any resolved mirror (first by name)
|
|
||||||
3. default SSH URL derived from provider/account/repository
|
|
||||||
"""
|
"""
|
||||||
repo_dir = ctx.repo_dir
|
repo_dir = ctx.repo_dir
|
||||||
resolved_mirrors = ctx.resolved_mirrors
|
resolved_mirrors = ctx.resolved_mirrors
|
||||||
@@ -109,6 +132,7 @@ def ensure_origin_remote(
|
|||||||
|
|
||||||
url = determine_primary_remote_url(repo, resolved_mirrors)
|
url = determine_primary_remote_url(repo, resolved_mirrors)
|
||||||
|
|
||||||
|
if not has_origin_remote(repo_dir):
|
||||||
if not url:
|
if not url:
|
||||||
print(
|
print(
|
||||||
"[WARN] Could not determine URL for 'origin' remote. "
|
"[WARN] Could not determine URL for 'origin' remote. "
|
||||||
@@ -116,26 +140,40 @@ def ensure_origin_remote(
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not has_origin_remote(repo_dir):
|
|
||||||
cmd = f"git remote add origin {url}"
|
cmd = f"git remote add origin {url}"
|
||||||
if preview:
|
if preview:
|
||||||
print(f"[PREVIEW] Would run in {repo_dir!r}: {cmd}")
|
print(f"[PREVIEW] Would run in {repo_dir!r}: {cmd}")
|
||||||
else:
|
else:
|
||||||
print(f"[INFO] Adding 'origin' remote in {repo_dir}: {url}")
|
print(f"[INFO] Adding 'origin' remote in {repo_dir}: {url}")
|
||||||
run_command(cmd, cwd=repo_dir, preview=False)
|
run_command(cmd, cwd=repo_dir, preview=False)
|
||||||
return
|
|
||||||
|
|
||||||
current = current_origin_url(repo_dir)
|
|
||||||
if current == url:
|
|
||||||
print(f"[INFO] 'origin' already points to {url} (no change needed).")
|
|
||||||
return
|
|
||||||
|
|
||||||
cmd = f"git remote set-url origin {url}"
|
|
||||||
if preview:
|
|
||||||
print(f"[PREVIEW] Would run in {repo_dir!r}: {cmd}")
|
|
||||||
else:
|
else:
|
||||||
|
current = current_origin_url(repo_dir)
|
||||||
|
if current == url or not url:
|
||||||
print(
|
print(
|
||||||
f"[INFO] Updating 'origin' remote in {repo_dir} "
|
f"[INFO] 'origin' already points to "
|
||||||
f"from {current or '<unknown>'} to {url}"
|
f"{current or '<unknown>'} (no change needed)."
|
||||||
)
|
)
|
||||||
run_command(cmd, cwd=repo_dir, preview=False)
|
else:
|
||||||
|
# We do not auto-change origin here, only log the mismatch.
|
||||||
|
print(
|
||||||
|
"[INFO] 'origin' exists with URL "
|
||||||
|
f"{current or '<unknown>'}; not changing to {url}."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure all mirrors are present as push URLs
|
||||||
|
_ensure_push_urls_for_origin(repo_dir, resolved_mirrors, preview)
|
||||||
|
|
||||||
|
|
||||||
|
def is_remote_reachable(url: str, cwd: Optional[str] = None) -> bool:
|
||||||
|
"""
|
||||||
|
Check whether a remote repository is reachable via `git ls-remote`.
|
||||||
|
|
||||||
|
This does NOT modify anything; it only probes the remote.
|
||||||
|
"""
|
||||||
|
workdir = cwd or os.getcwd()
|
||||||
|
try:
|
||||||
|
# --exit-code → non-zero exit code if the remote does not exist
|
||||||
|
run_git(["ls-remote", "--exit-code", url], cwd=workdir)
|
||||||
|
return True
|
||||||
|
except GitError:
|
||||||
|
return False
|
||||||
|
|||||||
@@ -1,47 +1,28 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
from typing import List, Mapping
|
from typing import List, Mapping
|
||||||
|
|
||||||
from .types import MirrorMap, Repository
|
from .types import MirrorMap, Repository
|
||||||
|
|
||||||
|
|
||||||
def load_config_mirrors(repo: Repository) -> MirrorMap:
|
def load_config_mirrors(repo: Repository) -> MirrorMap:
|
||||||
"""
|
|
||||||
Load mirrors from the repository configuration entry.
|
|
||||||
|
|
||||||
Supported shapes:
|
|
||||||
|
|
||||||
repo["mirrors"] = {
|
|
||||||
"origin": "ssh://git@example.com/...",
|
|
||||||
"backup": "ssh://git@backup/...",
|
|
||||||
}
|
|
||||||
|
|
||||||
or
|
|
||||||
|
|
||||||
repo["mirrors"] = [
|
|
||||||
{"name": "origin", "url": "ssh://git@example.com/..."},
|
|
||||||
{"name": "backup", "url": "ssh://git@backup/..."},
|
|
||||||
]
|
|
||||||
"""
|
|
||||||
mirrors = repo.get("mirrors") or {}
|
mirrors = repo.get("mirrors") or {}
|
||||||
result: MirrorMap = {}
|
result: MirrorMap = {}
|
||||||
|
|
||||||
if isinstance(mirrors, dict):
|
if isinstance(mirrors, dict):
|
||||||
for name, url in mirrors.items():
|
for name, url in mirrors.items():
|
||||||
if not url:
|
if url:
|
||||||
continue
|
|
||||||
result[str(name)] = str(url)
|
result[str(name)] = str(url)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if isinstance(mirrors, list):
|
if isinstance(mirrors, list):
|
||||||
for entry in mirrors:
|
for entry in mirrors:
|
||||||
if not isinstance(entry, dict):
|
if isinstance(entry, dict):
|
||||||
continue
|
|
||||||
name = entry.get("name")
|
name = entry.get("name")
|
||||||
url = entry.get("url")
|
url = entry.get("url")
|
||||||
if not name or not url:
|
if name and url:
|
||||||
continue
|
|
||||||
result[str(name)] = str(url)
|
result[str(name)] = str(url)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -49,13 +30,9 @@ def load_config_mirrors(repo: Repository) -> MirrorMap:
|
|||||||
|
|
||||||
def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
||||||
"""
|
"""
|
||||||
Read mirrors from the MIRRORS file in the repository directory.
|
Supports:
|
||||||
|
NAME URL
|
||||||
Simple text format:
|
URL → auto name = hostname
|
||||||
|
|
||||||
# comment
|
|
||||||
origin ssh://git@example.com/account/repo.git
|
|
||||||
backup ssh://git@backup/account/repo.git
|
|
||||||
"""
|
"""
|
||||||
path = os.path.join(repo_dir, filename)
|
path = os.path.join(repo_dir, filename)
|
||||||
mirrors: MirrorMap = {}
|
mirrors: MirrorMap = {}
|
||||||
@@ -71,10 +48,24 @@ def read_mirrors_file(repo_dir: str, filename: str = "MIRRORS") -> MirrorMap:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
parts = stripped.split(None, 1)
|
parts = stripped.split(None, 1)
|
||||||
if len(parts) != 2:
|
|
||||||
# Ignore malformed lines silently
|
# Case 1: "name url"
|
||||||
continue
|
if len(parts) == 2:
|
||||||
name, url = parts
|
name, url = parts
|
||||||
|
# Case 2: "url" → auto-generate name
|
||||||
|
elif len(parts) == 1:
|
||||||
|
url = parts[0]
|
||||||
|
parsed = urlparse(url)
|
||||||
|
host = (parsed.netloc or "").split(":")[0]
|
||||||
|
base = host or "mirror"
|
||||||
|
name = base
|
||||||
|
i = 2
|
||||||
|
while name in mirrors:
|
||||||
|
name = f"{base}{i}"
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
mirrors[name] = url
|
mirrors[name] = url
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
print(f"[WARN] Could not read MIRRORS file at {path}: {exc}")
|
print(f"[WARN] Could not read MIRRORS file at {path}: {exc}")
|
||||||
@@ -88,22 +79,14 @@ def write_mirrors_file(
|
|||||||
filename: str = "MIRRORS",
|
filename: str = "MIRRORS",
|
||||||
preview: bool = False,
|
preview: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
|
||||||
Write mirrors to MIRRORS file.
|
|
||||||
|
|
||||||
Existing file is overwritten. In preview mode we only print what would
|
|
||||||
be written.
|
|
||||||
"""
|
|
||||||
path = os.path.join(repo_dir, filename)
|
path = os.path.join(repo_dir, filename)
|
||||||
lines: List[str] = [f"{name} {url}" for name, url in sorted(mirrors.items())]
|
lines = [f"{name} {url}" for name, url in sorted(mirrors.items())]
|
||||||
content = "\n".join(lines) + ("\n" if lines else "")
|
content = "\n".join(lines) + ("\n" if lines else "")
|
||||||
|
|
||||||
if preview:
|
if preview:
|
||||||
print(f"[PREVIEW] Would write MIRRORS file at {path}:")
|
print(f"[PREVIEW] Would write MIRRORS file at {path}:")
|
||||||
if content:
|
print(content or "(empty)")
|
||||||
print(content.rstrip())
|
|
||||||
else:
|
|
||||||
print("(empty)")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import List
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from pkgmgr.core.git import run_git, GitError
|
||||||
|
|
||||||
from .context import build_context
|
from .context import build_context
|
||||||
from .git_remote import determine_primary_remote_url, ensure_origin_remote
|
from .git_remote import determine_primary_remote_url, ensure_origin_remote
|
||||||
@@ -13,6 +15,9 @@ def _setup_local_mirrors_for_repo(
|
|||||||
all_repos: List[Repository],
|
all_repos: List[Repository],
|
||||||
preview: bool,
|
preview: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""
|
||||||
|
Ensure local Git state is sane (currently: 'origin' remote).
|
||||||
|
"""
|
||||||
ctx = build_context(repo, repositories_base_dir, all_repos)
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
|
|
||||||
print("------------------------------------------------------------")
|
print("------------------------------------------------------------")
|
||||||
@@ -24,6 +29,27 @@ def _setup_local_mirrors_for_repo(
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def _probe_mirror(url: str, repo_dir: str) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Probe a remote mirror by running `git ls-remote <url>`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(True, "") on success,
|
||||||
|
(False, error_message) on failure.
|
||||||
|
|
||||||
|
Wichtig:
|
||||||
|
- Wir werten ausschließlich den Exit-Code aus.
|
||||||
|
- STDERR kann Hinweise/Warnings enthalten und ist NICHT automatisch ein Fehler.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Wir ignorieren stdout komplett; wichtig ist nur, dass der Befehl ohne
|
||||||
|
# GitError (also Exit-Code 0) durchläuft.
|
||||||
|
run_git(["ls-remote", url], cwd=repo_dir)
|
||||||
|
return True, ""
|
||||||
|
except GitError as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
|
||||||
|
|
||||||
def _setup_remote_mirrors_for_repo(
|
def _setup_remote_mirrors_for_repo(
|
||||||
repo: Repository,
|
repo: Repository,
|
||||||
repositories_base_dir: str,
|
repositories_base_dir: str,
|
||||||
@@ -31,45 +57,75 @@ def _setup_remote_mirrors_for_repo(
|
|||||||
preview: bool,
|
preview: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Placeholder for remote-side setup.
|
Remote-side setup / validation.
|
||||||
|
|
||||||
This is intentionally conservative:
|
Aktuell werden nur **nicht-destruktive Checks** gemacht:
|
||||||
- We *do not* call any provider APIs automatically here.
|
|
||||||
- Instead, we show what should exist and which URL should be created.
|
- Für jeden Mirror (aus config + MIRRORS-Datei, file gewinnt):
|
||||||
|
* `git ls-remote <url>` wird ausgeführt.
|
||||||
|
* Bei Exit-Code 0 → [OK]
|
||||||
|
* Bei Fehler → [WARN] + Details aus der GitError-Exception
|
||||||
|
|
||||||
|
Es werden **keine** Provider-APIs aufgerufen und keine Repos angelegt.
|
||||||
"""
|
"""
|
||||||
ctx = build_context(repo, repositories_base_dir, all_repos)
|
ctx = build_context(repo, repositories_base_dir, all_repos)
|
||||||
resolved_m = ctx.resolved_mirrors
|
resolved_m = ctx.resolved_mirrors
|
||||||
|
|
||||||
primary_url = determine_primary_remote_url(repo, resolved_m)
|
|
||||||
|
|
||||||
print("------------------------------------------------------------")
|
print("------------------------------------------------------------")
|
||||||
print(f"[MIRROR SETUP:REMOTE] {ctx.identifier}")
|
print(f"[MIRROR SETUP:REMOTE] {ctx.identifier}")
|
||||||
print(f"[MIRROR SETUP:REMOTE] dir: {ctx.repo_dir}")
|
print(f"[MIRROR SETUP:REMOTE] dir: {ctx.repo_dir}")
|
||||||
print("------------------------------------------------------------")
|
print("------------------------------------------------------------")
|
||||||
|
|
||||||
|
if not resolved_m:
|
||||||
|
# Optional: Fallback auf eine heuristisch bestimmte URL, falls wir
|
||||||
|
# irgendwann "automatisch anlegen" implementieren wollen.
|
||||||
|
primary_url = determine_primary_remote_url(repo, resolved_m)
|
||||||
if not primary_url:
|
if not primary_url:
|
||||||
print(
|
print(
|
||||||
"[WARN] Could not determine primary remote URL for this repository.\n"
|
"[INFO] No mirrors configured (config or MIRRORS file), and no "
|
||||||
" Please ensure provider/account/repository and/or mirrors "
|
"primary URL could be derived from provider/account/repository."
|
||||||
"are set in your config."
|
|
||||||
)
|
)
|
||||||
print()
|
print()
|
||||||
return
|
return
|
||||||
|
|
||||||
if preview:
|
ok, error_message = _probe_mirror(primary_url, ctx.repo_dir)
|
||||||
print(
|
if ok:
|
||||||
"[PREVIEW] Would ensure that a remote repository exists for:\n"
|
print(f"[OK] Remote mirror (primary) is reachable: {primary_url}")
|
||||||
f" {primary_url}\n"
|
|
||||||
" (Provider-specific API calls not implemented yet.)"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
print(
|
print("[WARN] Primary remote URL is NOT reachable:")
|
||||||
"[INFO] Remote-setup logic is not implemented yet.\n"
|
print(f" {primary_url}")
|
||||||
" Please create the remote repository manually if needed:\n"
|
if error_message:
|
||||||
f" {primary_url}\n"
|
print(" Details:")
|
||||||
)
|
for line in error_message.splitlines():
|
||||||
|
print(f" {line}")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
print(
|
||||||
|
"[INFO] Remote checks are non-destructive and only use `git ls-remote` "
|
||||||
|
"to probe mirror URLs."
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Normaler Fall: wir haben benannte Mirrors aus config/MIRRORS
|
||||||
|
for name, url in sorted(resolved_m.items()):
|
||||||
|
ok, error_message = _probe_mirror(url, ctx.repo_dir)
|
||||||
|
if ok:
|
||||||
|
print(f"[OK] Remote mirror '{name}' is reachable: {url}")
|
||||||
|
else:
|
||||||
|
print(f"[WARN] Remote mirror '{name}' is NOT reachable:")
|
||||||
|
print(f" {url}")
|
||||||
|
if error_message:
|
||||||
|
print(" Details:")
|
||||||
|
for line in error_message.splitlines():
|
||||||
|
print(f" {line}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(
|
||||||
|
"[INFO] Remote checks are non-destructive and only use `git ls-remote` "
|
||||||
|
"to probe mirror URLs."
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def setup_mirrors(
|
def setup_mirrors(
|
||||||
@@ -88,8 +144,8 @@ def setup_mirrors(
|
|||||||
points to a reasonable URL).
|
points to a reasonable URL).
|
||||||
|
|
||||||
remote:
|
remote:
|
||||||
- Placeholder that prints what should exist on the remote side.
|
- Non-destructive remote checks using `git ls-remote` for each mirror URL.
|
||||||
Actual API calls to providers are not implemented yet.
|
Es werden keine Repositories auf dem Provider angelegt.
|
||||||
"""
|
"""
|
||||||
for repo in selected_repos:
|
for repo in selected_repos:
|
||||||
if local:
|
if local:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
0
tests/unit/pkgmgr/actions/mirror/__init__.py
Normal file
0
tests/unit/pkgmgr/actions/mirror/__init__.py
Normal file
110
tests/unit/pkgmgr/actions/mirror/test_git_remote.py
Normal file
110
tests/unit/pkgmgr/actions/mirror/test_git_remote.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from pkgmgr.actions.mirror.git_remote import (
|
||||||
|
build_default_ssh_url,
|
||||||
|
determine_primary_remote_url,
|
||||||
|
)
|
||||||
|
from pkgmgr.actions.mirror.types import MirrorMap, Repository
|
||||||
|
|
||||||
|
|
||||||
|
class TestMirrorGitRemote(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for SSH URL and primary remote selection logic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_build_default_ssh_url_without_port(self) -> None:
|
||||||
|
repo: Repository = {
|
||||||
|
"provider": "github.com",
|
||||||
|
"account": "kevinveenbirkenbach",
|
||||||
|
"repository": "package-manager",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = build_default_ssh_url(repo)
|
||||||
|
self.assertEqual(
|
||||||
|
url,
|
||||||
|
"git@github.com:kevinveenbirkenbach/package-manager.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_build_default_ssh_url_with_port(self) -> None:
|
||||||
|
repo: Repository = {
|
||||||
|
"provider": "code.cymais.cloud",
|
||||||
|
"account": "kevinveenbirkenbach",
|
||||||
|
"repository": "pkgmgr",
|
||||||
|
"port": 2201,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = build_default_ssh_url(repo)
|
||||||
|
self.assertEqual(
|
||||||
|
url,
|
||||||
|
"ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_build_default_ssh_url_missing_fields_returns_none(self) -> None:
|
||||||
|
repo: Repository = {
|
||||||
|
"provider": "github.com",
|
||||||
|
"account": "kevinveenbirkenbach",
|
||||||
|
# "repository" fehlt absichtlich
|
||||||
|
}
|
||||||
|
|
||||||
|
url = build_default_ssh_url(repo)
|
||||||
|
self.assertIsNone(url)
|
||||||
|
|
||||||
|
def test_determine_primary_remote_url_prefers_origin_in_resolved_mirrors(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
repo: Repository = {
|
||||||
|
"provider": "github.com",
|
||||||
|
"account": "kevinveenbirkenbach",
|
||||||
|
"repository": "package-manager",
|
||||||
|
}
|
||||||
|
mirrors: MirrorMap = {
|
||||||
|
"origin": "git@github.com:kevinveenbirkenbach/package-manager.git",
|
||||||
|
"backup": "ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = determine_primary_remote_url(repo, mirrors)
|
||||||
|
self.assertEqual(
|
||||||
|
url,
|
||||||
|
"git@github.com:kevinveenbirkenbach/package-manager.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_determine_primary_remote_url_uses_any_mirror_if_no_origin(self) -> None:
|
||||||
|
repo: Repository = {
|
||||||
|
"provider": "github.com",
|
||||||
|
"account": "kevinveenbirkenbach",
|
||||||
|
"repository": "package-manager",
|
||||||
|
}
|
||||||
|
mirrors: MirrorMap = {
|
||||||
|
"backup": "ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
"mirror2": "ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
}
|
||||||
|
|
||||||
|
url = determine_primary_remote_url(repo, mirrors)
|
||||||
|
# Alphabetisch sortiert: backup, mirror2 → backup gewinnt
|
||||||
|
self.assertEqual(
|
||||||
|
url,
|
||||||
|
"ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_determine_primary_remote_url_falls_back_to_default_ssh(self) -> None:
|
||||||
|
repo: Repository = {
|
||||||
|
"provider": "github.com",
|
||||||
|
"account": "kevinveenbirkenbach",
|
||||||
|
"repository": "package-manager",
|
||||||
|
}
|
||||||
|
mirrors: MirrorMap = {}
|
||||||
|
|
||||||
|
url = determine_primary_remote_url(repo, mirrors)
|
||||||
|
self.assertEqual(
|
||||||
|
url,
|
||||||
|
"git@github.com:kevinveenbirkenbach/package-manager.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
135
tests/unit/pkgmgr/actions/mirror/test_io.py
Normal file
135
tests/unit/pkgmgr/actions/mirror/test_io.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from pkgmgr.actions.mirror.io import (
|
||||||
|
load_config_mirrors,
|
||||||
|
read_mirrors_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMirrorIO(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for pkgmgr.actions.mirror.io helpers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# load_config_mirrors
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_load_config_mirrors_from_dict(self) -> None:
|
||||||
|
repo = {
|
||||||
|
"mirrors": {
|
||||||
|
"origin": "ssh://git@example.com/account/repo.git",
|
||||||
|
"backup": "ssh://git@backup/account/repo.git",
|
||||||
|
"empty": "",
|
||||||
|
"none": None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mirrors = load_config_mirrors(repo)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
mirrors,
|
||||||
|
{
|
||||||
|
"origin": "ssh://git@example.com/account/repo.git",
|
||||||
|
"backup": "ssh://git@backup/account/repo.git",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_load_config_mirrors_from_list(self) -> None:
|
||||||
|
repo = {
|
||||||
|
"mirrors": [
|
||||||
|
{"name": "origin", "url": "ssh://git@example.com/account/repo.git"},
|
||||||
|
{"name": "backup", "url": "ssh://git@backup/account/repo.git"},
|
||||||
|
{"name": "", "url": "ssh://git@invalid/ignored.git"},
|
||||||
|
{"name": "missing-url"},
|
||||||
|
"not-a-dict",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
mirrors = load_config_mirrors(repo)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
mirrors,
|
||||||
|
{
|
||||||
|
"origin": "ssh://git@example.com/account/repo.git",
|
||||||
|
"backup": "ssh://git@backup/account/repo.git",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_load_config_mirrors_empty_when_missing(self) -> None:
|
||||||
|
repo = {}
|
||||||
|
mirrors = load_config_mirrors(repo)
|
||||||
|
self.assertEqual(mirrors, {})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# read_mirrors_file
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def test_read_mirrors_file_with_named_and_url_only_entries(self) -> None:
|
||||||
|
"""
|
||||||
|
Ensure that the MIRRORS file format is parsed correctly:
|
||||||
|
|
||||||
|
- 'name url' → exact name
|
||||||
|
- 'url' → auto name derived from netloc (host[:port]),
|
||||||
|
with numeric suffix if duplicated.
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
mirrors_path = os.path.join(tmpdir, "MIRRORS")
|
||||||
|
content = "\n".join(
|
||||||
|
[
|
||||||
|
"# comment",
|
||||||
|
"",
|
||||||
|
"origin ssh://git@example.com/account/repo.git",
|
||||||
|
"https://github.com/kevinveenbirkenbach/package-manager",
|
||||||
|
"https://github.com/kevinveenbirkenbach/another-repo",
|
||||||
|
"ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(mirrors_path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(content + "\n")
|
||||||
|
|
||||||
|
mirrors = read_mirrors_file(tmpdir)
|
||||||
|
|
||||||
|
# 'origin' is preserved as given
|
||||||
|
self.assertIn("origin", mirrors)
|
||||||
|
self.assertEqual(
|
||||||
|
mirrors["origin"],
|
||||||
|
"ssh://git@example.com/account/repo.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Two GitHub URLs → auto names: github.com, github.com2
|
||||||
|
github_urls = {
|
||||||
|
mirrors.get("github.com"),
|
||||||
|
mirrors.get("github.com2"),
|
||||||
|
}
|
||||||
|
self.assertIn(
|
||||||
|
"https://github.com/kevinveenbirkenbach/package-manager",
|
||||||
|
github_urls,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"https://github.com/kevinveenbirkenbach/another-repo",
|
||||||
|
github_urls,
|
||||||
|
)
|
||||||
|
|
||||||
|
# SSH-URL mit User-Teil → netloc ist "git@git.veen.world:2201"
|
||||||
|
# → host = "git@git.veen.world"
|
||||||
|
self.assertIn("git@git.veen.world", mirrors)
|
||||||
|
self.assertEqual(
|
||||||
|
mirrors["git@git.veen.world"],
|
||||||
|
"ssh://git@git.veen.world:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_read_mirrors_file_missing_returns_empty(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
mirrors = read_mirrors_file(tmpdir) # no MIRRORS file
|
||||||
|
self.assertEqual(mirrors, {})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
59
tests/unit/pkgmgr/actions/mirror/test_setup_cmd.py
Normal file
59
tests/unit/pkgmgr/actions/mirror/test_setup_cmd.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pkgmgr.actions.mirror.setup_cmd import _probe_mirror
|
||||||
|
from pkgmgr.core.git import GitError
|
||||||
|
|
||||||
|
|
||||||
|
class TestMirrorSetupCmd(unittest.TestCase):
|
||||||
|
"""
|
||||||
|
Unit tests for the non-destructive remote probing logic in setup_cmd.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@patch("pkgmgr.actions.mirror.setup_cmd.run_git")
|
||||||
|
def test_probe_mirror_success_returns_true_and_empty_message(
|
||||||
|
self,
|
||||||
|
mock_run_git,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
If run_git returns successfully, _probe_mirror must report (True, "").
|
||||||
|
"""
|
||||||
|
mock_run_git.return_value = "dummy-output"
|
||||||
|
|
||||||
|
ok, message = _probe_mirror(
|
||||||
|
"ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
"/tmp/some-repo",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(ok)
|
||||||
|
self.assertEqual(message, "")
|
||||||
|
mock_run_git.assert_called_once()
|
||||||
|
|
||||||
|
@patch("pkgmgr.actions.mirror.setup_cmd.run_git")
|
||||||
|
def test_probe_mirror_failure_returns_false_and_error_message(
|
||||||
|
self,
|
||||||
|
mock_run_git,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
If run_git raises GitError, _probe_mirror must report (False, <message>),
|
||||||
|
and not re-raise the exception.
|
||||||
|
"""
|
||||||
|
mock_run_git.side_effect = GitError("Git command failed (simulated)")
|
||||||
|
|
||||||
|
ok, message = _probe_mirror(
|
||||||
|
"ssh://git@code.cymais.cloud:2201/kevinveenbirkenbach/pkgmgr.git",
|
||||||
|
"/tmp/some-repo",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(ok)
|
||||||
|
self.assertIn("Git command failed", message)
|
||||||
|
mock_run_git.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user