Refactor pkgmgr into modular installer pipeline with Nix flake support, PKGBUILD build workflow, local Nix cache, and full test suite restructuring.
See conversation: https://chatgpt.com/share/69332519-7ff4-800f-bc21-7fcd24a66c10
This commit is contained in:
26
.dockerignore
Normal file
26
.dockerignore
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Docker build context cleanup
|
||||||
|
|
||||||
|
# Nix local store/cache
|
||||||
|
.nix/
|
||||||
|
|
||||||
|
# Git internals
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# venvs
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.venvs/
|
||||||
|
|
||||||
|
# Editor/OS noise
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,3 +25,6 @@ build/
|
|||||||
# OS noise
|
# OS noise
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Nix Cache to speed up tests
|
||||||
|
.nix/
|
||||||
43
Dockerfile
43
Dockerfile
@@ -1,40 +1,31 @@
|
|||||||
FROM archlinux:latest
|
FROM archlinux:latest
|
||||||
|
|
||||||
# Update system and install core tooling
|
# 1) System basis + Nix
|
||||||
RUN pacman -Syu --noconfirm \
|
RUN pacman -Syu --noconfirm \
|
||||||
&& pacman -S --noconfirm --needed \
|
&& pacman -S --noconfirm --needed \
|
||||||
|
base-devel \
|
||||||
git \
|
git \
|
||||||
make \
|
nix \
|
||||||
sudo \
|
|
||||||
python \
|
|
||||||
python-pip \
|
|
||||||
python-virtualenv \
|
|
||||||
python-setuptools \
|
|
||||||
python-wheel \
|
|
||||||
&& pacman -Scc --noconfirm
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
# Ensure local bin is in PATH (for pkgmgr links)
|
ENV NIX_CONFIG="experimental-features = nix-command flakes"
|
||||||
ENV PATH="/root/.local/bin:$PATH"
|
|
||||||
|
|
||||||
# Create virtual environment
|
# 2) Unprivileged user for building Arch packages
|
||||||
ENV VIRTUAL_ENV=/root/.venvs/pkgmgr
|
RUN useradd -m builder
|
||||||
RUN python -m venv $VIRTUAL_ENV
|
WORKDIR /build
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
# Working directory for the package-manager project
|
# 3) Only PKGBUILD rein, um dein Wrapper-Paket zu bauen
|
||||||
WORKDIR /root/Repositories/github.com/kevinveenbirkenbach/package-manager
|
COPY PKGBUILD .
|
||||||
|
|
||||||
# Copy local package-manager source into container
|
RUN chown -R builder:builder /build \
|
||||||
COPY . .
|
&& su builder -c "makepkg -s --noconfirm --clean" \
|
||||||
|
&& pacman -U --noconfirm package-manager-*.pkg.tar.* \
|
||||||
# Install Python dependencies and register pkgmgr inside the venv
|
&& rm -rf /build
|
||||||
RUN pip install --upgrade pip \
|
|
||||||
&& pip install PyYAML \
|
# 4) Projekt-Quellen für Tests in den Container kopieren
|
||||||
&& chmod +x main.py \
|
WORKDIR /src
|
||||||
&& python main.py install package-manager --quiet --clone-mode shallow --no-verification
|
|
||||||
|
|
||||||
# Copy again to allow rebuild-based code changes
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# pkgmgr (Arch-Package) ist installiert und ruft nix run auf.
|
||||||
ENTRYPOINT ["pkgmgr"]
|
ENTRYPOINT ["pkgmgr"]
|
||||||
CMD ["--help"]
|
CMD ["--help"]
|
||||||
|
|||||||
64
Makefile
64
Makefile
@@ -1,31 +1,53 @@
|
|||||||
.PHONY: install setup uninstall aur_builder_setup
|
.PHONY: install setup uninstall aur_builder_setup test
|
||||||
|
|
||||||
|
# Local Nix cache directories in the repo
|
||||||
|
NIX_STORE_DIR := .nix/store
|
||||||
|
NIX_CACHE_DIR := .nix/cache
|
||||||
|
|
||||||
setup: install
|
setup: install
|
||||||
@python3 main.py install
|
@echo "Running pkgmgr setup via main.py..."
|
||||||
|
@if [ -x "$$HOME/.venvs/pkgmgr/bin/python" ]; then \
|
||||||
|
echo "Using virtualenv Python at $$HOME/.venvs/pkgmgr/bin/python"; \
|
||||||
|
"$$HOME/.venvs/pkgmgr/bin/python" main.py install; \
|
||||||
|
else \
|
||||||
|
echo "Virtualenv not found, falling back to system python3"; \
|
||||||
|
python3 main.py install; \
|
||||||
|
fi
|
||||||
|
|
||||||
test:
|
test:
|
||||||
|
@echo "Ensuring local Nix cache directories exist..."
|
||||||
|
@mkdir -p "$(NIX_STORE_DIR)" "$(NIX_CACHE_DIR)"
|
||||||
|
@echo "Building test image 'package-manager-test'..."
|
||||||
docker build -t package-manager-test .
|
docker build -t package-manager-test .
|
||||||
docker run --rm --entrypoint python package-manager-test -m unittest discover -s tests -p "test_*.py"
|
@echo "Running tests inside Nix devShell with local cache..."
|
||||||
|
docker run --rm \
|
||||||
|
-v "$$(pwd)/$(NIX_STORE_DIR):/nix" \
|
||||||
|
-v "$$(pwd)/$(NIX_CACHE_DIR):/root/.cache/nix" \
|
||||||
|
--workdir /src \
|
||||||
|
--entrypoint nix \
|
||||||
|
package-manager-test \
|
||||||
|
develop .#default --no-write-lock-file -c \
|
||||||
|
python -m unittest discover -s tests -p "test_*.py"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
@echo "Making 'main.py' executable..."
|
@echo "Making 'main.py' executable..."
|
||||||
@chmod +x main.py
|
@chmod +x main.py
|
||||||
@echo "Checking if global user virtual environment exists..."
|
@echo "Checking if global user virtual environment exists..."
|
||||||
@mkdir -p ~/.venvs
|
@mkdir -p "$$HOME/.venvs"
|
||||||
@if [ ! -d ~/.venvs/pkgmgr ]; then \
|
@if [ ! -d "$$HOME/.venvs/pkgmgr" ]; then \
|
||||||
echo "Creating global venv at ~/.venvs/pkgmgr..."; \
|
echo "Creating global venv at $$HOME/.venvs/pkgmgr..."; \
|
||||||
python3 -m venv ~/.venvs/pkgmgr; \
|
python3 -m venv "$$HOME/.venvs/pkgmgr"; \
|
||||||
fi
|
fi
|
||||||
@echo "Installing required Python packages into ~/.venvs/pkgmgr..."
|
@echo "Installing required Python packages into $$HOME/.venvs/pkgmgr..."
|
||||||
@~/.venvs/pkgmgr/bin/python -m ensurepip --upgrade
|
@$$HOME/.venvs/pkgmgr/bin/python -m ensurepip --upgrade
|
||||||
@~/.venvs/pkgmgr/bin/pip install --upgrade pip setuptools wheel
|
@$$HOME/.venvs/pkgmgr/bin/pip install --upgrade pip setuptools wheel
|
||||||
@~/.venvs/pkgmgr/bin/pip install -r requirements.txt
|
@$$HOME/.venvs/pkgmgr/bin/pip install -r requirements.txt
|
||||||
@echo "Ensuring ~/.bashrc and ~/.zshrc exist..."
|
@echo "Ensuring $$HOME/.bashrc and $$HOME/.zshrc exist..."
|
||||||
@touch ~/.bashrc ~/.zshrc
|
@touch "$$HOME/.bashrc" "$$HOME/.zshrc"
|
||||||
@echo "Ensuring automatic activation of ~/.venvs/pkgmgr for this user..."
|
@echo "Ensuring automatic activation of $$HOME/.venvs/pkgmgr for this user..."
|
||||||
@for rc in ~/.bashrc ~/.zshrc; do \
|
@for rc in "$$HOME/.bashrc" "$$HOME/.zshrc"; do \
|
||||||
rc_line='if [ -d "$${HOME}/.venvs/pkgmgr" ]; then . "$${HOME}/.venvs/pkgmgr/bin/activate"; echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi'; \
|
rc_line='if [ -d "$${HOME}/.venvs/pkgmgr" ]; then . "$${HOME}/.venvs/pkgmgr/bin/activate"; if [ -n "$${PS1:-}" ]; then echo "Global Python virtual environment '\''~/.venvs/pkgmgr'\'' activated."; fi; fi'; \
|
||||||
grep -qxF "$${rc_line}" $$rc || echo "$${rc_line}" >> $$rc; \
|
grep -qxF "$${rc_line}" "$$rc" || echo "$${rc_line}" >> "$$rc"; \
|
||||||
done
|
done
|
||||||
@echo "Arch/Manjaro detection and optional AUR setup..."
|
@echo "Arch/Manjaro detection and optional AUR setup..."
|
||||||
@if command -v pacman >/dev/null 2>&1; then \
|
@if command -v pacman >/dev/null 2>&1; then \
|
||||||
@@ -56,9 +78,9 @@ aur_builder_setup:
|
|||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@echo "Removing global user virtual environment if it exists..."
|
@echo "Removing global user virtual environment if it exists..."
|
||||||
@rm -rf ~/.venvs/pkgmgr
|
@rm -rf "$$HOME/.venvs/pkgmgr"
|
||||||
@echo "Cleaning up ~/.bashrc and ~/.zshrc entries..."
|
@echo "Cleaning up $$HOME/.bashrc and $$HOME/.zshrc entries..."
|
||||||
@for rc in ~/.bashrc ~/.zshrc; do \
|
@for rc in "$$HOME/.bashrc" "$$HOME/.zshrc"; do \
|
||||||
sed -i '/\.venvs\/pkgmgr\/bin\/activate"; echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi/d' $$rc; \
|
sed -i '/\.venvs\/pkgmgr\/bin\/activate"; if \[ -n "\$${PS1:-}" \]; then echo "Global Python virtual environment '\''~\/\.venvs\/pkgmgr'\'' activated."; fi; fi/d' "$$rc"; \
|
||||||
done
|
done
|
||||||
@echo "Uninstallation complete. Please restart your shell (or 'exec bash' or 'exec zsh') for the changes to fully apply."
|
@echo "Uninstallation complete. Please restart your shell (or 'exec bash' or 'exec zsh') for the changes to fully apply."
|
||||||
|
|||||||
43
PKGBUILD
43
PKGBUILD
@@ -3,37 +3,38 @@
|
|||||||
pkgname=package-manager
|
pkgname=package-manager
|
||||||
pkgver=0.1.0
|
pkgver=0.1.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A configurable Python tool to manage multiple repositories via Bash and automate common Git operations."
|
pkgdesc="Wrapper that runs Kevin's package-manager via Nix flake."
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://github.com/kevinveenbirkenbach/package-manager"
|
url="https://github.com/kevinveenbirkenbach/package-manager"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
|
|
||||||
depends=(
|
# Nix is the only runtime dependency.
|
||||||
'python'
|
depends=('nix')
|
||||||
'python-yaml'
|
|
||||||
'git'
|
|
||||||
'bash'
|
|
||||||
)
|
|
||||||
|
|
||||||
makedepends=(
|
makedepends=()
|
||||||
'python-build'
|
|
||||||
'python-installer'
|
|
||||||
'python-wheel'
|
|
||||||
'python-setuptools'
|
|
||||||
)
|
|
||||||
|
|
||||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz")
|
source=()
|
||||||
sha256sums=('SKIP')
|
sha256sums=()
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$srcdir/$pkgname-$pkgver"
|
:
|
||||||
python -m build --wheel --no-isolation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$srcdir/$pkgname-$pkgver"
|
install -d "$pkgdir/usr/bin"
|
||||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
|
||||||
|
|
||||||
# Optional: add pkgmgr executable symlink
|
cat > "$pkgdir/usr/bin/pkgmgr" << 'EOF'
|
||||||
install -Dm755 main.py "$pkgdir/usr/bin/pkgmgr"
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Enable flakes if not already configured.
|
||||||
|
if [[ -z "${NIX_CONFIG:-}" ]]; then
|
||||||
|
export NIX_CONFIG="experimental-features = nix-command flakes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run package-manager via Nix flake
|
||||||
|
exec nix run "github:kevinveenbirkenbach/package-manager#pkgmgr" -- "$@"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod 755 "$pkgdir/usr/bin/pkgmgr"
|
||||||
}
|
}
|
||||||
|
|||||||
91
flake.nix
91
flake.nix
@@ -1,7 +1,3 @@
|
|||||||
# flake.nix
|
|
||||||
# This file defines a Nix flake providing a reproducible development environment
|
|
||||||
# and optional installation package for the package-manager tool.
|
|
||||||
|
|
||||||
{
|
{
|
||||||
description = "Nix flake for Kevin's package-manager tool";
|
description = "Nix flake for Kevin's package-manager tool";
|
||||||
|
|
||||||
@@ -11,30 +7,75 @@
|
|||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
outputs = { self, nixpkgs }:
|
||||||
let
|
let
|
||||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
|
||||||
|
# Small helper: build an attrset for all systems
|
||||||
|
forAllSystems = f:
|
||||||
|
builtins.listToAttrs (map (system: {
|
||||||
|
name = system;
|
||||||
|
value = f system;
|
||||||
|
}) systems);
|
||||||
in {
|
in {
|
||||||
|
# Dev shells: nix develop .#default (on both architectures)
|
||||||
|
devShells = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
python = pkgs.python311;
|
||||||
|
pypkgs = pkgs.python311Packages;
|
||||||
|
|
||||||
# Development environment used via: nix develop
|
# Be robust: ansible-core if available, otherwise ansible.
|
||||||
devShells.default = pkgs.mkShell {
|
ansiblePkg =
|
||||||
# System packages for development
|
if pkgs ? ansible-core then pkgs.ansible-core
|
||||||
buildInputs = [
|
else pkgs.ansible;
|
||||||
pkgs.python311
|
in {
|
||||||
pkgs.python311Packages.pyyaml
|
default = pkgs.mkShell {
|
||||||
pkgs.git
|
buildInputs = [
|
||||||
];
|
python
|
||||||
|
pypkgs.pyyaml
|
||||||
|
pkgs.git
|
||||||
|
ansiblePkg
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
echo "Entered pkgmgr development environment for ${system}";
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
# Message shown on environment entry
|
# Packages: nix build .#pkgmgr / .#default
|
||||||
shellHook = ''
|
packages = forAllSystems (system:
|
||||||
echo "Entered pkgmgr development environment";
|
let
|
||||||
'';
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
};
|
python = pkgs.python311;
|
||||||
|
pypkgs = pkgs.python311Packages;
|
||||||
|
|
||||||
# Optional installable package for "nix profile install"
|
pkgmgrPkg = pypkgs.buildPythonApplication {
|
||||||
packages.pkgmgr = pkgs.python311Packages.buildPythonApplication {
|
pname = "package-manager";
|
||||||
pname = "package-manager";
|
version = "0.1.0";
|
||||||
version = "0.1.0";
|
src = ./.;
|
||||||
src = ./.;
|
|
||||||
propagatedBuildInputs = [ pkgs.python311Packages.pyyaml ];
|
propagatedBuildInputs = [
|
||||||
};
|
pypkgs.pyyaml
|
||||||
|
# add further dependencies here
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
pkgmgr = pkgmgrPkg;
|
||||||
|
default = pkgmgrPkg;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
# Apps: nix run .#pkgmgr / .#default
|
||||||
|
apps = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgmgrPkg = self.packages.${system}.pkgmgr;
|
||||||
|
in {
|
||||||
|
pkgmgr = {
|
||||||
|
type = "app";
|
||||||
|
program = "${pkgmgrPkg}/bin/pkgmgr";
|
||||||
|
};
|
||||||
|
default = self.apps.${system}.pkgmgr;
|
||||||
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
30
pkgmgr/context.py
Normal file
30
pkgmgr/context.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Shared context object for repository installation steps.
|
||||||
|
|
||||||
|
This data class bundles all information needed by installer components so
|
||||||
|
they do not depend on global state or long parameter lists.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RepoContext:
|
||||||
|
"""Container for all repository-related data used during installation."""
|
||||||
|
|
||||||
|
repo: Dict[str, Any]
|
||||||
|
identifier: str
|
||||||
|
repo_dir: str
|
||||||
|
repositories_base_dir: str
|
||||||
|
bin_dir: str
|
||||||
|
all_repos: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
no_verification: bool
|
||||||
|
preview: bool
|
||||||
|
quiet: bool
|
||||||
|
clone_mode: str
|
||||||
|
update_dependencies: bool
|
||||||
@@ -1,243 +1,203 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Repository installation pipeline for pkgmgr.
|
||||||
|
|
||||||
|
This module orchestrates the installation of repositories by:
|
||||||
|
|
||||||
|
1. Ensuring the repository directory exists (cloning if necessary).
|
||||||
|
2. Verifying the repository according to the configured policies.
|
||||||
|
3. Creating executable links using create_ink().
|
||||||
|
4. Running a sequence of modular installer components that handle
|
||||||
|
specific technologies or manifests (pkgmgr.yml, PKGBUILD, Nix,
|
||||||
|
Ansible requirements, Python, Makefile).
|
||||||
|
|
||||||
|
The goal is to keep this file thin and delegate most logic to small,
|
||||||
|
focused installer classes.
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
from typing import List, Dict, Any, Tuple
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from pkgmgr.get_repo_identifier import get_repo_identifier
|
from pkgmgr.get_repo_identifier import get_repo_identifier
|
||||||
from pkgmgr.get_repo_dir import get_repo_dir
|
from pkgmgr.get_repo_dir import get_repo_dir
|
||||||
from pkgmgr.create_ink import create_ink
|
from pkgmgr.create_ink import create_ink
|
||||||
from pkgmgr.run_command import run_command
|
|
||||||
from pkgmgr.verify import verify_repository
|
from pkgmgr.verify import verify_repository
|
||||||
from pkgmgr.clone_repos import clone_repos
|
from pkgmgr.clone_repos import clone_repos
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
|
||||||
def _extract_pkgbuild_array(repo_dir: str, var_name: str) -> list:
|
# Installer implementations
|
||||||
"""
|
from pkgmgr.installers.pkgmgr_manifest import PkgmgrManifestInstaller
|
||||||
Extract a Bash array (depends/makedepends) from PKGBUILD using bash itself.
|
from pkgmgr.installers.pkgbuild import PkgbuildInstaller
|
||||||
Returns a list of package names or an empty list on error.
|
from pkgmgr.installers.nix_flake import NixFlakeInstaller
|
||||||
"""
|
from pkgmgr.installers.ansible_requirements import AnsibleRequirementsInstaller
|
||||||
pkgbuild_path = os.path.join(repo_dir, "PKGBUILD")
|
from pkgmgr.installers.python import PythonInstaller
|
||||||
if not os.path.exists(pkgbuild_path):
|
from pkgmgr.installers.makefile import MakefileInstaller
|
||||||
return []
|
from pkgmgr.installers.aur import AurInstaller
|
||||||
|
|
||||||
script = f'source PKGBUILD >/dev/null 2>&1; printf "%s\\n" "${{{var_name}[@]}}"'
|
|
||||||
try:
|
# Ordered list of installers to apply to each repository
|
||||||
output = subprocess.check_output(
|
INSTALLERS = [
|
||||||
["bash", "-lc", script],
|
PkgmgrManifestInstaller(),
|
||||||
cwd=repo_dir,
|
PkgbuildInstaller(),
|
||||||
text=True,
|
NixFlakeInstaller(),
|
||||||
|
AnsibleRequirementsInstaller(),
|
||||||
|
PythonInstaller(),
|
||||||
|
MakefileInstaller(),
|
||||||
|
AurInstaller(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_repo_dir(
|
||||||
|
repo: Dict[str, Any],
|
||||||
|
repositories_base_dir: str,
|
||||||
|
all_repos: List[Dict[str, Any]],
|
||||||
|
preview: bool,
|
||||||
|
no_verification: bool,
|
||||||
|
clone_mode: str,
|
||||||
|
identifier: str,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
Ensure the repository directory exists. If not, attempt to clone it.
|
||||||
|
|
||||||
|
Returns the repository directory path or an empty string if cloning failed.
|
||||||
|
"""
|
||||||
|
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
||||||
|
|
||||||
|
if not os.path.exists(repo_dir):
|
||||||
|
print(f"Repository directory '{repo_dir}' does not exist. Cloning it now...")
|
||||||
|
clone_repos(
|
||||||
|
[repo],
|
||||||
|
repositories_base_dir,
|
||||||
|
all_repos,
|
||||||
|
preview,
|
||||||
|
no_verification,
|
||||||
|
clone_mode,
|
||||||
)
|
)
|
||||||
except Exception:
|
if not os.path.exists(repo_dir):
|
||||||
return []
|
print(f"Cloning failed for repository {identifier}. Skipping installation.")
|
||||||
|
return ""
|
||||||
|
|
||||||
return [line.strip() for line in output.splitlines() if line.strip()]
|
return repo_dir
|
||||||
|
|
||||||
|
|
||||||
def _install_arch_dependencies_from_pkgbuild(repo_dir: str, preview: bool) -> None:
|
def _verify_repo(
|
||||||
"""
|
repo: Dict[str, Any],
|
||||||
If PKGBUILD exists and pacman is available, install depends + makedepends
|
|
||||||
via pacman.
|
|
||||||
"""
|
|
||||||
if shutil.which("pacman") is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
pkgbuild_path = os.path.join(repo_dir, "PKGBUILD")
|
|
||||||
if not os.path.exists(pkgbuild_path):
|
|
||||||
return
|
|
||||||
|
|
||||||
depends = _extract_pkgbuild_array(repo_dir, "depends")
|
|
||||||
makedepends = _extract_pkgbuild_array(repo_dir, "makedepends")
|
|
||||||
all_pkgs = depends + makedepends
|
|
||||||
|
|
||||||
if not all_pkgs:
|
|
||||||
return
|
|
||||||
|
|
||||||
cmd = "sudo pacman -S --noconfirm " + " ".join(all_pkgs)
|
|
||||||
run_command(cmd, preview=preview)
|
|
||||||
|
|
||||||
|
|
||||||
def _install_nix_flake_profile(repo_dir: str, preview: bool) -> None:
|
|
||||||
"""
|
|
||||||
If flake.nix exists and 'nix' is available, try to install a profile
|
|
||||||
from the flake. Convention: try .#pkgmgr, then .#default.
|
|
||||||
"""
|
|
||||||
flake_path = os.path.join(repo_dir, "flake.nix")
|
|
||||||
if not os.path.exists(flake_path):
|
|
||||||
return
|
|
||||||
if shutil.which("nix") is None:
|
|
||||||
print("Warning: flake.nix found but 'nix' command not available. Skipping flake setup.")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("Nix flake detected, attempting to install profile output...")
|
|
||||||
for output in ("pkgmgr", "default"):
|
|
||||||
cmd = f"nix profile install {repo_dir}#{output}"
|
|
||||||
try:
|
|
||||||
run_command(cmd, preview=preview)
|
|
||||||
print(f"Nix flake output '{output}' successfully installed.")
|
|
||||||
break
|
|
||||||
except SystemExit as e:
|
|
||||||
print(f"[Warning] Failed to install Nix flake output '{output}': {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def _install_pkgmgr_dependencies_from_manifest(
|
|
||||||
repo_dir: str,
|
repo_dir: str,
|
||||||
no_verification: bool,
|
no_verification: bool,
|
||||||
update_dependencies: bool,
|
identifier: str,
|
||||||
clone_mode: str,
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Verify the repository using verify_repository().
|
||||||
|
|
||||||
|
Returns True if installation should proceed, False if it should be skipped.
|
||||||
|
"""
|
||||||
|
verified_info = repo.get("verified")
|
||||||
|
verified_ok, errors, commit_hash, signing_key = verify_repository(
|
||||||
|
repo,
|
||||||
|
repo_dir,
|
||||||
|
mode="local",
|
||||||
|
no_verification=no_verification,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not no_verification and verified_info and not verified_ok:
|
||||||
|
print(f"Warning: Verification failed for {identifier}:")
|
||||||
|
for err in errors:
|
||||||
|
print(f" - {err}")
|
||||||
|
choice = input("Proceed with installation? (y/N): ").strip().lower()
|
||||||
|
if choice != "y":
|
||||||
|
print(f"Skipping installation for {identifier}.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _create_context(
|
||||||
|
repo: Dict[str, Any],
|
||||||
|
identifier: str,
|
||||||
|
repo_dir: str,
|
||||||
|
repositories_base_dir: str,
|
||||||
|
bin_dir: str,
|
||||||
|
all_repos: List[Dict[str, Any]],
|
||||||
|
no_verification: bool,
|
||||||
preview: bool,
|
preview: bool,
|
||||||
) -> None:
|
quiet: bool,
|
||||||
|
clone_mode: str,
|
||||||
|
update_dependencies: bool,
|
||||||
|
) -> RepoContext:
|
||||||
"""
|
"""
|
||||||
Read pkgmgr.yml (if present) and install referenced pkgmgr repository
|
Build a RepoContext for the given repository and parameters.
|
||||||
dependencies.
|
|
||||||
|
|
||||||
Expected format:
|
|
||||||
|
|
||||||
version: 1
|
|
||||||
author: "..."
|
|
||||||
url: "..."
|
|
||||||
description: "..."
|
|
||||||
dependencies:
|
|
||||||
- repository: github:user/repo
|
|
||||||
version: main
|
|
||||||
reason: "Optional description"
|
|
||||||
"""
|
"""
|
||||||
manifest_path = os.path.join(repo_dir, "pkgmgr.yml")
|
return RepoContext(
|
||||||
if not os.path.exists(manifest_path):
|
repo=repo,
|
||||||
return
|
identifier=identifier,
|
||||||
|
repo_dir=repo_dir,
|
||||||
try:
|
repositories_base_dir=repositories_base_dir,
|
||||||
with open(manifest_path, "r", encoding="utf-8") as f:
|
bin_dir=bin_dir,
|
||||||
manifest = yaml.safe_load(f) or {}
|
all_repos=all_repos,
|
||||||
except Exception as e:
|
no_verification=no_verification,
|
||||||
print(f"Error loading pkgmgr.yml in '{repo_dir}': {e}")
|
preview=preview,
|
||||||
return
|
quiet=quiet,
|
||||||
|
clone_mode=clone_mode,
|
||||||
dependencies = manifest.get("dependencies", []) or []
|
update_dependencies=update_dependencies,
|
||||||
if not isinstance(dependencies, list) or not dependencies:
|
)
|
||||||
return
|
|
||||||
|
|
||||||
# Optional: show basic metadata (author/url/description) if present
|
|
||||||
author = manifest.get("author")
|
|
||||||
url = manifest.get("url")
|
|
||||||
description = manifest.get("description")
|
|
||||||
|
|
||||||
if not preview:
|
|
||||||
print("pkgmgr manifest detected:")
|
|
||||||
if author:
|
|
||||||
print(f" author: {author}")
|
|
||||||
if url:
|
|
||||||
print(f" url: {url}")
|
|
||||||
if description:
|
|
||||||
print(f" description: {description}")
|
|
||||||
|
|
||||||
dep_repo_ids = []
|
|
||||||
for dep in dependencies:
|
|
||||||
if not isinstance(dep, dict):
|
|
||||||
continue
|
|
||||||
repo_id = dep.get("repository")
|
|
||||||
if repo_id:
|
|
||||||
dep_repo_ids.append(str(repo_id))
|
|
||||||
|
|
||||||
# Optionally: update (pull) dependencies before installing
|
|
||||||
if update_dependencies and dep_repo_ids:
|
|
||||||
cmd_pull = "pkgmgr pull " + " ".join(dep_repo_ids)
|
|
||||||
try:
|
|
||||||
run_command(cmd_pull, preview=preview)
|
|
||||||
except SystemExit as e:
|
|
||||||
print(f"Warning: 'pkgmgr pull' for dependencies failed (exit code {e}).")
|
|
||||||
|
|
||||||
# Install dependencies one by one
|
|
||||||
for dep in dependencies:
|
|
||||||
if not isinstance(dep, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
repo_id = dep.get("repository")
|
|
||||||
if not repo_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
version = dep.get("version")
|
|
||||||
reason = dep.get("reason")
|
|
||||||
|
|
||||||
if reason and not preview:
|
|
||||||
print(f"Installing dependency {repo_id}: {reason}")
|
|
||||||
else:
|
|
||||||
print(f"Installing dependency {repo_id}...")
|
|
||||||
|
|
||||||
cmd = f"pkgmgr install {repo_id}"
|
|
||||||
|
|
||||||
if version:
|
|
||||||
cmd += f" --version {version}"
|
|
||||||
|
|
||||||
if no_verification:
|
|
||||||
cmd += " --no-verification"
|
|
||||||
|
|
||||||
if update_dependencies:
|
|
||||||
cmd += " --dependencies"
|
|
||||||
|
|
||||||
if clone_mode:
|
|
||||||
cmd += f" --clone-mode {clone_mode}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
run_command(cmd, preview=preview)
|
|
||||||
except SystemExit as e:
|
|
||||||
print(f"[Warning] Failed to install dependency '{repo_id}': {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def install_repos(
|
def install_repos(
|
||||||
selected_repos,
|
selected_repos: List[Dict[str, Any]],
|
||||||
repositories_base_dir,
|
repositories_base_dir: str,
|
||||||
bin_dir,
|
bin_dir: str,
|
||||||
all_repos,
|
all_repos: List[Dict[str, Any]],
|
||||||
no_verification,
|
no_verification: bool,
|
||||||
preview,
|
preview: bool,
|
||||||
quiet,
|
quiet: bool,
|
||||||
clone_mode: str,
|
clone_mode: str,
|
||||||
update_dependencies: bool,
|
update_dependencies: bool,
|
||||||
):
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Install repositories by creating symbolic links and processing standard
|
Install repositories by creating symbolic links and processing standard
|
||||||
manifest files (pkgmgr.yml, PKGBUILD, flake.nix, Ansible requirements,
|
manifest files (pkgmgr.yml, PKGBUILD, flake.nix, Ansible requirements,
|
||||||
Python manifests, Makefile).
|
Python manifests, Makefile) via dedicated installer components.
|
||||||
"""
|
"""
|
||||||
for repo in selected_repos:
|
for repo in selected_repos:
|
||||||
repo_identifier = get_repo_identifier(repo, all_repos)
|
identifier = get_repo_identifier(repo, all_repos)
|
||||||
repo_dir = get_repo_dir(repositories_base_dir, repo)
|
repo_dir = _ensure_repo_dir(
|
||||||
|
repo=repo,
|
||||||
if not os.path.exists(repo_dir):
|
repositories_base_dir=repositories_base_dir,
|
||||||
print(f"Repository directory '{repo_dir}' does not exist. Cloning it now...")
|
all_repos=all_repos,
|
||||||
# Pass the clone_mode parameter to clone_repos
|
preview=preview,
|
||||||
clone_repos(
|
|
||||||
[repo],
|
|
||||||
repositories_base_dir,
|
|
||||||
all_repos,
|
|
||||||
preview,
|
|
||||||
no_verification,
|
|
||||||
clone_mode,
|
|
||||||
)
|
|
||||||
if not os.path.exists(repo_dir):
|
|
||||||
print(f"Cloning failed for repository {repo_identifier}. Skipping installation.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
verified_info = repo.get("verified")
|
|
||||||
verified_ok, errors, commit_hash, signing_key = verify_repository(
|
|
||||||
repo,
|
|
||||||
repo_dir,
|
|
||||||
mode="local",
|
|
||||||
no_verification=no_verification,
|
no_verification=no_verification,
|
||||||
|
clone_mode=clone_mode,
|
||||||
|
identifier=identifier,
|
||||||
|
)
|
||||||
|
if not repo_dir:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not _verify_repo(
|
||||||
|
repo=repo,
|
||||||
|
repo_dir=repo_dir,
|
||||||
|
no_verification=no_verification,
|
||||||
|
identifier=identifier,
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
ctx = _create_context(
|
||||||
|
repo=repo,
|
||||||
|
identifier=identifier,
|
||||||
|
repo_dir=repo_dir,
|
||||||
|
repositories_base_dir=repositories_base_dir,
|
||||||
|
bin_dir=bin_dir,
|
||||||
|
all_repos=all_repos,
|
||||||
|
no_verification=no_verification,
|
||||||
|
preview=preview,
|
||||||
|
quiet=quiet,
|
||||||
|
clone_mode=clone_mode,
|
||||||
|
update_dependencies=update_dependencies,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not no_verification and verified_info and not verified_ok:
|
# Create the symlink using create_ink before running installers.
|
||||||
print(f"Warning: Verification failed for {repo_identifier}:")
|
|
||||||
for err in errors:
|
|
||||||
print(f" - {err}")
|
|
||||||
choice = input("Proceed with installation? (y/N): ").strip().lower()
|
|
||||||
if choice != "y":
|
|
||||||
print(f"Skipping installation for {repo_identifier}.")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Create the symlink using create_ink.
|
|
||||||
create_ink(
|
create_ink(
|
||||||
repo,
|
repo,
|
||||||
repositories_base_dir,
|
repositories_base_dir,
|
||||||
@@ -247,77 +207,7 @@ def install_repos(
|
|||||||
preview=preview,
|
preview=preview,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 1) pkgmgr.yml (pkgmgr-internal manifest for other repositories)
|
# Run all installers that support this repository.
|
||||||
_install_pkgmgr_dependencies_from_manifest(
|
for installer in INSTALLERS:
|
||||||
repo_dir=repo_dir,
|
if installer.supports(ctx):
|
||||||
no_verification=no_verification,
|
installer.run(ctx)
|
||||||
update_dependencies=update_dependencies,
|
|
||||||
clone_mode=clone_mode,
|
|
||||||
preview=preview,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2) Arch: PKGBUILD (depends/makedepends)
|
|
||||||
_install_arch_dependencies_from_pkgbuild(repo_dir, preview=preview)
|
|
||||||
|
|
||||||
# 3) Nix: flake.nix
|
|
||||||
_install_nix_flake_profile(repo_dir, preview=preview)
|
|
||||||
|
|
||||||
# 4) Ansible: requirements.yml (only collections/roles)
|
|
||||||
req_file = os.path.join(repo_dir, "requirements.yml")
|
|
||||||
if os.path.exists(req_file):
|
|
||||||
try:
|
|
||||||
with open(req_file, "r", encoding="utf-8") as f:
|
|
||||||
requirements = yaml.safe_load(f) or {}
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading requirements.yml in {repo_identifier}: {e}")
|
|
||||||
requirements = None
|
|
||||||
|
|
||||||
if requirements and isinstance(requirements, dict):
|
|
||||||
if "collections" in requirements or "roles" in requirements:
|
|
||||||
print(f"Ansible dependencies found in {repo_identifier}, installing...")
|
|
||||||
|
|
||||||
ansible_requirements = {}
|
|
||||||
if "collections" in requirements:
|
|
||||||
ansible_requirements["collections"] = requirements["collections"]
|
|
||||||
if "roles" in requirements:
|
|
||||||
ansible_requirements["roles"] = requirements["roles"]
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
mode="w",
|
|
||||||
suffix=".yml",
|
|
||||||
delete=False,
|
|
||||||
) as tmp:
|
|
||||||
yaml.dump(ansible_requirements, tmp, default_flow_style=False)
|
|
||||||
tmp_filename = tmp.name
|
|
||||||
|
|
||||||
if "collections" in ansible_requirements:
|
|
||||||
print(f"Ansible collections found in {repo_identifier}, installing...")
|
|
||||||
cmd = f"ansible-galaxy collection install -r {tmp_filename}"
|
|
||||||
run_command(cmd, cwd=repo_dir, preview=preview)
|
|
||||||
|
|
||||||
if "roles" in ansible_requirements:
|
|
||||||
print(f"Ansible roles found in {repo_identifier}, installing...")
|
|
||||||
cmd = f"ansible-galaxy role install -r {tmp_filename}"
|
|
||||||
run_command(cmd, cwd=repo_dir, preview=preview)
|
|
||||||
|
|
||||||
# 5) Python: pyproject.toml (modern) / requirements.txt (classic)
|
|
||||||
pyproject_path = os.path.join(repo_dir, "pyproject.toml")
|
|
||||||
if os.path.exists(pyproject_path):
|
|
||||||
print(f"pyproject.toml found in {repo_identifier}, installing Python project...")
|
|
||||||
cmd = "~/.venvs/pkgmgr/bin/pip install ."
|
|
||||||
run_command(cmd, cwd=repo_dir, preview=preview)
|
|
||||||
|
|
||||||
req_txt_file = os.path.join(repo_dir, "requirements.txt")
|
|
||||||
if os.path.exists(req_txt_file):
|
|
||||||
print(f"requirements.txt found in {repo_identifier}, installing Python dependencies...")
|
|
||||||
cmd = "~/.venvs/pkgmgr/bin/pip install -r requirements.txt"
|
|
||||||
run_command(cmd, cwd=repo_dir, preview=preview)
|
|
||||||
|
|
||||||
# 6) Makefile: make install (if present)
|
|
||||||
makefile_path = os.path.join(repo_dir, "Makefile")
|
|
||||||
if os.path.exists(makefile_path):
|
|
||||||
cmd = "make install"
|
|
||||||
try:
|
|
||||||
run_command(cmd, cwd=repo_dir, preview=preview)
|
|
||||||
except SystemExit as e:
|
|
||||||
print(f"[Warning] Failed to run '{cmd}' for {repo_identifier}: {e}")
|
|
||||||
|
|||||||
9
pkgmgr/installers/__init__.py
Normal file
9
pkgmgr/installers/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installer package for pkgmgr.
|
||||||
|
|
||||||
|
Each installer implements a small, focused step in the repository
|
||||||
|
installation pipeline (e.g. PKGBUILD dependencies, Nix flakes, Python, etc.).
|
||||||
|
"""
|
||||||
71
pkgmgr/installers/ansible_requirements.py
Normal file
71
pkgmgr/installers/ansible_requirements.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installer for Ansible dependencies defined in requirements.yml.
|
||||||
|
|
||||||
|
This installer installs collections and roles via ansible-galaxy when found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleRequirementsInstaller(BaseInstaller):
|
||||||
|
"""Install Ansible collections and roles from requirements.yml."""
|
||||||
|
|
||||||
|
REQUIREMENTS_FILE = "requirements.yml"
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
req_file = os.path.join(ctx.repo_dir, self.REQUIREMENTS_FILE)
|
||||||
|
return os.path.exists(req_file)
|
||||||
|
|
||||||
|
def _load_requirements(self, req_path: str, identifier: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
with open(req_path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Error loading {self.REQUIREMENTS_FILE} in {identifier}: {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
req_file = os.path.join(ctx.repo_dir, self.REQUIREMENTS_FILE)
|
||||||
|
requirements = self._load_requirements(req_file, ctx.identifier)
|
||||||
|
if not requirements or not isinstance(requirements, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
if "collections" not in requirements and "roles" not in requirements:
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Ansible dependencies found in {ctx.identifier}, installing...")
|
||||||
|
|
||||||
|
ansible_requirements: Dict[str, Any] = {}
|
||||||
|
if "collections" in requirements:
|
||||||
|
ansible_requirements["collections"] = requirements["collections"]
|
||||||
|
if "roles" in requirements:
|
||||||
|
ansible_requirements["roles"] = requirements["roles"]
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="w",
|
||||||
|
suffix=".yml",
|
||||||
|
delete=False,
|
||||||
|
) as tmp:
|
||||||
|
yaml.dump(ansible_requirements, tmp, default_flow_style=False)
|
||||||
|
tmp_filename = tmp.name
|
||||||
|
|
||||||
|
if "collections" in ansible_requirements:
|
||||||
|
print(f"Ansible collections found in {ctx.identifier}, installing...")
|
||||||
|
cmd = f"ansible-galaxy collection install -r {tmp_filename}"
|
||||||
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
|
||||||
|
if "roles" in ansible_requirements:
|
||||||
|
print(f"Ansible roles found in {ctx.identifier}, installing...")
|
||||||
|
cmd = f"ansible-galaxy role install -r {tmp_filename}"
|
||||||
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
131
pkgmgr/installers/aur.py
Normal file
131
pkgmgr/installers/aur.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# pkgmgr/installers/aur.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import yaml
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
AUR_CONFIG_FILENAME = "aur.yml"
|
||||||
|
|
||||||
|
|
||||||
|
class AurInstaller(BaseInstaller):
|
||||||
|
"""
|
||||||
|
Installer for Arch AUR dependencies declared in an `aur.yml` file.
|
||||||
|
|
||||||
|
This installer is:
|
||||||
|
- Arch-only (requires `pacman`)
|
||||||
|
- optional helper-driven (yay/paru/..)
|
||||||
|
- safe to ignore on non-Arch systems
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _is_arch_like(self) -> bool:
|
||||||
|
return shutil.which("pacman") is not None
|
||||||
|
|
||||||
|
def _config_path(self, ctx: RepoContext) -> str:
|
||||||
|
return os.path.join(ctx.repo_dir, AUR_CONFIG_FILENAME)
|
||||||
|
|
||||||
|
def _load_config(self, ctx: RepoContext) -> dict:
|
||||||
|
path = self._config_path(ctx)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
data = yaml.safe_load(f) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[Warning] Failed to load AUR config from '{path}': {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
print(f"[Warning] AUR config '{path}' is not a mapping. Ignoring.")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _get_helper(self, cfg: dict) -> str:
|
||||||
|
# Priority: config.helper > $AUR_HELPER > "yay"
|
||||||
|
helper = cfg.get("helper")
|
||||||
|
if isinstance(helper, str) and helper.strip():
|
||||||
|
return helper.strip()
|
||||||
|
|
||||||
|
env_helper = os.environ.get("AUR_HELPER")
|
||||||
|
if env_helper:
|
||||||
|
return env_helper.strip()
|
||||||
|
|
||||||
|
return "yay"
|
||||||
|
|
||||||
|
def _get_packages(self, cfg: dict) -> List[str]:
|
||||||
|
raw = cfg.get("packages", [])
|
||||||
|
if not isinstance(raw, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
names: List[str] = []
|
||||||
|
for entry in raw:
|
||||||
|
if isinstance(entry, str):
|
||||||
|
name = entry.strip()
|
||||||
|
if name:
|
||||||
|
names.append(name)
|
||||||
|
elif isinstance(entry, dict):
|
||||||
|
name = str(entry.get("name", "")).strip()
|
||||||
|
if name:
|
||||||
|
names.append(name)
|
||||||
|
|
||||||
|
return names
|
||||||
|
|
||||||
|
# --- BaseInstaller API -------------------------------------------------
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
"""
|
||||||
|
This installer is supported if:
|
||||||
|
- We are on an Arch-like system (pacman available),
|
||||||
|
- An aur.yml exists,
|
||||||
|
- That aur.yml declares at least one package.
|
||||||
|
"""
|
||||||
|
if not self._is_arch_like():
|
||||||
|
return False
|
||||||
|
|
||||||
|
cfg = self._load_config(ctx)
|
||||||
|
if not cfg:
|
||||||
|
return False
|
||||||
|
|
||||||
|
packages = self._get_packages(cfg)
|
||||||
|
return len(packages) > 0
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
"""
|
||||||
|
Install AUR packages using the configured helper (default: yay).
|
||||||
|
"""
|
||||||
|
if not self._is_arch_like():
|
||||||
|
print("AUR installer skipped: not an Arch-like system.")
|
||||||
|
return
|
||||||
|
|
||||||
|
cfg = self._load_config(ctx)
|
||||||
|
if not cfg:
|
||||||
|
print("AUR installer: no valid aur.yml found; skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
packages = self._get_packages(cfg)
|
||||||
|
if not packages:
|
||||||
|
print("AUR installer: no AUR packages defined; skipping.")
|
||||||
|
return
|
||||||
|
|
||||||
|
helper = self._get_helper(cfg)
|
||||||
|
if shutil.which(helper) is None:
|
||||||
|
print(
|
||||||
|
f"[Warning] AUR helper '{helper}' is not available on PATH. "
|
||||||
|
f"Please install it (e.g. via your aur_builder setup). "
|
||||||
|
f"Skipping AUR installation."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
pkg_list_str = " ".join(packages)
|
||||||
|
print(f"Installing AUR packages via '{helper}': {pkg_list_str}")
|
||||||
|
|
||||||
|
cmd = f"{helper} -S --noconfirm {pkg_list_str}"
|
||||||
|
# We respect preview mode to allow dry runs.
|
||||||
|
run_command(cmd, preview=ctx.preview)
|
||||||
34
pkgmgr/installers/base.py
Normal file
34
pkgmgr/installers/base.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base interface for all installer components in the pkgmgr installation pipeline.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInstaller(ABC):
|
||||||
|
"""
|
||||||
|
A single step in the installation pipeline for a repository.
|
||||||
|
|
||||||
|
Implementations should be small and focused on one technology or manifest
|
||||||
|
type (e.g. PKGBUILD, Nix, Python, Ansible).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if this installer should run for the given repository
|
||||||
|
context. This is typically based on file existence or platform checks.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
"""
|
||||||
|
Execute the installer logic for the given repository context.
|
||||||
|
Implementations may raise SystemExit via run_command() on errors.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
32
pkgmgr/installers/makefile.py
Normal file
32
pkgmgr/installers/makefile.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installer that triggers `make install` if a Makefile is present.
|
||||||
|
|
||||||
|
This is useful for repositories that expose a standard Makefile-based
|
||||||
|
installation step.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class MakefileInstaller(BaseInstaller):
|
||||||
|
"""Run `make install` if a Makefile exists in the repository."""
|
||||||
|
|
||||||
|
MAKEFILE_NAME = "Makefile"
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
makefile_path = os.path.join(ctx.repo_dir, self.MAKEFILE_NAME)
|
||||||
|
return os.path.exists(makefile_path)
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
cmd = "make install"
|
||||||
|
try:
|
||||||
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
except SystemExit as exc:
|
||||||
|
print(f"[Warning] Failed to run '{cmd}' for {ctx.identifier}: {exc}")
|
||||||
47
pkgmgr/installers/nix_flake.py
Normal file
47
pkgmgr/installers/nix_flake.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installer for Nix flakes.
|
||||||
|
|
||||||
|
If a repository contains flake.nix and the 'nix' command is available, this
|
||||||
|
installer will try to install a profile output from the flake.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class NixFlakeInstaller(BaseInstaller):
|
||||||
|
"""Install Nix flake profiles for repositories that define flake.nix."""
|
||||||
|
|
||||||
|
FLAKE_FILE = "flake.nix"
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
if shutil.which("nix") is None:
|
||||||
|
return False
|
||||||
|
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
|
||||||
|
return os.path.exists(flake_path)
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
|
||||||
|
if not os.path.exists(flake_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
if shutil.which("nix") is None:
|
||||||
|
print("Warning: flake.nix found but 'nix' command not available. Skipping flake setup.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Nix flake detected, attempting to install profile output...")
|
||||||
|
for output in ("pkgmgr", "default"):
|
||||||
|
cmd = f"nix profile install {ctx.repo_dir}#{output}"
|
||||||
|
try:
|
||||||
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
print(f"Nix flake output '{output}' successfully installed.")
|
||||||
|
except SystemExit as e:
|
||||||
|
print(f"[Warning] Failed to install Nix flake output '{output}': {e}")
|
||||||
|
|
||||||
71
pkgmgr/installers/pkgbuild.py
Normal file
71
pkgmgr/installers/pkgbuild.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installer for Arch Linux dependencies defined in PKGBUILD files.
|
||||||
|
|
||||||
|
This installer extracts depends/makedepends from PKGBUILD and installs them
|
||||||
|
via pacman on Arch-based systems.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class PkgbuildInstaller(BaseInstaller):
|
||||||
|
"""Install Arch dependencies (depends/makedepends) from PKGBUILD."""
|
||||||
|
|
||||||
|
PKGBUILD_NAME = "PKGBUILD"
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
if shutil.which("pacman") is None:
|
||||||
|
return False
|
||||||
|
pkgbuild_path = os.path.join(ctx.repo_dir, self.PKGBUILD_NAME)
|
||||||
|
return os.path.exists(pkgbuild_path)
|
||||||
|
|
||||||
|
def _extract_pkgbuild_array(self, ctx: RepoContext, var_name: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Extract a Bash array (depends/makedepends) from PKGBUILD using bash itself.
|
||||||
|
Returns a list of package names or an empty list on error.
|
||||||
|
|
||||||
|
Uses a minimal shell environment (no profile/rc) to avoid noise from MOTD
|
||||||
|
or interactive shell banners polluting the output.
|
||||||
|
"""
|
||||||
|
pkgbuild_path = os.path.join(ctx.repo_dir, self.PKGBUILD_NAME)
|
||||||
|
if not os.path.exists(pkgbuild_path):
|
||||||
|
return []
|
||||||
|
|
||||||
|
script = f'source {self.PKGBUILD_NAME} >/dev/null 2>&1; printf "%s\\n" "${{{var_name}[@]}}"'
|
||||||
|
try:
|
||||||
|
output = subprocess.check_output(
|
||||||
|
["bash", "--noprofile", "--norc", "-c", script],
|
||||||
|
cwd=ctx.repo_dir,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
packages: List[str] = []
|
||||||
|
for line in output.splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
packages.append(line)
|
||||||
|
return packages
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
depends = self._extract_pkgbuild_array(ctx, "depends")
|
||||||
|
makedepends = self._extract_pkgbuild_array(ctx, "makedepends")
|
||||||
|
all_pkgs = depends + makedepends
|
||||||
|
|
||||||
|
if not all_pkgs:
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = "sudo pacman -S --noconfirm " + " ".join(all_pkgs)
|
||||||
|
run_command(cmd, preview=ctx.preview)
|
||||||
114
pkgmgr/installers/pkgmgr_manifest.py
Normal file
114
pkgmgr/installers/pkgmgr_manifest.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Installer for pkgmgr.yml manifest dependencies.
|
||||||
|
|
||||||
|
This installer reads pkgmgr.yml (if present) and installs referenced pkgmgr
|
||||||
|
repository dependencies via pkgmgr itself.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class PkgmgrManifestInstaller(BaseInstaller):
|
||||||
|
"""Install pkgmgr-defined repository dependencies from pkgmgr.yml."""
|
||||||
|
|
||||||
|
MANIFEST_NAME = "pkgmgr.yml"
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
manifest_path = os.path.join(ctx.repo_dir, self.MANIFEST_NAME)
|
||||||
|
return os.path.exists(manifest_path)
|
||||||
|
|
||||||
|
def _load_manifest(self, manifest_path: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
with open(manifest_path, "r", encoding="utf-8") as f:
|
||||||
|
return yaml.safe_load(f) or {}
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Error loading {self.MANIFEST_NAME} in '{manifest_path}': {exc}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _collect_dependency_ids(self, dependencies: List[Dict[str, Any]]) -> List[str]:
|
||||||
|
ids: List[str] = []
|
||||||
|
for dep in dependencies:
|
||||||
|
if not isinstance(dep, dict):
|
||||||
|
continue
|
||||||
|
repo_id = dep.get("repository")
|
||||||
|
if repo_id:
|
||||||
|
ids.append(str(repo_id))
|
||||||
|
return ids
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
manifest_path = os.path.join(ctx.repo_dir, self.MANIFEST_NAME)
|
||||||
|
manifest = self._load_manifest(manifest_path)
|
||||||
|
if not manifest:
|
||||||
|
return
|
||||||
|
|
||||||
|
dependencies = manifest.get("dependencies", []) or []
|
||||||
|
if not isinstance(dependencies, list) or not dependencies:
|
||||||
|
return
|
||||||
|
|
||||||
|
author = manifest.get("author")
|
||||||
|
url = manifest.get("url")
|
||||||
|
description = manifest.get("description")
|
||||||
|
|
||||||
|
if not ctx.preview:
|
||||||
|
print("pkgmgr manifest detected:")
|
||||||
|
if author:
|
||||||
|
print(f" author: {author}")
|
||||||
|
if url:
|
||||||
|
print(f" url: {url}")
|
||||||
|
if description:
|
||||||
|
print(f" description: {description}")
|
||||||
|
|
||||||
|
dep_repo_ids = self._collect_dependency_ids(dependencies)
|
||||||
|
|
||||||
|
if ctx.update_dependencies and dep_repo_ids:
|
||||||
|
cmd_pull = "pkgmgr pull " + " ".join(dep_repo_ids)
|
||||||
|
try:
|
||||||
|
run_command(cmd_pull, preview=ctx.preview)
|
||||||
|
except SystemExit as exc:
|
||||||
|
print(f"Warning: 'pkgmgr pull' for dependencies failed (exit code {exc}).")
|
||||||
|
|
||||||
|
# Install dependencies one by one
|
||||||
|
for dep in dependencies:
|
||||||
|
if not isinstance(dep, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
repo_id = dep.get("repository")
|
||||||
|
if not repo_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
version = dep.get("version")
|
||||||
|
reason = dep.get("reason")
|
||||||
|
|
||||||
|
if reason and not ctx.preview:
|
||||||
|
print(f"Installing dependency {repo_id}: {reason}")
|
||||||
|
else:
|
||||||
|
print(f"Installing dependency {repo_id}...")
|
||||||
|
|
||||||
|
cmd = f"pkgmgr install {repo_id}"
|
||||||
|
|
||||||
|
if version:
|
||||||
|
cmd += f" --version {version}"
|
||||||
|
|
||||||
|
if ctx.no_verification:
|
||||||
|
cmd += " --no-verification"
|
||||||
|
|
||||||
|
if ctx.update_dependencies:
|
||||||
|
cmd += " --dependencies"
|
||||||
|
|
||||||
|
if ctx.clone_mode:
|
||||||
|
cmd += f" --clone-mode {ctx.clone_mode}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
run_command(cmd, preview=ctx.preview)
|
||||||
|
except SystemExit as exc:
|
||||||
|
print(f"[Warning] Failed to install dependency '{repo_id}': {exc}")
|
||||||
89
pkgmgr/installers/python.py
Normal file
89
pkgmgr/installers/python.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from .base import BaseInstaller
|
||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
|
||||||
|
|
||||||
|
class PythonInstaller(BaseInstaller):
|
||||||
|
"""
|
||||||
|
Install Python projects based on pyproject.toml and/or requirements.txt.
|
||||||
|
|
||||||
|
Strategy:
|
||||||
|
- Determine a pip command in this order:
|
||||||
|
1. $PKGMGR_PIP (explicit override, e.g. ~/.venvs/pkgmgr/bin/pip)
|
||||||
|
2. sys.executable -m pip (current interpreter)
|
||||||
|
3. "pip" from PATH as last resort
|
||||||
|
- If pyproject.toml exists: pip install .
|
||||||
|
- If requirements.txt exists: pip install -r requirements.txt
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "python"
|
||||||
|
|
||||||
|
def supports(self, ctx) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if this installer should handle the given repository.
|
||||||
|
|
||||||
|
ctx must provide:
|
||||||
|
- repo_dir: filesystem path to the repository
|
||||||
|
"""
|
||||||
|
repo_dir = ctx.repo_dir
|
||||||
|
return (
|
||||||
|
os.path.exists(os.path.join(repo_dir, "pyproject.toml"))
|
||||||
|
or os.path.exists(os.path.join(repo_dir, "requirements.txt"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _pip_cmd(self) -> str:
|
||||||
|
"""
|
||||||
|
Resolve the pip command to use.
|
||||||
|
"""
|
||||||
|
# 1) Explicit override via environment variable
|
||||||
|
explicit = os.environ.get("PKGMGR_PIP", "").strip()
|
||||||
|
if explicit:
|
||||||
|
return explicit
|
||||||
|
|
||||||
|
# 2) Current Python interpreter (works well in Nix/dev shells)
|
||||||
|
if sys.executable:
|
||||||
|
return f"{sys.executable} -m pip"
|
||||||
|
|
||||||
|
# 3) Fallback to plain pip
|
||||||
|
return "pip"
|
||||||
|
|
||||||
|
def run(self, ctx) -> None:
|
||||||
|
"""
|
||||||
|
ctx must provide:
|
||||||
|
- repo_dir: path to repository
|
||||||
|
- identifier: human readable name
|
||||||
|
- preview: bool
|
||||||
|
"""
|
||||||
|
pip_cmd = self._pip_cmd()
|
||||||
|
|
||||||
|
pyproject = os.path.join(ctx.repo_dir, "pyproject.toml")
|
||||||
|
if os.path.exists(pyproject):
|
||||||
|
print(
|
||||||
|
f"pyproject.toml found in {ctx.identifier}, "
|
||||||
|
f"installing Python project..."
|
||||||
|
)
|
||||||
|
cmd = f"{pip_cmd} install ."
|
||||||
|
try:
|
||||||
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
except SystemExit as exc:
|
||||||
|
print(
|
||||||
|
f"[Warning] Failed to install Python project in {ctx.identifier}: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
req_txt = os.path.join(ctx.repo_dir, "requirements.txt")
|
||||||
|
if os.path.exists(req_txt):
|
||||||
|
print(
|
||||||
|
f"requirements.txt found in {ctx.identifier}, "
|
||||||
|
f"installing Python dependencies..."
|
||||||
|
)
|
||||||
|
cmd = f"{pip_cmd} install -r requirements.txt"
|
||||||
|
try:
|
||||||
|
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
|
||||||
|
except SystemExit as exc:
|
||||||
|
print(
|
||||||
|
f"[Warning] Failed to install Python dependencies in {ctx.identifier}: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
@@ -1,4 +1,3 @@
|
|||||||
# tests/test_integration_install_all_shallow.py
|
|
||||||
"""
|
"""
|
||||||
Integration test: install all configured repositories using
|
Integration test: install all configured repositories using
|
||||||
--clone-mode shallow (HTTPS shallow clone) and --no-verification.
|
--clone-mode shallow (HTTPS shallow clone) and --no-verification.
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
# tests/test_install_repos.py
|
|
||||||
import os
|
|
||||||
import unittest
|
|
||||||
from unittest.mock import patch, MagicMock, mock_open
|
|
||||||
|
|
||||||
from pkgmgr.install_repos import install_repos
|
|
||||||
|
|
||||||
|
|
||||||
class TestInstallRepos(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.repo = {
|
|
||||||
"provider": "github.com",
|
|
||||||
"account": "user",
|
|
||||||
"repository": "repo",
|
|
||||||
}
|
|
||||||
self.selected = [self.repo]
|
|
||||||
self.base_dir = "/tmp/repos"
|
|
||||||
self.bin_dir = "/tmp/bin"
|
|
||||||
self.all_repos = self.selected
|
|
||||||
|
|
||||||
@patch("pkgmgr.install_repos.clone_repos")
|
|
||||||
@patch("pkgmgr.install_repos.os.path.exists")
|
|
||||||
@patch("pkgmgr.install_repos.get_repo_dir")
|
|
||||||
@patch("pkgmgr.install_repos.get_repo_identifier")
|
|
||||||
def test_calls_clone_repos_with_clone_mode(
|
|
||||||
self,
|
|
||||||
mock_get_repo_identifier,
|
|
||||||
mock_get_repo_dir,
|
|
||||||
mock_exists,
|
|
||||||
mock_clone_repos,
|
|
||||||
):
|
|
||||||
mock_get_repo_identifier.return_value = "github.com/user/repo"
|
|
||||||
mock_get_repo_dir.return_value = "/tmp/repos/user/repo"
|
|
||||||
# Repo-Verzeichnis existiert nicht -> soll geklont werden
|
|
||||||
mock_exists.return_value = False
|
|
||||||
|
|
||||||
install_repos(
|
|
||||||
self.selected,
|
|
||||||
self.base_dir,
|
|
||||||
self.bin_dir,
|
|
||||||
self.all_repos,
|
|
||||||
no_verification=True,
|
|
||||||
preview=False,
|
|
||||||
quiet=True,
|
|
||||||
clone_mode="shallow",
|
|
||||||
update_dependencies=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_clone_repos.assert_called_once()
|
|
||||||
args, kwargs = mock_clone_repos.call_args
|
|
||||||
# clone_mode ist letztes Argument
|
|
||||||
self.assertEqual(args[-1], "shallow")
|
|
||||||
|
|
||||||
@patch("pkgmgr.install_repos.run_command")
|
|
||||||
@patch("pkgmgr.install_repos.open", new_callable=mock_open, create=True)
|
|
||||||
@patch("pkgmgr.install_repos.yaml.safe_load")
|
|
||||||
@patch("pkgmgr.install_repos.os.path.exists")
|
|
||||||
@patch("pkgmgr.install_repos.create_ink")
|
|
||||||
@patch("pkgmgr.install_repos.verify_repository")
|
|
||||||
@patch("pkgmgr.install_repos.get_repo_dir")
|
|
||||||
@patch("pkgmgr.install_repos.get_repo_identifier")
|
|
||||||
def test_pkgmgr_requirements_propagate_clone_mode(
|
|
||||||
self,
|
|
||||||
mock_get_repo_identifier,
|
|
||||||
mock_get_repo_dir,
|
|
||||||
mock_verify,
|
|
||||||
mock_create_ink,
|
|
||||||
mock_exists,
|
|
||||||
mock_safe_load,
|
|
||||||
mock_open_file,
|
|
||||||
mock_run_command,
|
|
||||||
):
|
|
||||||
mock_get_repo_identifier.return_value = "github.com/user/repo"
|
|
||||||
repo_dir = "/tmp/repos/user/repo"
|
|
||||||
mock_get_repo_dir.return_value = repo_dir
|
|
||||||
|
|
||||||
# exists() muss True für repo_dir & requirements.yml liefern,
|
|
||||||
# sonst werden die Anforderungen nie verarbeitet.
|
|
||||||
def exists_side_effect(path):
|
|
||||||
if path == repo_dir:
|
|
||||||
return True
|
|
||||||
if path == os.path.join(repo_dir, "requirements.yml"):
|
|
||||||
return True
|
|
||||||
# requirements.txt und Makefile sollen "nicht existieren"
|
|
||||||
return False
|
|
||||||
|
|
||||||
mock_exists.side_effect = exists_side_effect
|
|
||||||
|
|
||||||
mock_verify.return_value = (True, [], "hash", "key")
|
|
||||||
|
|
||||||
# requirements.yml enthält pkgmgr-Dependencies
|
|
||||||
mock_safe_load.return_value = {
|
|
||||||
"pkgmgr": ["github.com/other/account/dep"],
|
|
||||||
}
|
|
||||||
|
|
||||||
commands = []
|
|
||||||
|
|
||||||
def run_command_side_effect(cmd, cwd=None, preview=False):
|
|
||||||
commands.append((cmd, cwd, preview))
|
|
||||||
|
|
||||||
mock_run_command.side_effect = run_command_side_effect
|
|
||||||
|
|
||||||
install_repos(
|
|
||||||
self.selected,
|
|
||||||
self.base_dir,
|
|
||||||
self.bin_dir,
|
|
||||||
self.all_repos,
|
|
||||||
no_verification=False,
|
|
||||||
preview=False,
|
|
||||||
quiet=True,
|
|
||||||
clone_mode="shallow",
|
|
||||||
update_dependencies=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Prüfen, dass ein pkgmgr install Befehl mit --clone-mode shallow gebaut wurde
|
|
||||||
pkgmgr_install_cmds = [
|
|
||||||
c for (c, cwd, preview) in commands if "pkgmgr install" in c
|
|
||||||
]
|
|
||||||
self.assertTrue(
|
|
||||||
pkgmgr_install_cmds,
|
|
||||||
f"No pkgmgr install command was executed. Commands seen: {commands}",
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd = pkgmgr_install_cmds[0]
|
|
||||||
self.assertIn("--clone-mode shallow", cmd)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# tests/test_main.py
|
|
||||||
import unittest
|
|
||||||
import main
|
|
||||||
|
|
||||||
|
|
||||||
class TestMainModule(unittest.TestCase):
|
|
||||||
def test_proxy_commands_defined(self):
|
|
||||||
"""
|
|
||||||
Basic sanity check: main.py should define PROXY_COMMANDS
|
|
||||||
with git/docker/docker compose entries.
|
|
||||||
"""
|
|
||||||
self.assertTrue(hasattr(main, "PROXY_COMMANDS"))
|
|
||||||
self.assertIn("git", main.PROXY_COMMANDS)
|
|
||||||
self.assertIn("docker", main.PROXY_COMMANDS)
|
|
||||||
self.assertIn("docker compose", main.PROXY_COMMANDS)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
0
tests/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/pkgmgr/__init__.py
Normal file
0
tests/unit/pkgmgr/__init__.py
Normal file
0
tests/unit/pkgmgr/installers/__init__.py
Normal file
0
tests/unit/pkgmgr/installers/__init__.py
Normal file
71
tests/unit/pkgmgr/installers/test_ansible_requirements.py
Normal file
71
tests/unit/pkgmgr/installers/test_ansible_requirements.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_ansible_requirements.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, mock_open
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.ansible_requirements import AnsibleRequirementsInstaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestAnsibleRequirementsInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
self.installer = AnsibleRequirementsInstaller()
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_supports_true_when_requirements_exist(self, mock_exists):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "requirements.yml"))
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
def test_supports_false_when_requirements_missing(self, mock_exists):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.ansible_requirements.run_command")
|
||||||
|
@patch("tempfile.NamedTemporaryFile")
|
||||||
|
@patch(
|
||||||
|
"builtins.open",
|
||||||
|
new_callable=mock_open,
|
||||||
|
read_data="""
|
||||||
|
collections:
|
||||||
|
- name: community.docker
|
||||||
|
roles:
|
||||||
|
- src: geerlingguy.docker
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_run_installs_collections_and_roles(
|
||||||
|
self, mock_exists, mock_file, mock_tmp, mock_run_command
|
||||||
|
):
|
||||||
|
# Fake temp file name
|
||||||
|
mock_tmp().__enter__().name = "/tmp/req.yml"
|
||||||
|
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
|
||||||
|
cmds = [call[0][0] for call in mock_run_command.call_args_list]
|
||||||
|
self.assertIn(
|
||||||
|
"ansible-galaxy collection install -r /tmp/req.yml",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"ansible-galaxy role install -r /tmp/req.yml",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
97
tests/unit/pkgmgr/installers/test_aur.py
Normal file
97
tests/unit/pkgmgr/installers/test_aur.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_aur.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, mock_open
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.aur import AurInstaller, AUR_CONFIG_FILENAME
|
||||||
|
|
||||||
|
|
||||||
|
class TestAurInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
self.installer = AurInstaller()
|
||||||
|
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/pacman")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
@patch(
|
||||||
|
"builtins.open",
|
||||||
|
new_callable=mock_open,
|
||||||
|
read_data="""
|
||||||
|
helper: yay
|
||||||
|
packages:
|
||||||
|
- aurutils
|
||||||
|
- name: some-aur-only-tool
|
||||||
|
reason: "Test tool"
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
def test_supports_true_when_arch_and_aur_config_present(
|
||||||
|
self, mock_file, mock_exists, mock_which
|
||||||
|
):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
mock_which.assert_called_with("pacman")
|
||||||
|
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, AUR_CONFIG_FILENAME))
|
||||||
|
|
||||||
|
@patch("shutil.which", return_value=None)
|
||||||
|
def test_supports_false_when_not_arch(self, mock_which):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/pacman")
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
def test_supports_false_when_no_config(self, mock_exists, mock_which):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("shutil.which", side_effect=lambda name: "/usr/bin/pacman" if name == "pacman" else "/usr/bin/yay")
|
||||||
|
@patch("pkgmgr.installers.aur.run_command")
|
||||||
|
@patch(
|
||||||
|
"builtins.open",
|
||||||
|
new_callable=mock_open,
|
||||||
|
read_data="""
|
||||||
|
helper: yay
|
||||||
|
packages:
|
||||||
|
- aurutils
|
||||||
|
- some-aur-only-tool
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_run_installs_packages_with_helper(
|
||||||
|
self, mock_exists, mock_file, mock_run_command, mock_which
|
||||||
|
):
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
|
||||||
|
cmd = mock_run_command.call_args[0][0]
|
||||||
|
self.assertTrue(cmd.startswith("yay -S --noconfirm "))
|
||||||
|
self.assertIn("aurutils", cmd)
|
||||||
|
self.assertIn("some-aur-only-tool", cmd)
|
||||||
|
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/pacman")
|
||||||
|
@patch(
|
||||||
|
"builtins.open",
|
||||||
|
new_callable=mock_open,
|
||||||
|
read_data="packages: []",
|
||||||
|
)
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_run_skips_when_no_packages(
|
||||||
|
self, mock_exists, mock_file, mock_which
|
||||||
|
):
|
||||||
|
with patch("pkgmgr.installers.aur.run_command") as mock_run_command:
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
mock_run_command.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
43
tests/unit/pkgmgr/installers/test_base.py
Normal file
43
tests/unit/pkgmgr/installers/test_base.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_base.py
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from pkgmgr.installers.base import BaseInstaller
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
|
||||||
|
|
||||||
|
class DummyInstaller(BaseInstaller):
|
||||||
|
def __init__(self, supports_value: bool = True):
|
||||||
|
self._supports_value = supports_value
|
||||||
|
self.ran_with = None
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
return self._supports_value
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
self.ran_with = ctx
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseInstaller(unittest.TestCase):
|
||||||
|
def test_dummy_installer_supports_and_run(self):
|
||||||
|
ctx = RepoContext(
|
||||||
|
repo={},
|
||||||
|
identifier="id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
inst = DummyInstaller(supports_value=True)
|
||||||
|
self.assertTrue(inst.supports(ctx))
|
||||||
|
self.assertIsNone(inst.ran_with)
|
||||||
|
inst.run(ctx)
|
||||||
|
self.assertIs(inst.ran_with, ctx)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
51
tests/unit/pkgmgr/installers/test_makefile_installer.py
Normal file
51
tests/unit/pkgmgr/installers/test_makefile_installer.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_makefile_installer.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.makefile import MakefileInstaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestMakefileInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
self.installer = MakefileInstaller()
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_supports_true_when_makefile_exists(self, mock_exists):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "Makefile"))
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
def test_supports_false_when_makefile_missing(self, mock_exists):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.makefile.run_command")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_run_executes_make_install(self, mock_exists, mock_run_command):
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
cmd = mock_run_command.call_args[0][0]
|
||||||
|
self.assertEqual(cmd, "make install")
|
||||||
|
self.assertEqual(
|
||||||
|
mock_run_command.call_args[1].get("cwd"),
|
||||||
|
self.ctx.repo_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
64
tests/unit/pkgmgr/installers/test_nix_flake.py
Normal file
64
tests/unit/pkgmgr/installers/test_nix_flake.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.nix_flake import NixFlakeInstaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestNixFlakeInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
self.installer = NixFlakeInstaller()
|
||||||
|
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/nix")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_supports_true_when_nix_and_flake_exist(self, mock_exists, mock_which):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
mock_which.assert_called_with("nix")
|
||||||
|
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "flake.nix"))
|
||||||
|
|
||||||
|
@patch("shutil.which", return_value=None)
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_supports_false_when_nix_missing(self, mock_exists, mock_which):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/nix")
|
||||||
|
@mock.patch("pkgmgr.installers.nix_flake.run_command")
|
||||||
|
def test_run_tries_pkgmgr_then_default(self, mock_run_command, mock_which, mock_exists):
|
||||||
|
cmds = []
|
||||||
|
|
||||||
|
def side_effect(cmd, cwd=None, preview=False, *args, **kwargs):
|
||||||
|
cmds.append(cmd)
|
||||||
|
return None
|
||||||
|
|
||||||
|
mock_run_command.side_effect = side_effect
|
||||||
|
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
f"nix profile install {self.ctx.repo_dir}#pkgmgr",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
f"nix profile install {self.ctx.repo_dir}#default",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
65
tests/unit/pkgmgr/installers/test_pkgbuild.py
Normal file
65
tests/unit/pkgmgr/installers/test_pkgbuild.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_pkgbuild.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.pkgbuild import PkgbuildInstaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestPkgbuildInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
self.installer = PkgbuildInstaller()
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/pacman")
|
||||||
|
def test_supports_true_when_pacman_and_pkgbuild_exist(self, mock_which, mock_exists):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
mock_which.assert_called_with("pacman")
|
||||||
|
mock_exists.assert_called_with(os.path.join(self.ctx.repo_dir, "PKGBUILD"))
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/pacman")
|
||||||
|
def test_supports_false_when_pkgbuild_missing(self, mock_which, mock_exists):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.pkgbuild.run_command")
|
||||||
|
@patch("subprocess.check_output", return_value="python\ngit\n")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
@patch("shutil.which", return_value="/usr/bin/pacman")
|
||||||
|
def test_run_installs_all_packages_and_uses_clean_bash(
|
||||||
|
self, mock_which, mock_exists, mock_check_output, mock_run_command
|
||||||
|
):
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
|
||||||
|
# Check subprocess.check_output arguments (clean shell)
|
||||||
|
args, kwargs = mock_check_output.call_args
|
||||||
|
cmd_list = args[0]
|
||||||
|
self.assertEqual(cmd_list[0], "bash")
|
||||||
|
self.assertIn("--noprofile", cmd_list)
|
||||||
|
self.assertIn("--norc", cmd_list)
|
||||||
|
|
||||||
|
# Check that pacman is called with the extracted packages
|
||||||
|
cmd = mock_run_command.call_args[0][0]
|
||||||
|
self.assertTrue(cmd.startswith("sudo pacman -S --noconfirm "))
|
||||||
|
self.assertIn("python", cmd)
|
||||||
|
self.assertIn("git", cmd)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
87
tests/unit/pkgmgr/installers/test_pkgmgr_manifest.py
Normal file
87
tests/unit/pkgmgr/installers/test_pkgmgr_manifest.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_pkgmgr_manifest.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, mock_open
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.pkgmgr_manifest import PkgmgrManifestInstaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestPkgmgrManifestInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=True,
|
||||||
|
)
|
||||||
|
self.installer = PkgmgrManifestInstaller()
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_supports_true_when_manifest_exists(self, mock_exists):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
manifest_path = os.path.join(self.ctx.repo_dir, "pkgmgr.yml")
|
||||||
|
mock_exists.assert_called_with(manifest_path)
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
def test_supports_false_when_manifest_missing(self, mock_exists):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.pkgmgr_manifest.run_command")
|
||||||
|
@patch("builtins.open", new_callable=mock_open, read_data="""
|
||||||
|
version: 1
|
||||||
|
author: "Kevin"
|
||||||
|
url: "https://example.com"
|
||||||
|
description: "Test repo"
|
||||||
|
dependencies:
|
||||||
|
- repository: github:user/repo1
|
||||||
|
version: main
|
||||||
|
reason: "Core dependency"
|
||||||
|
- repository: github:user/repo2
|
||||||
|
""")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_run_installs_dependencies_and_pulls_when_update_enabled(
|
||||||
|
self, mock_exists, mock_file, mock_run_command
|
||||||
|
):
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
|
||||||
|
# First call: pkgmgr pull github:user/repo1 github:user/repo2
|
||||||
|
# Then calls to pkgmgr install ...
|
||||||
|
cmds = [call_args[0][0] for call_args in mock_run_command.call_args_list]
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"pkgmgr pull github:user/repo1 github:user/repo2",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"pkgmgr install github:user/repo1 --version main --dependencies --clone-mode ssh",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
# For repo2: no version but dependencies + clone_mode
|
||||||
|
self.assertIn(
|
||||||
|
"pkgmgr install github:user/repo2 --dependencies --clone-mode ssh",
|
||||||
|
cmds,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.pkgmgr_manifest.run_command")
|
||||||
|
@patch("builtins.open", new_callable=mock_open, read_data="{}")
|
||||||
|
@patch("os.path.exists", return_value=True)
|
||||||
|
def test_run_no_dependencies_no_command_called(
|
||||||
|
self, mock_exists, mock_file, mock_run_command
|
||||||
|
):
|
||||||
|
self.ctx.update_dependencies = True
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
mock_run_command.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
71
tests/unit/pkgmgr/installers/test_python_installer.py
Normal file
71
tests/unit/pkgmgr/installers/test_python_installer.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# tests/unit/pkgmgr/installers/test_python_installer.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
from pkgmgr.installers.python import PythonInstaller
|
||||||
|
|
||||||
|
|
||||||
|
class TestPythonInstaller(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.repo = {"name": "test-repo"}
|
||||||
|
self.ctx = RepoContext(
|
||||||
|
repo=self.repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/repo",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=[self.repo],
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
self.installer = PythonInstaller()
|
||||||
|
|
||||||
|
@patch("os.path.exists", side_effect=lambda path: path.endswith("pyproject.toml"))
|
||||||
|
def test_supports_true_when_pyproject_exists(self, mock_exists):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("os.path.exists", side_effect=lambda path: path.endswith("requirements.txt"))
|
||||||
|
def test_supports_true_when_requirements_exists(self, mock_exists):
|
||||||
|
self.assertTrue(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
def test_supports_false_when_no_python_files(self, mock_exists):
|
||||||
|
self.assertFalse(self.installer.supports(self.ctx))
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.python.run_command")
|
||||||
|
@patch(
|
||||||
|
"os.path.exists",
|
||||||
|
side_effect=lambda path: path.endswith("pyproject.toml")
|
||||||
|
)
|
||||||
|
def test_run_installs_project_from_pyproject(self, mock_exists, mock_run_command):
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
cmd = mock_run_command.call_args[0][0]
|
||||||
|
self.assertIn("pip install .", cmd)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_run_command.call_args[1].get("cwd"),
|
||||||
|
self.ctx.repo_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("pkgmgr.installers.python.run_command")
|
||||||
|
@patch(
|
||||||
|
"os.path.exists",
|
||||||
|
side_effect=lambda path: path.endswith("requirements.txt")
|
||||||
|
)
|
||||||
|
def test_run_installs_dependencies_from_requirements(self, mock_exists, mock_run_command):
|
||||||
|
self.installer.run(self.ctx)
|
||||||
|
cmd = mock_run_command.call_args[0][0]
|
||||||
|
self.assertIn("pip install -r requirements.txt", cmd)
|
||||||
|
self.assertEqual(
|
||||||
|
mock_run_command.call_args[1].get("cwd"),
|
||||||
|
self.ctx.repo_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
36
tests/unit/pkgmgr/test_context.py
Normal file
36
tests/unit/pkgmgr/test_context.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import unittest
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepoContext(unittest.TestCase):
|
||||||
|
def test_repo_context_fields_are_stored(self):
|
||||||
|
repo = {"name": "test-repo"}
|
||||||
|
ctx = RepoContext(
|
||||||
|
repo=repo,
|
||||||
|
identifier="test-id",
|
||||||
|
repo_dir="/tmp/test",
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/usr/local/bin",
|
||||||
|
all_repos=[repo],
|
||||||
|
no_verification=True,
|
||||||
|
preview=False,
|
||||||
|
quiet=True,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(ctx.repo, repo)
|
||||||
|
self.assertEqual(ctx.identifier, "test-id")
|
||||||
|
self.assertEqual(ctx.repo_dir, "/tmp/test")
|
||||||
|
self.assertEqual(ctx.repositories_base_dir, "/tmp")
|
||||||
|
self.assertEqual(ctx.bin_dir, "/usr/local/bin")
|
||||||
|
self.assertEqual(ctx.all_repos, [repo])
|
||||||
|
self.assertTrue(ctx.no_verification)
|
||||||
|
self.assertFalse(ctx.preview)
|
||||||
|
self.assertTrue(ctx.quiet)
|
||||||
|
self.assertEqual(ctx.clone_mode, "ssh")
|
||||||
|
self.assertTrue(ctx.update_dependencies)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
122
tests/unit/pkgmgr/test_install_repos.py
Normal file
122
tests/unit/pkgmgr/test_install_repos.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from pkgmgr.run_command import run_command
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
from pkgmgr.context import RepoContext
|
||||||
|
import pkgmgr.install_repos as install_module
|
||||||
|
|
||||||
|
|
||||||
|
class DummyInstaller:
|
||||||
|
"""Simple installer for testing orchestration."""
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def supports(self, ctx: RepoContext) -> bool:
|
||||||
|
# Always support to verify that the pipeline runs
|
||||||
|
return True
|
||||||
|
|
||||||
|
def run(self, ctx: RepoContext) -> None:
|
||||||
|
self.calls.append(ctx.identifier)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstallReposOrchestration(unittest.TestCase):
|
||||||
|
@patch("pkgmgr.install_repos.create_ink")
|
||||||
|
@patch("pkgmgr.install_repos.verify_repository")
|
||||||
|
@patch("pkgmgr.install_repos.get_repo_dir")
|
||||||
|
@patch("pkgmgr.install_repos.get_repo_identifier")
|
||||||
|
@patch("pkgmgr.install_repos.clone_repos")
|
||||||
|
def test_install_repos_runs_pipeline_for_each_repo(
|
||||||
|
self,
|
||||||
|
mock_clone_repos,
|
||||||
|
mock_get_repo_identifier,
|
||||||
|
mock_get_repo_dir,
|
||||||
|
mock_verify_repository,
|
||||||
|
mock_create_ink,
|
||||||
|
):
|
||||||
|
repo1 = {"name": "repo1"}
|
||||||
|
repo2 = {"name": "repo2"}
|
||||||
|
selected_repos = [repo1, repo2]
|
||||||
|
all_repos = selected_repos
|
||||||
|
|
||||||
|
# Return identifiers and directories
|
||||||
|
mock_get_repo_identifier.side_effect = ["id1", "id2"]
|
||||||
|
mock_get_repo_dir.side_effect = ["/tmp/repo1", "/tmp/repo2"]
|
||||||
|
|
||||||
|
# Simulate verification success: (ok, errors, commit, key)
|
||||||
|
mock_verify_repository.return_value = (True, [], "commit", "key")
|
||||||
|
|
||||||
|
# Ensure directories exist (no cloning)
|
||||||
|
with patch("os.path.exists", return_value=True):
|
||||||
|
dummy_installer = DummyInstaller()
|
||||||
|
# Monkeypatch INSTALLERS for this test
|
||||||
|
old_installers = install_module.INSTALLERS
|
||||||
|
install_module.INSTALLERS = [dummy_installer]
|
||||||
|
try:
|
||||||
|
install_module.install_repos(
|
||||||
|
selected_repos=selected_repos,
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=all_repos,
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
install_module.INSTALLERS = old_installers
|
||||||
|
|
||||||
|
# Check that installers ran with both identifiers
|
||||||
|
self.assertEqual(dummy_installer.calls, ["id1", "id2"])
|
||||||
|
self.assertEqual(mock_create_ink.call_count, 2)
|
||||||
|
self.assertEqual(mock_verify_repository.call_count, 2)
|
||||||
|
|
||||||
|
@patch("pkgmgr.install_repos.verify_repository")
|
||||||
|
@patch("pkgmgr.install_repos.get_repo_dir")
|
||||||
|
@patch("pkgmgr.install_repos.get_repo_identifier")
|
||||||
|
@patch("pkgmgr.install_repos.clone_repos")
|
||||||
|
def test_install_repos_skips_on_failed_verification(
|
||||||
|
self,
|
||||||
|
mock_clone_repos,
|
||||||
|
mock_get_repo_identifier,
|
||||||
|
mock_get_repo_dir,
|
||||||
|
mock_verify_repository,
|
||||||
|
):
|
||||||
|
repo = {"name": "repo1", "verified": True}
|
||||||
|
selected_repos = [repo]
|
||||||
|
all_repos = selected_repos
|
||||||
|
|
||||||
|
mock_get_repo_identifier.return_value = "id1"
|
||||||
|
mock_get_repo_dir.return_value = "/tmp/repo1"
|
||||||
|
|
||||||
|
# Verification fails: ok=False, with error list
|
||||||
|
mock_verify_repository.return_value = (False, ["sig error"], None, None)
|
||||||
|
|
||||||
|
dummy_installer = DummyInstaller()
|
||||||
|
with patch("os.path.exists", return_value=True), \
|
||||||
|
patch("pkgmgr.install_repos.create_ink") as mock_create_ink, \
|
||||||
|
patch("builtins.input", return_value="n"):
|
||||||
|
old_installers = install_module.INSTALLERS
|
||||||
|
install_module.INSTALLERS = [dummy_installer]
|
||||||
|
try:
|
||||||
|
install_module.install_repos(
|
||||||
|
selected_repos=selected_repos,
|
||||||
|
repositories_base_dir="/tmp",
|
||||||
|
bin_dir="/bin",
|
||||||
|
all_repos=all_repos,
|
||||||
|
no_verification=False,
|
||||||
|
preview=False,
|
||||||
|
quiet=False,
|
||||||
|
clone_mode="ssh",
|
||||||
|
update_dependencies=False,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
install_module.INSTALLERS = old_installers
|
||||||
|
|
||||||
|
# No installer run and no create_ink when user declines
|
||||||
|
self.assertEqual(dummy_installer.calls, [])
|
||||||
|
mock_create_ink.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user