Compare commits

..

8 Commits

Author SHA1 Message Date
Kevin Veen-Birkenbach
103f49c8f6 Release version 1.4.1
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-12 23:06:15 +01:00
Kevin Veen-Birkenbach
f5d428950e **Replace main.py with module-based entry point and unify CLI execution**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Remove legacy *main.py* and introduce *pkgmgr* module entry via *python -m pkgmgr*
* Add ***main**.py* as the canonical entry point delegating to the CLI
* Export *PYTHONPATH=src* in Makefile to ensure reliable imports in dev and CI
* Update setup scripts (venv & nix) to use module execution
* Refactor all E2E tests to execute the real module entry instead of file paths

This aligns pkgmgr with standard Python packaging practices and simplifies testing, setup, and execution across environments.

https://chatgpt.com/share/693c9056-716c-800f-b583-fc9245eab2b4
2025-12-12 22:59:46 +01:00
Kevin Veen-Birkenbach
b40787ffc5 ci: publish GHCR images after successful mark-stable workflow
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Trigger container publishing via workflow_run on "Mark stable commit", gate on success,
checkout the workflow_run head SHA, force-refresh tags, and derive version from the v* tag
pointing at the tested commit to correctly detect and publish stable images.

https://chatgpt.com/share/693c836b-0b00-800f-9536-9e273abd0fb5
2025-12-12 22:50:33 +01:00
Kevin Veen-Birkenbach
0482a7f88d Release version 1.4.0
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Publish container images (GHCR) / publish (push) Has been cancelled
2025-12-12 22:20:07 +01:00
Kevin Veen-Birkenbach
8c127cc45a ci: fix container publish workflow to run on version tag pushes
Switch publish-containers workflow from workflow_run to direct v* tag triggers,
remove obsolete workflow_run logic, simplify version detection via GITHUB_REF_NAME,
and keep stable-tag detection aligned with the stable ref.

https://chatgpt.com/share/693c836b-0b00-800f-9536-9e273abd0fb5
2025-12-12 22:17:32 +01:00
Kevin Veen-Birkenbach
2761e829cb ci: add GHCR container publish pipeline with semantic tags
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
Introduce a dedicated publish-containers workflow triggered after stable releases.
Unify container build and publish logic via scripts, add buildx-based multi-tag publishing,
default base image resolution, and Arch alias tags for latest/version/stable.

https://chatgpt.com/share/693c836b-0b00-800f-9536-9e273abd0fb5
2025-12-12 22:04:39 +01:00
Kevin Veen-Birkenbach
d0c01b6955 Updated dependencies instructions
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
2025-12-12 21:37:50 +01:00
Kevin Veen-Birkenbach
b2421c9b84 **Refactor OS detection and normalize Manjaro to Arch**
Some checks failed
Mark stable commit / test-unit (push) Has been cancelled
Mark stable commit / test-integration (push) Has been cancelled
Mark stable commit / test-env-virtual (push) Has been cancelled
Mark stable commit / test-env-nix (push) Has been cancelled
Mark stable commit / test-e2e (push) Has been cancelled
Mark stable commit / test-virgin-user (push) Has been cancelled
Mark stable commit / test-virgin-root (push) Has been cancelled
Mark stable commit / mark-stable (push) Has been cancelled
* Centralize OS detection and normalization in a dedicated resolver module
* Treat Manjaro consistently as Arch across dependencies and package install
* Remove duplicated OS logic and legacy lib.sh
* Rename installation entrypoint to init.sh and update Makefile accordingly

https://chatgpt.com/share/693c7b50-3be0-800f-8aeb-daf3ee929ea3
2025-12-12 21:30:03 +01:00
39 changed files with 433 additions and 139 deletions

View File

@@ -0,0 +1,66 @@
name: Publish container images (GHCR)
on:
workflow_run:
workflows: ["Mark stable commit"]
types: [completed]
jobs:
publish:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository (with tags)
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Checkout workflow_run commit and refresh tags
run: |
set -euo pipefail
git checkout -f "${{ github.event.workflow_run.head_sha }}"
git fetch --tags --force
git tag --list 'stable' 'v*' --sort=version:refname | tail -n 20
- name: Compute version and stable flag
id: info
run: |
set -euo pipefail
SHA="$(git rev-parse HEAD)"
V_TAG="$(git tag --points-at "${SHA}" --list 'v*' | sort -V | tail -n1)"
[[ -n "$V_TAG" ]] || { echo "No version tag found"; exit 1; }
VERSION="${V_TAG#v}"
STABLE_SHA="$(git rev-parse -q --verify refs/tags/stable^{commit} 2>/dev/null || true)"
IS_STABLE=false
[[ -n "${STABLE_SHA}" && "${STABLE_SHA}" == "${SHA}" ]] && IS_STABLE=true
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "is_stable=${IS_STABLE}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
use: true
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Publish all images
run: |
set -euo pipefail
OWNER="${{ github.repository_owner }}" \
VERSION="${{ steps.info.outputs.version }}" \
IS_STABLE="${{ steps.info.outputs.is_stable }}" \
bash scripts/build/publish.sh

View File

@@ -1,3 +1,17 @@
## [1.4.1] - 2025-12-12
* Fixed (#1) stable release container publishing
## [1.4.0] - 2025-12-12
* **Docker Container Building**
* New official container images are automatically published on each release.
* Images are available per distribution and as a default Arch-based image.
* Stable releases now provide an additional `stable` container tag.
## [1.3.1] - 2025-12-12 ## [1.3.1] - 2025-12-12
* Updated documentation with better run and installation instructions * Updated documentation with better run and installation instructions

View File

@@ -30,13 +30,14 @@ export BASE_IMAGE_CENTOS
# PYthon Unittest Pattern # PYthon Unittest Pattern
TEST_PATTERN := test_*.py TEST_PATTERN := test_*.py
export TEST_PATTERN export TEST_PATTERN
export PYTHONPATH := src
# ------------------------------------------------------------ # ------------------------------------------------------------
# System install # System install
# ------------------------------------------------------------ # ------------------------------------------------------------
install: install:
@echo "Building and installing distro-native package-manager for this system..." @echo "Building and installing distro-native package-manager for this system..."
@bash scripts/installation/main.sh @bash scripts/installation/init.sh
# ------------------------------------------------------------ # ------------------------------------------------------------
# PKGMGR setup # PKGMGR setup
@@ -45,7 +46,7 @@ install:
# Default: keep current auto-detection behavior # Default: keep current auto-detection behavior
setup: setup-nix setup-venv setup: setup-nix setup-venv
# Explicit: developer setup (Python venv + shell RC + main.py install) # Explicit: developer setup (Python venv + shell RC + install)
setup-venv: setup-nix setup-venv: setup-nix
@bash scripts/setup/venv.sh @bash scripts/setup/venv.sh

View File

@@ -116,6 +116,19 @@ README-ready, ohne Over-Engineering.
PKGMGR can be installed using `make`. PKGMGR can be installed using `make`.
The setup mode defines **which runtime layers are prepared**. The setup mode defines **which runtime layers are prepared**.
---
### Dependency installation (optional)
System dependencies required **before running any *make* commands** are installed via:
```
scripts/installation/dependencies.sh
```
The script detects and normalizes the OS and installs the required **system-level dependencies** accordingly.
--- ---
### Setup modes ### Setup modes

View File

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

14
main.py
View File

@@ -1,14 +0,0 @@
#!/usr/bin/env python3
import sys
from pathlib import Path
# Ensure local src/ overrides installed package
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "src"
if SRC.is_dir():
sys.path.insert(0, str(SRC))
from pkgmgr.cli import main
if __name__ == "__main__":
main()

View File

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

View File

@@ -1,18 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
: "${BASE_IMAGE_ARCH:=archlinux:latest}"
: "${BASE_IMAGE_DEBIAN:=debian:stable-slim}"
: "${BASE_IMAGE_UBUNTU:=ubuntu:latest}"
: "${BASE_IMAGE_FEDORA:=fedora:latest}"
: "${BASE_IMAGE_CENTOS:=quay.io/centos/centos:stream9}"
resolve_base_image() { resolve_base_image() {
local distro="$1" local distro="$1"
case "$distro" in case "$distro" in
arch) echo "$BASE_IMAGE_ARCH" ;; arch) echo "$BASE_IMAGE_ARCH" ;;
debian) echo "$BASE_IMAGE_DEBIAN" ;; debian) echo "$BASE_IMAGE_DEBIAN" ;;
ubuntu) echo "$BASE_IMAGE_UBUNTU" ;; ubuntu) echo "$BASE_IMAGE_UBUNTU" ;;
fedora) echo "$BASE_IMAGE_FEDORA" ;; fedora) echo "$BASE_IMAGE_FEDORA" ;;
centos) echo "$BASE_IMAGE_CENTOS" ;; centos) echo "$BASE_IMAGE_CENTOS" ;;
*) *) echo "ERROR: Unknown distro '$distro'" >&2; exit 1 ;;
echo "ERROR: Unknown distro '$distro'" >&2
exit 1
;;
esac esac
} }

View File

@@ -1,28 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Unified docker image builder for all distros.
#
# Supports:
# --missing Build only if image does not exist
# --no-cache Disable docker layer cache
# --target Dockerfile target (e.g. virgin|full)
# --tag Override image tag (default: pkgmgr-$distro[-$target])
#
# Requires:
# - env var: distro (arch|debian|ubuntu|fedora|centos)
# - base.sh in same dir
#
# Examples:
# distro=arch bash scripts/build/image.sh
# distro=arch bash scripts/build/image.sh --no-cache
# distro=arch bash scripts/build/image.sh --missing
# distro=arch bash scripts/build/image.sh --target virgin
# distro=arch bash scripts/build/image.sh --target virgin --missing
# distro=arch bash scripts/build/image.sh --tag myimg:arch
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=/dev/null
source "${SCRIPT_DIR}/base.sh" source "${SCRIPT_DIR}/base.sh"
: "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}" : "${distro:?Environment variable 'distro' must be set (arch|debian|ubuntu|fedora|centos)}"
@@ -30,7 +9,15 @@ source "${SCRIPT_DIR}/base.sh"
NO_CACHE=0 NO_CACHE=0
MISSING_ONLY=0 MISSING_ONLY=0
TARGET="" TARGET=""
IMAGE_TAG="" # derive later unless --tag is provided IMAGE_TAG="" # local image name or base tag (without registry)
PUSH=0 # if 1 -> use buildx and push (requires docker buildx)
PUBLISH=0 # if 1 -> push with semantic tags (latest/version/stable + arch aliases)
REGISTRY="" # e.g. ghcr.io
OWNER="" # e.g. github org/user
REPO_PREFIX="pkgmgr" # image base name (pkgmgr)
VERSION="" # X.Y.Z (required for --publish)
IS_STABLE="false" # "true" -> publish stable tags
DEFAULT_DISTRO="arch"
usage() { usage() {
local default_tag="pkgmgr-${distro}" local default_tag="pkgmgr-${distro}"
@@ -39,14 +26,26 @@ usage() {
fi fi
cat <<EOF cat <<EOF
Usage: distro=<distro> $0 [--missing] [--no-cache] [--target <name>] [--tag <image>] Usage: distro=<distro> $0 [options]
Options: Build options:
--missing Build only if the image does not already exist --missing Build only if the image does not already exist (local build only)
--no-cache Build with --no-cache --no-cache Build with --no-cache
--target <name> Build a specific Dockerfile target (e.g. virgin|full) --target <name> Build a specific Dockerfile target (e.g. virgin)
--tag <image> Override the output image tag (default: ${default_tag}) --tag <image> Override the output image tag (default: ${default_tag})
-h, --help Show help
Publish options:
--push Push the built image (uses docker buildx build --push)
--publish Publish semantic tags (latest, <version>, optional stable) + arch aliases
--registry <reg> Registry (e.g. ghcr.io)
--owner <owner> Registry namespace (e.g. \${GITHUB_REPOSITORY_OWNER})
--repo-prefix <name> Image base name (default: pkgmgr)
--version <X.Y.Z> Version for --publish
--stable <true|false> Whether to publish :stable tags (default: false)
Notes:
- --publish implies --push and requires --registry, --owner, and --version.
- Local build (no --push) uses "docker build" and creates local images like "pkgmgr-arch" / "pkgmgr-arch-virgin".
EOF EOF
} }
@@ -56,18 +55,39 @@ while [[ $# -gt 0 ]]; do
--missing) MISSING_ONLY=1; shift ;; --missing) MISSING_ONLY=1; shift ;;
--target) --target)
TARGET="${2:-}" TARGET="${2:-}"
if [[ -z "${TARGET}" ]]; then [[ -n "${TARGET}" ]] || { echo "ERROR: --target requires a value (e.g. virgin)"; exit 2; }
echo "ERROR: --target requires a value (e.g. virgin|full)" >&2
exit 2
fi
shift 2 shift 2
;; ;;
--tag) --tag)
IMAGE_TAG="${2:-}" IMAGE_TAG="${2:-}"
if [[ -z "${IMAGE_TAG}" ]]; then [[ -n "${IMAGE_TAG}" ]] || { echo "ERROR: --tag requires a value"; exit 2; }
echo "ERROR: --tag requires a value" >&2 shift 2
exit 2 ;;
fi --push) PUSH=1; shift ;;
--publish) PUBLISH=1; PUSH=1; shift ;;
--registry)
REGISTRY="${2:-}"
[[ -n "${REGISTRY}" ]] || { echo "ERROR: --registry requires a value"; exit 2; }
shift 2
;;
--owner)
OWNER="${2:-}"
[[ -n "${OWNER}" ]] || { echo "ERROR: --owner requires a value"; exit 2; }
shift 2
;;
--repo-prefix)
REPO_PREFIX="${2:-}"
[[ -n "${REPO_PREFIX}" ]] || { echo "ERROR: --repo-prefix requires a value"; exit 2; }
shift 2
;;
--version)
VERSION="${2:-}"
[[ -n "${VERSION}" ]] || { echo "ERROR: --version requires a value"; exit 2; }
shift 2
;;
--stable)
IS_STABLE="${2:-}"
[[ -n "${IS_STABLE}" ]] || { echo "ERROR: --stable requires a value (true|false)"; exit 2; }
shift 2 shift 2
;; ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
@@ -79,9 +99,9 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
# Auto-tag: if --tag not provided, derive from distro (+ target suffix) # Derive default local tag if not provided
if [[ -z "${IMAGE_TAG}" ]]; then if [[ -z "${IMAGE_TAG}" ]]; then
IMAGE_TAG="pkgmgr-${distro}" IMAGE_TAG="${REPO_PREFIX}-${distro}"
if [[ -n "${TARGET}" ]]; then if [[ -n "${TARGET}" ]]; then
IMAGE_TAG="${IMAGE_TAG}-${TARGET}" IMAGE_TAG="${IMAGE_TAG}-${TARGET}"
fi fi
@@ -89,22 +109,51 @@ fi
BASE_IMAGE="$(resolve_base_image "$distro")" BASE_IMAGE="$(resolve_base_image "$distro")"
# Local-only "missing" shortcut
if [[ "${MISSING_ONLY}" == "1" ]]; then if [[ "${MISSING_ONLY}" == "1" ]]; then
if [[ "${PUSH}" == "1" ]]; then
echo "ERROR: --missing is only supported for local builds (without --push/--publish)" >&2
exit 2
fi
if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then if docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
echo "[build] Image already exists: ${IMAGE_TAG} (skipping due to --missing)" echo "[build] Image already exists: ${IMAGE_TAG} (skipping due to --missing)"
exit 0 exit 0
fi fi
fi fi
# Validate publish parameters
if [[ "${PUBLISH}" == "1" ]]; then
[[ -n "${REGISTRY}" ]] || { echo "ERROR: --publish requires --registry"; exit 2; }
[[ -n "${OWNER}" ]] || { echo "ERROR: --publish requires --owner"; exit 2; }
[[ -n "${VERSION}" ]] || { echo "ERROR: --publish requires --version"; exit 2; }
fi
# Guard: --push without --publish requires fully-qualified --tag
if [[ "${PUSH}" == "1" && "${PUBLISH}" != "1" ]]; then
if [[ "${IMAGE_TAG}" != */* ]]; then
echo "ERROR: --push requires --tag with a fully-qualified name (e.g. ghcr.io/<owner>/<image>:tag), or use --publish" >&2
exit 2
fi
fi
echo echo
echo "------------------------------------------------------------" echo "------------------------------------------------------------"
echo "[build] Building image: ${IMAGE_TAG}" echo "[build] Building image"
echo "distro = ${distro}" echo "distro = ${distro}"
echo "BASE_IMAGE = ${BASE_IMAGE}" echo "BASE_IMAGE = ${BASE_IMAGE}"
if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi if [[ -n "${TARGET}" ]]; then echo "target = ${TARGET}"; fi
if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi if [[ "${NO_CACHE}" == "1" ]]; then echo "cache = disabled"; fi
if [[ "${PUSH}" == "1" ]]; then echo "push = enabled"; fi
if [[ "${PUBLISH}" == "1" ]]; then
echo "publish = enabled"
echo "registry = ${REGISTRY}"
echo "owner = ${OWNER}"
echo "version = ${VERSION}"
echo "stable = ${IS_STABLE}"
fi
echo "------------------------------------------------------------" echo "------------------------------------------------------------"
# Common build args
build_args=(--build-arg "BASE_IMAGE=${BASE_IMAGE}") build_args=(--build-arg "BASE_IMAGE=${BASE_IMAGE}")
if [[ "${NO_CACHE}" == "1" ]]; then if [[ "${NO_CACHE}" == "1" ]]; then
@@ -115,6 +164,62 @@ if [[ -n "${TARGET}" ]]; then
build_args+=(--target "${TARGET}") build_args+=(--target "${TARGET}")
fi fi
build_args+=(-t "${IMAGE_TAG}" .) compute_publish_tags() {
local distro_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}-${distro}"
local alias_tag_base=""
docker build "${build_args[@]}" if [[ -n "${TARGET}" ]]; then
distro_tag_base="${distro_tag_base}-${TARGET}"
fi
if [[ "${distro}" == "${DEFAULT_DISTRO}" ]]; then
alias_tag_base="${REGISTRY}/${OWNER}/${REPO_PREFIX}"
if [[ -n "${TARGET}" ]]; then
alias_tag_base="${alias_tag_base}-${TARGET}"
fi
fi
local tags=()
tags+=("${distro_tag_base}:latest")
tags+=("${distro_tag_base}:${VERSION}")
if [[ "${IS_STABLE}" == "true" ]]; then
tags+=("${distro_tag_base}:stable")
fi
if [[ -n "${alias_tag_base}" ]]; then
tags+=("${alias_tag_base}:latest")
tags+=("${alias_tag_base}:${VERSION}")
if [[ "${IS_STABLE}" == "true" ]]; then
tags+=("${alias_tag_base}:stable")
fi
fi
printf '%s\n' "${tags[@]}"
}
if [[ "${PUSH}" == "1" ]]; then
bx_args=(docker buildx build --push)
if [[ "${PUBLISH}" == "1" ]]; then
while IFS= read -r t; do
bx_args+=(-t "$t")
done < <(compute_publish_tags)
else
bx_args+=(-t "${IMAGE_TAG}")
fi
bx_args+=("${build_args[@]}")
bx_args+=(.)
echo "[build] Running: ${bx_args[*]}"
"${bx_args[@]}"
else
local_args=(docker build)
local_args+=("${build_args[@]}")
local_args+=(-t "${IMAGE_TAG}")
local_args+=(.)
echo "[build] Running: ${local_args[*]}"
"${local_args[@]}"
fi

55
scripts/build/publish.sh Executable file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
# Publish all distro images (full + virgin) to a registry via image.sh --publish
#
# Required env:
# OWNER (e.g. GITHUB_REPOSITORY_OWNER)
# VERSION (e.g. 1.2.3)
#
# Optional env:
# REGISTRY (default: ghcr.io)
# IS_STABLE (default: false)
# DISTROS (default: "arch debian ubuntu fedora centos")
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REGISTRY="${REGISTRY:-ghcr.io}"
IS_STABLE="${IS_STABLE:-false}"
DISTROS="${DISTROS:-arch debian ubuntu fedora centos}"
: "${OWNER:?Environment variable OWNER must be set (e.g. github.repository_owner)}"
: "${VERSION:?Environment variable VERSION must be set (e.g. 1.2.3)}"
echo "[publish] REGISTRY=${REGISTRY}"
echo "[publish] OWNER=${OWNER}"
echo "[publish] VERSION=${VERSION}"
echo "[publish] IS_STABLE=${IS_STABLE}"
echo "[publish] DISTROS=${DISTROS}"
for d in ${DISTROS}; do
echo
echo "============================================================"
echo "[publish] distro=${d}"
echo "============================================================"
# virgin
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
--publish \
--registry "${REGISTRY}" \
--owner "${OWNER}" \
--version "${VERSION}" \
--stable "${IS_STABLE}" \
--target virgin
# full (default target)
distro="${d}" bash "${SCRIPT_DIR}/image.sh" \
--publish \
--registry "${REGISTRY}" \
--owner "${OWNER}" \
--version "${VERSION}" \
--stable "${IS_STABLE}"
done
echo
echo "[publish] Done."

View File

@@ -3,22 +3,19 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null # shellcheck disable=SC1091
source "${SCRIPT_DIR}/lib.sh" source "${SCRIPT_DIR}/os_resolver.sh"
OS_ID="$(detect_os_id)" OS_ID="$(osr_get_os_id)"
echo "[run-dependencies] Detected OS: ${OS_ID}" echo "[run-dependencies] Detected OS: ${OS_ID}"
case "${OS_ID}" in if ! osr_is_supported "${OS_ID}"; then
arch|debian|ubuntu|fedora|centos)
DEP_SCRIPT="${SCRIPT_DIR}/${OS_ID}/dependencies.sh"
;;
*)
echo "[run-dependencies] Unsupported OS: ${OS_ID}" echo "[run-dependencies] Unsupported OS: ${OS_ID}"
exit 1 exit 1
;; fi
esac
DEP_SCRIPT="$(osr_script_path_for "${SCRIPT_DIR}" "${OS_ID}" "dependencies")"
if [[ ! -f "${DEP_SCRIPT}" ]]; then if [[ ! -f "${DEP_SCRIPT}" ]]; then
echo "[run-dependencies] Dependency script not found: ${DEP_SCRIPT}" echo "[run-dependencies] Dependency script not found: ${DEP_SCRIPT}"

View File

@@ -1,12 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
detect_os_id() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
echo "${ID:-unknown}"
else
echo "unknown"
fi
}

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
# -----------------------------------------------------------------------------
# OsResolver (bash "class-style" module)
# Centralizes OS detection + normalization + supported checks + script paths.
# -----------------------------------------------------------------------------
osr_detect_raw_id() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
echo "${ID:-unknown}"
else
echo "unknown"
fi
}
osr_detect_id_like() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
. /etc/os-release
echo "${ID_LIKE:-}"
else
echo ""
fi
}
osr_normalize_id() {
local raw="${1:-unknown}"
local like="${2:-}"
# Explicit mapping first (your bugfix: manjaro -> arch everywhere)
case "${raw}" in
manjaro) echo "arch"; return 0 ;;
esac
# Keep direct IDs when they are already supported
case "${raw}" in
arch|debian|ubuntu|fedora|centos) echo "${raw}"; return 0 ;;
esac
# Fallback mapping via ID_LIKE for better portability
# Example: many Arch derivatives expose ID_LIKE="arch"
if [[ " ${like} " == *" arch "* ]]; then
echo "arch"; return 0
fi
if [[ " ${like} " == *" debian "* ]]; then
echo "debian"; return 0
fi
if [[ " ${like} " == *" fedora "* ]]; then
echo "fedora"; return 0
fi
if [[ " ${like} " == *" rhel "* || " ${like} " == *" centos "* ]]; then
echo "centos"; return 0
fi
echo "${raw}"
}
osr_get_os_id() {
local raw like
raw="$(osr_detect_raw_id)"
like="$(osr_detect_id_like)"
osr_normalize_id "${raw}" "${like}"
}
osr_is_supported() {
local id="${1:-unknown}"
case "${id}" in
arch|debian|ubuntu|fedora|centos) return 0 ;;
*) return 1 ;;
esac
}
osr_script_path_for() {
local script_dir="${1:?script_dir required}"
local os_id="${2:?os_id required}"
local kind="${3:?kind required}" # "dependencies" or "package"
echo "${script_dir}/${os_id}/${kind}.sh"
}

View File

@@ -3,28 +3,19 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null # shellcheck disable=SC1091
source "${SCRIPT_DIR}/lib.sh" source "${SCRIPT_DIR}/os_resolver.sh"
OS_ID="$(detect_os_id)" OS_ID="$(osr_get_os_id)"
# Map Manjaro to Arch
if [[ "${OS_ID}" == "manjaro" ]]; then
echo "[package] Mapping OS 'manjaro' → 'arch'"
OS_ID="arch"
fi
echo "[package] Detected OS: ${OS_ID}" echo "[package] Detected OS: ${OS_ID}"
case "${OS_ID}" in if ! osr_is_supported "${OS_ID}"; then
arch|debian|ubuntu|fedora|centos)
PKG_SCRIPT="${SCRIPT_DIR}/${OS_ID}/package.sh"
;;
*)
echo "[package] Unsupported OS: ${OS_ID}" echo "[package] Unsupported OS: ${OS_ID}"
exit 1 exit 1
;; fi
esac
PKG_SCRIPT="$(osr_script_path_for "${SCRIPT_DIR}" "${OS_ID}" "package")"
if [[ ! -f "${PKG_SCRIPT}" ]]; then if [[ ! -f "${PKG_SCRIPT}" ]]; then
echo "[package] Package script not found: ${PKG_SCRIPT}" echo "[package] Package script not found: ${PKG_SCRIPT}"

0
scripts/nix/lib/config.sh Normal file → Executable file
View File

0
scripts/nix/lib/detect.sh Normal file → Executable file
View File

0
scripts/nix/lib/install.sh Normal file → Executable file
View File

0
scripts/nix/lib/path.sh Normal file → Executable file
View File

0
scripts/nix/lib/symlinks.sh Normal file → Executable file
View File

0
scripts/nix/lib/users.sh Normal file → Executable file
View File

View File

@@ -1,9 +1,9 @@
# ------------------------------------------------------------ # ------------------------------------------------------------
# Nix shell mode: do not touch venv, only run main.py install # Nix shell mode: do not touch venv, only run install
# ------------------------------------------------------------ # ------------------------------------------------------------
echo "[setup] Nix mode enabled (NIX_ENABLED=1)." echo "[setup] Nix mode enabled (NIX_ENABLED=1)."
echo "[setup] Skipping virtualenv creation and dependency installation." echo "[setup] Skipping virtualenv creation and dependency installation."
echo "[setup] Running main.py install via system python3..." echo "[setup] Running install via system python3..."
python3 main.py install python3 -m pkgmgr install
echo "[setup] Setup finished (Nix mode)." echo "[setup] Setup finished (Nix mode)."

View File

@@ -15,9 +15,6 @@ RC_LINE='if [ -d "${HOME}/.venvs/pkgmgr" ]; then . "${HOME}/.venvs/pkgmgr/bin/ac
echo "[setup] Running in normal user mode (developer setup)." echo "[setup] Running in normal user mode (developer setup)."
echo "[setup] Ensuring main.py is executable..."
chmod +x main.py || true
echo "[setup] Ensuring global virtualenv root: ${HOME}/.venvs" echo "[setup] Ensuring global virtualenv root: ${HOME}/.venvs"
mkdir -p "${HOME}/.venvs" mkdir -p "${HOME}/.venvs"
@@ -90,8 +87,8 @@ for rc in "${HOME}/.bashrc" "${HOME}/.zshrc"; do
fi fi
done done
echo "[setup] Running main.py install via venv Python..." echo "[setup] Running install via venv Python..."
"${VENV_DIR}/bin/python" main.py install "${VENV_DIR}/bin/python" -m pkgmgr install
echo echo
echo "[setup] Developer setup complete." echo "[setup] Developer setup complete."

5
src/pkgmgr/__main__.py Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
from pkgmgr.cli import main
if __name__ == "__main__":
main()

View File

@@ -27,7 +27,7 @@ class TestIntegrationBranchCommands(unittest.TestCase):
try: try:
# argv[0] is the program name; the rest are CLI arguments. # argv[0] is the program name; the rest are CLI arguments.
sys.argv = ["pkgmgr"] + list(extra_args) sys.argv = ["pkgmgr"] + list(extra_args)
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
finally: finally:
sys.argv = original_argv sys.argv = original_argv

View File

@@ -24,7 +24,7 @@ def _run_pkgmgr_help(argv_tail: list[str]) -> str:
try: try:
with redirect_stdout(buffer), redirect_stderr(buffer): with redirect_stdout(buffer), redirect_stderr(buffer):
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else None code = exc.code if isinstance(exc.code, int) else None
if code not in (0, None): if code not in (0, None):

View File

@@ -53,7 +53,7 @@ class TestIntegrationChangelogCommands(unittest.TestCase):
sys.argv = ["pkgmgr", "changelog"] + list(extra_args) sys.argv = ["pkgmgr", "changelog"] + list(extra_args)
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0:

View File

@@ -47,7 +47,7 @@ class TestIntegrationCloneAllHttps(unittest.TestCase):
try: try:
# Execute main.py as if it was called from CLI. # Execute main.py as if it was called from CLI.
# This will run the full clone pipeline inside the container. # This will run the full clone pipeline inside the container.
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
# Determine the exit code (int or string) # Determine the exit code (int or string)
exit_code = exc.code exit_code = exc.code

View File

@@ -34,7 +34,7 @@ def _run_pkgmgr_config(extra_args: list[str]) -> None:
sys.argv = ["pkgmgr"] + extra_args sys.argv = ["pkgmgr"] + extra_args
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0:

View File

@@ -139,7 +139,7 @@ class TestIntegrationInstalPKGMGRShallow(unittest.TestCase):
] ]
# Execute installation via main.py # Execute installation via main.py
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
# Debug: interactive shell test # Debug: interactive shell test
pkgmgr_help_debug() pkgmgr_help_debug()

View File

@@ -27,7 +27,7 @@ class TestIntegrationListCommands(unittest.TestCase):
sys.argv = ["pkgmgr"] + args sys.argv = ["pkgmgr"] + args
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0:

View File

@@ -44,7 +44,7 @@ class TestIntegrationMakeCommands(unittest.TestCase):
sys.argv = ["pkgmgr"] + extra_args sys.argv = ["pkgmgr"] + extra_args
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0:

View File

@@ -50,7 +50,7 @@ class TestIntegrationMirrorCommands(unittest.TestCase):
try: try:
with redirect_stdout(buffer), redirect_stderr(buffer): with redirect_stdout(buffer), redirect_stderr(buffer):
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else None code = exc.code if isinstance(exc.code, int) else None
if code not in (0, None): if code not in (0, None):

View File

@@ -50,7 +50,7 @@ class TestPathCommandsE2E(unittest.TestCase):
try: try:
# Capture stdout while running the CLI entry point. # Capture stdout while running the CLI entry point.
with redirect_stdout(buffer): with redirect_stdout(buffer):
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
# Determine the exit code (int or string) # Determine the exit code (int or string)
exit_code = exc.code exit_code = exc.code

View File

@@ -27,7 +27,7 @@ class TestIntegrationProxyCommands(unittest.TestCase):
sys.argv = ["pkgmgr"] + args sys.argv = ["pkgmgr"] + args
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0:

View File

@@ -44,7 +44,7 @@ class TestIntegrationReleaseCommand(unittest.TestCase):
try: try:
# argv[0] is the program name; the rest are CLI arguments. # argv[0] is the program name; the rest are CLI arguments.
sys.argv = ["pkgmgr"] + list(extra_args) sys.argv = ["pkgmgr"] + list(extra_args)
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
finally: finally:
sys.argv = original_argv sys.argv = original_argv
@@ -152,7 +152,7 @@ class TestIntegrationReleaseCommand(unittest.TestCase):
# argparse will call sys.exit(), so we expect a SystemExit here. # argparse will call sys.exit(), so we expect a SystemExit here.
with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf): with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
with self.assertRaises(SystemExit) as cm: with self.assertRaises(SystemExit) as cm:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
finally: finally:
sys.argv = original_argv sys.argv = original_argv

View File

@@ -55,7 +55,7 @@ class TestIntegrationToolsCommands(unittest.TestCase):
sys.argv = ["pkgmgr"] + extra_args sys.argv = ["pkgmgr"] + extra_args
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0:

View File

@@ -18,14 +18,6 @@ import sys
import unittest import unittest
from typing import List from typing import List
# Resolve project root (the repo where main.py lives, e.g. /src)
PROJECT_ROOT = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..")
)
MAIN_PATH = os.path.join(PROJECT_ROOT, "main.py")
def _run_main(argv: List[str]) -> None: def _run_main(argv: List[str]) -> None:
""" """
Helper to run main.py with the given argv. Helper to run main.py with the given argv.
@@ -40,7 +32,7 @@ def _run_main(argv: List[str]) -> None:
try: try:
sys.argv = ["pkgmgr"] + argv sys.argv = ["pkgmgr"] + argv
try: try:
runpy.run_path(MAIN_PATH, run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: # argparse uses this for --help except SystemExit as exc: # argparse uses this for --help
# SystemExit.code can be int, str or None; for our purposes: # SystemExit.code can be int, str or None; for our purposes:
code = exc.code code = exc.code

View File

@@ -130,7 +130,7 @@ class TestIntegrationVersionCommands(unittest.TestCase):
sys.argv = ["pkgmgr", "version"] + extra_args sys.argv = ["pkgmgr", "version"] + extra_args
try: try:
runpy.run_module("main", run_name="__main__") runpy.run_module("pkgmgr", run_name="__main__")
except SystemExit as exc: except SystemExit as exc:
code = exc.code if isinstance(exc.code, int) else str(exc.code) code = exc.code if isinstance(exc.code, int) else str(exc.code)
if code != 0: if code != 0: