Compare commits
16 Commits
39b16b87a8
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72fc69c2f8 | ||
|
|
6d8c6deae8 | ||
|
|
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}."
|
||||||
|
|||||||
93
CHANGELOG.md
93
CHANGELOG.md
@@ -1,3 +1,96 @@
|
|||||||
|
## [1.0.0] - 2025-12-11
|
||||||
|
|
||||||
|
* **1.0.0 – Official Stable Release 🎉**
|
||||||
|
*First stable release of PKGMGR, the multi-distro development and package workflow manager.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Key Features**
|
||||||
|
|
||||||
|
**Core Functionality**
|
||||||
|
|
||||||
|
* Manage many repositories with one CLI: `clone`, `update`, `install`, `list`, `path`, `config`
|
||||||
|
* Proxy wrappers for Git, Docker/Compose and Make
|
||||||
|
* Multi-repo execution with safe *preview mode*
|
||||||
|
* Mirror management: `mirror list/diff/merge/setup`
|
||||||
|
|
||||||
|
**Releases & Versioning**
|
||||||
|
|
||||||
|
* Automated SemVer bumps, tagging and changelog generation
|
||||||
|
* Supports PKGBUILD, Debian, RPM, pyproject.toml, flake.nix
|
||||||
|
|
||||||
|
**Developer Tools**
|
||||||
|
|
||||||
|
* Open repositories in VS Code, file manager or terminal
|
||||||
|
* Unified workflows across all major Linux distros
|
||||||
|
|
||||||
|
**Nix Integration**
|
||||||
|
|
||||||
|
* Cross-distro reproducible builds via Nix flakes
|
||||||
|
* CI-tested across all supported environments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Summary**
|
||||||
|
PKGMGR 1.0.0 unifies repository management, build tooling, release automation and reproducible multi-distro workflows into one cohesive CLI tool.
|
||||||
|
|
||||||
|
*This is the first official stable release.*
|
||||||
|
|
||||||
|
|
||||||
|
## [0.10.2] - 2025-12-11
|
||||||
|
|
||||||
|
* * Stable tag now updates only when a new highest version is released.
|
||||||
|
* Debian package now includes sudo to ensure privilege escalation works reliably.
|
||||||
|
* Nix setup is significantly more resilient with retries, correct permissions, and better environment handling.
|
||||||
|
* AUR builder setup uses retries so yay installs succeed even under network instability.
|
||||||
|
* Nix flake installation now fails only on mandatory parts; optional outputs no longer block installation.
|
||||||
|
|
||||||
|
|
||||||
|
## [0.10.1] - 2025-12-11
|
||||||
|
|
||||||
|
* Fixed Debian\Ubuntu to pass container e2e tests
|
||||||
|
|
||||||
|
|
||||||
|
## [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
|
||||||
164
README.md
164
README.md
@@ -1,68 +1,184 @@
|
|||||||
# Package Manager 🤖📦
|
# Package Manager 🤖📦
|
||||||
|
|
||||||
[](https://github.com/sponsors/kevinveenbirkenbach)
|
[](https://github.com/sponsors/kevinveenbirkenbach)
|
||||||
[](https://www.patreon.com/c/kevinveenbirkenbach)
|
[](https://www.patreon.com/c/kevinveenbirkenbach)
|
||||||
[](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
[](https://buymeacoffee.com/kevinveenbirkenbach)
|
||||||
|
[](https://s.veen.world/paypaldonate)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/kevinveenbirkenbach/package-manager)
|
[](https://github.com/kevinveenbirkenbach/package-manager)
|
||||||
|
|
||||||
*Kevins's* Package Manager is a configurable Python tool designed to manage multiple repositories via Bash. It automates common Git operations such as clone, pull, push, status, and more. Additionally, it handles the creation of executable wrappers and alias links for your repositories.
|
**Kevin's Package Manager (PKGMGR)** is a *multi-distro* package manager and workflow orchestrator.
|
||||||
|
It helps you **develop, package, release and manage projects across multiple Linux-based
|
||||||
|
operating systems** (Arch, Debian, Ubuntu, Fedora, CentOS, …).
|
||||||
|
|
||||||
|
PKGMGR is implemented in **Python** and uses **Nix (flakes)** as a foundation for
|
||||||
|
distribution-independent builds and tooling. On top of that it provides a rich
|
||||||
|
CLI that proxies common developer tools (Git, Docker, Make, …) and glues them
|
||||||
|
together into repeatable development workflows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why PKGMGR? 🧠
|
||||||
|
|
||||||
|
Traditional distro package managers like `apt`, `pacman` or `dnf` focus on a
|
||||||
|
single operating system. PKGMGR instead focuses on **your repositories and
|
||||||
|
development lifecycle**:
|
||||||
|
|
||||||
|
* one configuration for all your repos,
|
||||||
|
* one CLI to interact with them,
|
||||||
|
* one Nix-based layer to keep tooling reproducible across distros.
|
||||||
|
|
||||||
|
You keep using your native package manager where it makes sense – PKGMGR
|
||||||
|
coordinates the *development and release flow* around it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Features 🚀
|
## Features 🚀
|
||||||
|
|
||||||
- **Installation & Setup:**
|
### Multi-distro development & packaging
|
||||||
Create executable wrappers with auto-detected commands (e.g. `main.sh` or `main.py`).
|
|
||||||
|
|
||||||
- **Git Operations:**
|
* Manage **many repositories at once** from a single `config/config.yaml`.
|
||||||
Easily perform `git pull`, `push`, `status`, `commit`, `diff`, `add`, `show`, and `checkout` with extra parameters passed through.
|
* Drive full **release pipelines** across Linux distributions using:
|
||||||
|
|
||||||
- **Configuration Management:**
|
* Nix flakes (`flake.nix`)
|
||||||
Manage repository configurations via a default file (`config/defaults.yaml`) and a user-specific file (`config/config.yaml`). Initialize, add, delete, or ignore entries using subcommands.
|
* PyPI style builds (`pyproject.toml`)
|
||||||
|
* OS packages (PKGBUILD, Debian control/changelog, RPM spec)
|
||||||
|
* Ansible Galaxy metadata and more.
|
||||||
|
|
||||||
- **Path & Listing:**
|
### Rich CLI for daily work
|
||||||
Display repository paths or list all configured packages with their details.
|
|
||||||
|
|
||||||
- **Custom Aliases:**
|
All commands are exposed via the `pkgmgr` CLI and are available on every distro:
|
||||||
Generate and manage custom aliases for easy command invocation.
|
|
||||||
|
* **Repository management**
|
||||||
|
|
||||||
|
* `clone`, `update`, `install`, `delete`, `deinstall`, `path`, `list`, `config`
|
||||||
|
* **Git proxies**
|
||||||
|
|
||||||
|
* `pull`, `push`, `status`, `diff`, `add`, `show`, `checkout`,
|
||||||
|
`reset`, `revert`, `rebase`, `commit`, `branch`
|
||||||
|
* **Docker & Compose orchestration**
|
||||||
|
|
||||||
|
* `build`, `up`, `down`, `exec`, `ps`, `start`, `stop`, `restart`
|
||||||
|
* **Release toolchain**
|
||||||
|
|
||||||
|
* `version`, `release`, `changelog`, `make`
|
||||||
|
* **Mirror & workflow helpers**
|
||||||
|
|
||||||
|
* `mirror` (list/diff/merge/setup), `shell`, `terminal`, `code`, `explore`
|
||||||
|
|
||||||
|
Many of these commands support `--preview` mode so you can inspect the
|
||||||
|
underlying Git or Docker calls without executing them.
|
||||||
|
|
||||||
|
### Full development workflows
|
||||||
|
|
||||||
|
PKGMGR is not just a helper around Git commands. Combined with its release and
|
||||||
|
versioning features it can drive **end-to-end workflows**:
|
||||||
|
|
||||||
|
1. Clone and mirror repositories.
|
||||||
|
2. Run tests and builds through `make` or Nix.
|
||||||
|
3. Bump versions, update changelogs and tags.
|
||||||
|
4. Build distro-specific packages.
|
||||||
|
5. Keep all mirrors and working copies in sync.
|
||||||
|
|
||||||
|
The extensive E2E tests (`tests/e2e/`) and GitHub Actions workflows (including
|
||||||
|
“virgin user” and “virgin root” Arch tests) validate these flows across
|
||||||
|
different Linux environments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Architecture & Setup Map 🗺️
|
## Architecture & Setup Map 🗺️
|
||||||
|
|
||||||
The following diagram provides a full overview of PKGMGR’s package structure,
|
The following diagram gives a full overview of:
|
||||||
installation layers, and setup controller flow:
|
|
||||||
|
* PKGMGR’s package structure,
|
||||||
|
* the layered installers (OS, foundation, Python, Makefile),
|
||||||
|
* and the setup controller that decides which layer to use on a given system.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
**Diagram status:** *Stand: 11. Dezember 2025*
|
**Diagram status:** 11 December 2025
|
||||||
**Always-up-to-date version:** https://s.veen.world/pkgmgrmp
|
**Always-up-to-date version:** [https://s.veen.world/pkgmgrmp](https://s.veen.world/pkgmgrmp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Installation ⚙️
|
## Installation ⚙️
|
||||||
|
|
||||||
Clone the repository and ensure your `~/.local/bin` is in your system PATH:
|
### 1. Get the latest stable version
|
||||||
|
|
||||||
|
For a stable setup, use the **latest tagged release** (the tag pointed to by
|
||||||
|
`latest`):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
git clone https://github.com/kevinveenbirkenbach/package-manager.git
|
||||||
cd package-manager
|
cd package-manager
|
||||||
|
|
||||||
|
# Optional but recommended: checkout the latest stable tag
|
||||||
|
git fetch --tags
|
||||||
|
git checkout "$(git describe --tags --abbrev=0)"
|
||||||
```
|
```
|
||||||
|
|
||||||
Install make and pip if not installed yet:
|
### 2. Install via Make
|
||||||
|
|
||||||
|
The project ships with a Makefile that encapsulates the typical installation
|
||||||
|
flow. On most systems you only need:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pacman -S make python-pip
|
# Ensure make, Python and pip are installed via your distro package manager
|
||||||
|
# (e.g. pacman -S make python python-pip, apt install make python3-pip, ...)
|
||||||
|
|
||||||
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run the following command to set up the project:
|
This will:
|
||||||
|
|
||||||
|
* create or reuse a Python virtual environment,
|
||||||
|
* install PKGMGR (and its Python dependencies) into that environment,
|
||||||
|
* expose the `pkgmgr` executable on your PATH (usually via `~/.local/bin`),
|
||||||
|
* prepare Nix-based integration where available so PKGMGR can build and manage
|
||||||
|
packages distribution-independently.
|
||||||
|
|
||||||
|
For development use, you can also run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make setup
|
make setup
|
||||||
```
|
```
|
||||||
|
|
||||||
The `make setup` command will:
|
which prepares the environment and leaves you with a fully wired development
|
||||||
- Make `main.py` executable.
|
workspace (including Nix, tests and scripts).
|
||||||
- Install required packages from `requirements.txt`.
|
|
||||||
- Execute `python main.py install` to complete the installation.
|
---
|
||||||
|
|
||||||
|
## Usage 🧰
|
||||||
|
|
||||||
|
After installation, the main entry point is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkgmgr --help
|
||||||
|
```
|
||||||
|
|
||||||
|
This prints a list of all available subcommands, for example:
|
||||||
|
|
||||||
|
* `pkgmgr list --all` – show all repositories in the config
|
||||||
|
* `pkgmgr update --all --clone-mode https` – update every repository
|
||||||
|
* `pkgmgr release patch --preview` – simulate a patch release
|
||||||
|
* `pkgmgr version --all` – show version information for all repositories
|
||||||
|
* `pkgmgr mirror setup --preview --all` – prepare Git mirrors (no changes in preview)
|
||||||
|
* `pkgmgr make install --preview pkgmgr` – preview make install for the pkgmgr repo
|
||||||
|
|
||||||
|
The help for each command is available via:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pkgmgr <command> --help
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License 📄
|
## License 📄
|
||||||
|
|
||||||
This project is licensed under the MIT License.
|
This project is licensed under the MIT License.
|
||||||
|
See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Author 👤
|
## Author 👤
|
||||||
|
|
||||||
|
|||||||
BIN
assets/map.png
BIN
assets/map.png
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
@@ -36,7 +36,7 @@
|
|||||||
rec {
|
rec {
|
||||||
pkgmgr = pyPkgs.buildPythonApplication {
|
pkgmgr = pyPkgs.buildPythonApplication {
|
||||||
pname = "package-manager";
|
pname = "package-manager";
|
||||||
version = "0.9.1";
|
version = "1.0.0";
|
||||||
|
|
||||||
# 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 = "1.0.0"
|
||||||
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:
|
||||||
|
|||||||
@@ -18,52 +18,17 @@ USER_CONFIG_PATH = os.path.expanduser("~/.config/pkgmgr/config.yaml")
|
|||||||
|
|
||||||
DESCRIPTION_TEXT = """\
|
DESCRIPTION_TEXT = """\
|
||||||
\033[1;32mPackage Manager 🤖📦\033[0m
|
\033[1;32mPackage Manager 🤖📦\033[0m
|
||||||
\033[3mKevin's Package Manager is a multi-repository, multi-package, and multi-format
|
\033[3mKevin's multi-distro package and workflow manager.\033[0m
|
||||||
development tool crafted by and designed for:\033[0m
|
\033[1;34mKevin Veen-Birkenbach\033[0m – \033[4mhttps://www.veen.world/\033[0m
|
||||||
\033[1;34mKevin Veen-Birkenbach\033[0m
|
|
||||||
\033[4mhttps://www.veen.world/\033[0m
|
|
||||||
|
|
||||||
\033[1mOverview:\033[0m
|
Built in \033[1;33mPython\033[0m on top of \033[1;33mNix flakes\033[0m to manage many
|
||||||
A powerful toolchain that unifies and automates workflows across heterogeneous
|
repositories and packaging formats (pyproject.toml, flake.nix,
|
||||||
project ecosystems. pkgmgr is not only a package manager — it is a full
|
PKGBUILD, debian, Ansible, …) with one CLI.
|
||||||
developer-oriented orchestration tool.
|
|
||||||
|
|
||||||
It automatically detects, merges, and processes metadata from multiple
|
For details on any command, run:
|
||||||
dependency formats, including:
|
|
||||||
• \033[1;33mPython:\033[0m pyproject.toml, requirements.txt
|
|
||||||
• \033[1;33mNix:\033[0m flake.nix
|
|
||||||
• \033[1;33mArch Linux:\033[0m PKGBUILD
|
|
||||||
• \033[1;33mAnsible:\033[0m requirements.yml
|
|
||||||
|
|
||||||
This allows pkgmgr to perform installation, updates, verification, dependency
|
|
||||||
resolution, and synchronization across complex multi-repo environments — with a
|
|
||||||
single unified command-line interface.
|
|
||||||
|
|
||||||
\033[1mDeveloper Tools:\033[0m
|
|
||||||
pkgmgr includes an integrated toolbox to enhance daily development workflows:
|
|
||||||
|
|
||||||
• \033[1;33mVS Code integration:\033[0m Auto-generate and open multi-repo workspaces
|
|
||||||
• \033[1;33mTerminal integration:\033[0m Open repositories in new GNOME Terminal tabs
|
|
||||||
• \033[1;33mExplorer integration:\033[0m Open repositories in your file manager
|
|
||||||
• \033[1;33mRelease automation:\033[0m Version bumping, changelog updates, and tagging
|
|
||||||
• \033[1;33mBatch operations:\033[0m Execute shell commands across multiple repositories
|
|
||||||
• \033[1;33mGit/Docker/Make wrappers:\033[0m Unified command proxying for many tools
|
|
||||||
|
|
||||||
\033[1mCapabilities:\033[0m
|
|
||||||
• Clone, pull, verify, update, and manage many repositories at once
|
|
||||||
• Resolve dependencies across languages and ecosystems
|
|
||||||
• Standardize install/update workflows
|
|
||||||
• Create symbolic executable wrappers for any project
|
|
||||||
• Merge configuration from default + user config layers
|
|
||||||
|
|
||||||
Use pkgmgr as both a robust package management framework and a versatile
|
|
||||||
development orchestration tool.
|
|
||||||
|
|
||||||
For detailed help on each command, use:
|
|
||||||
\033[1mpkgmgr <command> --help\033[0m
|
\033[1mpkgmgr <command> --help\033[0m
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
"""
|
"""
|
||||||
Entry point for the pkgmgr CLI.
|
Entry point for the pkgmgr CLI.
|
||||||
|
|||||||
@@ -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