git commit -m "Harden installers for Nix, OS packages and Docker CA handling

- NixFlakeInstaller:
  - Skip when running inside a Nix dev shell (IN_NIX_SHELL).
  - Add PKGMGR_DISABLE_NIX_FLAKE_INSTALLER kill-switch for CI/debugging.
  - Ensure run() respects supports() and handles preview/allow_failure cleanly.

- DebianControlInstaller:
  - Introduce _privileged_prefix() to handle sudo vs. root vs. no elevation.
  - Avoid hard-coded sudo usage and degrade gracefully when neither sudo nor
    root is available.
  - Improve messaging around build-dep and .deb installation.

- RpmSpecInstaller:
  - Prepare rpmbuild tree and source tarball in ~/rpmbuild/SOURCES based on
    Name/Version from the spec file.
  - Reuse a helper to resolve the rpmbuild topdir.
  - Install built RPMs via dnf/yum when available, falling back to rpm -Uvh
    to avoid file conflicts during upgrades.

- PythonInstaller:
  - Skip pip-based installation inside Nix dev shells (IN_NIX_SHELL).
  - Add PKGMGR_DISABLE_PYTHON_INSTALLER kill-switch.
  - Make pip command resolution explicit and overridable via PKGMGR_PIP.
  - Type-hint supports() and run() with RepoContext/InstallContext.

- Docker entrypoint:
  - Add robust CA bundle detection for Nix, Git, Python requests and curl.
  - Export NIX_SSL_CERT_FILE, SSL_CERT_FILE, REQUESTS_CA_BUNDLE and
    GIT_SSL_CAINFO from a single detected CA path.
  - Improve logging and section comments in the entrypoint script."

https://chatgpt.com/share/69387df8-bda0-800f-a053-aa9e2999dc84
This commit is contained in:
Kevin Veen-Birkenbach
2025-12-09 20:52:07 +01:00
parent 9357c4632e
commit 640b1042c2
5 changed files with 388 additions and 77 deletions

View File

@@ -13,6 +13,13 @@ Behavior:
* Then install the flake outputs (`pkgmgr`, `default`) via `nix profile install`. * Then install the flake outputs (`pkgmgr`, `default`) via `nix profile install`.
- Failure installing `pkgmgr` is treated as fatal. - Failure installing `pkgmgr` is treated as fatal.
- Failure installing `default` is logged as an error/warning but does not abort. - Failure installing `default` is logged as an error/warning but does not abort.
Special handling for dev shells / CI:
- If IN_NIX_SHELL is set (e.g. inside `nix develop`), the installer is
disabled. In that environment the flake outputs are already provided
by the dev shell and we must not touch the user profile.
- If PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 is set, the installer is
globally disabled (useful for CI or debugging).
""" """
import os import os
@@ -36,14 +43,45 @@ class NixFlakeInstaller(BaseInstaller):
FLAKE_FILE = "flake.nix" FLAKE_FILE = "flake.nix"
PROFILE_NAME = "package-manager" PROFILE_NAME = "package-manager"
def _in_nix_shell(self) -> bool:
"""
Return True if we appear to be running inside a Nix dev shell.
Nix sets IN_NIX_SHELL in `nix develop` environments. In that case
the flake outputs are already available, and touching the user
profile (nix profile install/remove) is undesirable.
"""
return bool(os.environ.get("IN_NIX_SHELL"))
def supports(self, ctx: "RepoContext") -> bool: def supports(self, ctx: "RepoContext") -> bool:
""" """
Only support repositories that: Only support repositories that:
- Have a flake.nix - Are NOT inside a Nix dev shell (IN_NIX_SHELL unset),
- Are NOT explicitly disabled via PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1,
- Have a flake.nix,
- And have the `nix` command available. - And have the `nix` command available.
""" """
# 1) Skip when running inside a dev shell flake is already active.
if self._in_nix_shell():
print(
"[INFO] IN_NIX_SHELL detected; skipping NixFlakeInstaller. "
"Flake outputs are provided by the development shell."
)
return False
# 2) Optional global kill-switch for CI or debugging.
if os.environ.get("PKGMGR_DISABLE_NIX_FLAKE_INSTALLER") == "1":
print(
"[INFO] PKGMGR_DISABLE_NIX_FLAKE_INSTALLER=1 "
"NixFlakeInstaller is disabled."
)
return False
# 3) Nix must be available.
if shutil.which("nix") is None: if shutil.which("nix") is None:
return False return False
# 4) flake.nix must exist in the repository.
flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE) flake_path = os.path.join(ctx.repo_dir, self.FLAKE_FILE)
return os.path.exists(flake_path) return os.path.exists(flake_path)
@@ -76,6 +114,14 @@ class NixFlakeInstaller(BaseInstaller):
Any failure installing `pkgmgr` is treated as fatal (SystemExit). Any failure installing `pkgmgr` is treated as fatal (SystemExit).
A failure installing `default` is logged but does not abort. A failure installing `default` is logged but does not abort.
""" """
# Extra guard in case run() is called directly without supports().
if self._in_nix_shell():
print(
"[INFO] IN_NIX_SHELL detected in run(); "
"skipping Nix flake profile installation."
)
return
# Reuse supports() to keep logic in one place # Reuse supports() to keep logic in one place
if not self.supports(ctx): # type: ignore[arg-type] if not self.supports(ctx): # type: ignore[arg-type]
return return
@@ -91,7 +137,12 @@ class NixFlakeInstaller(BaseInstaller):
try: try:
# For 'default' we don't want the process to exit on error # For 'default' we don't want the process to exit on error
allow_failure = output == "default" allow_failure = output == "default"
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview, allow_failure=allow_failure) run_command(
cmd,
cwd=ctx.repo_dir,
preview=ctx.preview,
allow_failure=allow_failure,
)
print(f"Nix flake output '{output}' successfully installed.") print(f"Nix flake output '{output}' successfully installed.")
except SystemExit as e: except SystemExit as e:
print(f"[Error] Failed to install Nix flake output '{output}': {e}") print(f"[Error] Failed to install Nix flake output '{output}': {e}")

View File

@@ -17,7 +17,6 @@ apt/dpkg tooling are available.
import glob import glob
import os import os
import shutil import shutil
from typing import List from typing import List
from pkgmgr.actions.repository.install.context import RepoContext from pkgmgr.actions.repository.install.context import RepoContext
@@ -68,6 +67,32 @@ class DebianControlInstaller(BaseInstaller):
pattern = os.path.join(parent, "*.deb") pattern = os.path.join(parent, "*.deb")
return sorted(glob.glob(pattern)) return sorted(glob.glob(pattern))
def _privileged_prefix(self) -> str | None:
"""
Determine how to run privileged commands:
- If 'sudo' is available, return 'sudo '.
- If we are running as root (e.g. inside CI/container), return ''.
- Otherwise, return None, meaning we cannot safely elevate.
Callers are responsible for handling the None case (usually by
warning and skipping automatic installation).
"""
sudo_path = shutil.which("sudo")
is_root = False
try:
is_root = os.geteuid() == 0
except AttributeError: # pragma: no cover - non-POSIX platforms
# On non-POSIX systems, fall back to assuming "not root".
is_root = False
if sudo_path is not None:
return "sudo "
if is_root:
return ""
return None
def _install_build_dependencies(self, ctx: RepoContext) -> None: def _install_build_dependencies(self, ctx: RepoContext) -> None:
""" """
Install build dependencies using `apt-get build-dep ./`. Install build dependencies using `apt-get build-dep ./`.
@@ -86,12 +111,25 @@ class DebianControlInstaller(BaseInstaller):
) )
return return
prefix = self._privileged_prefix()
if prefix is None:
print(
"[Warning] Neither 'sudo' is available nor running as root. "
"Skipping automatic build-dep installation for Debian. "
"Please install build dependencies from debian/control manually."
)
return
# Update package lists first for reliable build-dep resolution. # Update package lists first for reliable build-dep resolution.
run_command("sudo apt-get update", cwd=ctx.repo_dir, preview=ctx.preview) run_command(
f"{prefix}apt-get update",
cwd=ctx.repo_dir,
preview=ctx.preview,
)
# Install build dependencies based on debian/control in the current tree. # Install build dependencies based on debian/control in the current tree.
# `apt-get build-dep ./` uses the source in the current directory. # `apt-get build-dep ./` uses the source in the current directory.
builddep_cmd = "sudo apt-get build-dep -y ./" builddep_cmd = f"{prefix}apt-get build-dep -y ./"
run_command(builddep_cmd, cwd=ctx.repo_dir, preview=ctx.preview) run_command(builddep_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
def run(self, ctx: RepoContext) -> None: def run(self, ctx: RepoContext) -> None:
@@ -101,7 +139,7 @@ class DebianControlInstaller(BaseInstaller):
Steps: Steps:
1. apt-get build-dep ./ (automatic build dependency installation) 1. apt-get build-dep ./ (automatic build dependency installation)
2. dpkg-buildpackage -b -us -uc 2. dpkg-buildpackage -b -us -uc
3. sudo dpkg -i ../*.deb 3. sudo dpkg -i ../*.deb (or plain dpkg -i when running as root)
""" """
control_path = self._control_path(ctx) control_path = self._control_path(ctx)
if not os.path.exists(control_path): if not os.path.exists(control_path):
@@ -123,7 +161,17 @@ class DebianControlInstaller(BaseInstaller):
) )
return return
prefix = self._privileged_prefix()
if prefix is None:
print(
"[Warning] Neither 'sudo' is available nor running as root. "
"Skipping automatic .deb installation. "
"You can manually install the following files with dpkg -i:\n "
+ "\n ".join(debs)
)
return
# 4) Install .deb files # 4) Install .deb files
install_cmd = "sudo dpkg -i " + " ".join(os.path.basename(d) for d in debs) install_cmd = prefix + "dpkg -i " + " ".join(os.path.basename(d) for d in debs)
parent = os.path.dirname(ctx.repo_dir) parent = os.path.dirname(ctx.repo_dir)
run_command(install_cmd, cwd=parent, preview=ctx.preview) run_command(install_cmd, cwd=parent, preview=ctx.preview)

View File

@@ -7,8 +7,10 @@ Installer for RPM-based packages defined in *.spec files.
This installer: This installer:
1. Installs build dependencies via dnf/yum builddep (where available) 1. Installs build dependencies via dnf/yum builddep (where available)
2. Uses rpmbuild to build RPMs from the provided .spec file 2. Prepares a source tarball in ~/rpmbuild/SOURCES based on the .spec
3. Installs the resulting RPMs via `rpm -i` 3. Uses rpmbuild to build RPMs from the provided .spec file
4. Installs the resulting RPMs via the system package manager (dnf/yum)
or rpm as a fallback.
It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.). It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.).
""" """
@@ -16,8 +18,8 @@ It targets RPM-based systems (Fedora / RHEL / CentOS / Rocky / Alma, etc.).
import glob import glob
import os import os
import shutil import shutil
import tarfile
from typing import List, Optional from typing import List, Optional, Tuple
from pkgmgr.actions.repository.install.context import RepoContext from pkgmgr.actions.repository.install.context import RepoContext
from pkgmgr.actions.repository.install.installers.base import BaseInstaller from pkgmgr.actions.repository.install.installers.base import BaseInstaller
@@ -59,6 +61,117 @@ class RpmSpecInstaller(BaseInstaller):
return None return None
return matches[0] return matches[0]
# ------------------------------------------------------------------
# Helpers for preparing rpmbuild topdir and source tarball
# ------------------------------------------------------------------
def _rpmbuild_topdir(self) -> str:
"""
Return the rpmbuild topdir that rpmbuild will use by default.
By default this is: ~/rpmbuild
In the self-install tests, $HOME is set to /tmp/pkgmgr-self-install,
so this becomes /tmp/pkgmgr-self-install/rpmbuild which matches the
paths in the RPM build logs.
"""
home = os.path.expanduser("~")
return os.path.join(home, "rpmbuild")
def _ensure_rpmbuild_tree(self, topdir: str) -> None:
"""
Ensure the standard rpmbuild directory tree exists:
<topdir>/
BUILD/
BUILDROOT/
RPMS/
SOURCES/
SPECS/
SRPMS/
"""
for sub in ("BUILD", "BUILDROOT", "RPMS", "SOURCES", "SPECS", "SRPMS"):
os.makedirs(os.path.join(topdir, sub), exist_ok=True)
def _parse_name_version(self, spec_path: str) -> Optional[Tuple[str, str]]:
"""
Parse Name and Version from the given .spec file.
Returns (name, version) or None if either cannot be determined.
"""
name = None
version = None
with open(spec_path, "r", encoding="utf-8") as f:
for raw_line in f:
line = raw_line.strip()
# Ignore comments
if not line or line.startswith("#"):
continue
lower = line.lower()
if lower.startswith("name:"):
# e.g. "Name: package-manager"
parts = line.split(":", 1)
if len(parts) == 2:
name = parts[1].strip()
elif lower.startswith("version:"):
# e.g. "Version: 0.7.7"
parts = line.split(":", 1)
if len(parts) == 2:
version = parts[1].strip()
if name and version:
break
if not name or not version:
print(
"[Warning] Could not determine Name/Version from spec file "
f"'{spec_path}'. Skipping RPM source tarball preparation."
)
return None
return name, version
def _prepare_source_tarball(self, ctx: RepoContext, spec_path: str) -> None:
"""
Prepare a source tarball in <HOME>/rpmbuild/SOURCES that matches
the Name/Version in the .spec file.
"""
parsed = self._parse_name_version(spec_path)
if parsed is None:
return
name, version = parsed
topdir = self._rpmbuild_topdir()
self._ensure_rpmbuild_tree(topdir)
build_dir = os.path.join(topdir, "BUILD")
sources_dir = os.path.join(topdir, "SOURCES")
source_root = os.path.join(build_dir, f"{name}-{version}")
tarball_path = os.path.join(sources_dir, f"{name}-{version}.tar.gz")
# Clean any previous build directory for this name/version.
if os.path.exists(source_root):
shutil.rmtree(source_root)
# Copy the repository tree into BUILD/<name>-<version>.
shutil.copytree(ctx.repo_dir, source_root)
# Create the tarball with the top-level directory <name>-<version>.
if os.path.exists(tarball_path):
os.remove(tarball_path)
with tarfile.open(tarball_path, "w:gz") as tar:
tar.add(source_root, arcname=f"{name}-{version}")
print(
f"[INFO] Prepared RPM source tarball at '{tarball_path}' "
f"from '{ctx.repo_dir}'."
)
# ------------------------------------------------------------------
def supports(self, ctx: RepoContext) -> bool: def supports(self, ctx: RepoContext) -> bool:
""" """
This installer is supported if: This installer is supported if:
@@ -77,26 +190,13 @@ class RpmSpecInstaller(BaseInstaller):
By default, rpmbuild outputs RPMs into: By default, rpmbuild outputs RPMs into:
~/rpmbuild/RPMS/*/*.rpm ~/rpmbuild/RPMS/*/*.rpm
""" """
home = os.path.expanduser("~") topdir = self._rpmbuild_topdir()
pattern = os.path.join(home, "rpmbuild", "RPMS", "**", "*.rpm") pattern = os.path.join(topdir, "RPMS", "**", "*.rpm")
return sorted(glob.glob(pattern, recursive=True)) return sorted(glob.glob(pattern, recursive=True))
def _install_build_dependencies(self, ctx: RepoContext, spec_path: str) -> None: def _install_build_dependencies(self, ctx: RepoContext, spec_path: str) -> None:
""" """
Install build dependencies for the given .spec file. Install build dependencies for the given .spec file.
Strategy (best-effort):
1. If dnf is available:
sudo dnf builddep -y <spec>
2. Else if yum-builddep is available:
sudo yum-builddep -y <spec>
3. Else if yum is available:
sudo yum-builddep -y <spec> # Some systems provide it via yum plugin
4. Otherwise: print a warning and skip automatic builddep install.
Any failure in builddep installation is treated as fatal (SystemExit),
consistent with other installer steps.
""" """
spec_basename = os.path.basename(spec_path) spec_basename = os.path.basename(spec_path)
@@ -105,7 +205,6 @@ class RpmSpecInstaller(BaseInstaller):
elif shutil.which("yum-builddep") is not None: elif shutil.which("yum-builddep") is not None:
cmd = f"sudo yum-builddep -y {spec_basename}" cmd = f"sudo yum-builddep -y {spec_basename}"
elif shutil.which("yum") is not None: elif shutil.which("yum") is not None:
# Some distributions ship yum-builddep as a plugin.
cmd = f"sudo yum-builddep -y {spec_basename}" cmd = f"sudo yum-builddep -y {spec_basename}"
else: else:
print( print(
@@ -114,33 +213,17 @@ class RpmSpecInstaller(BaseInstaller):
) )
return return
# Run builddep in the repository directory so relative spec paths work.
run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview) run_command(cmd, cwd=ctx.repo_dir, preview=ctx.preview)
def run(self, ctx: RepoContext) -> None: def _install_built_rpms(self, ctx: RepoContext, rpms: List[str]) -> None:
""" """
Build and install RPM-based packages. Install or upgrade the built RPMs.
Steps: Strategy:
1. dnf/yum builddep <spec> (automatic build dependency installation) - Prefer dnf install -y <rpms> (handles upgrades cleanly)
2. rpmbuild -ba path/to/spec - Else yum install -y <rpms>
3. sudo rpm -i ~/rpmbuild/RPMS/*/*.rpm - Else fallback to rpm -Uvh <rpms> (upgrade/replace existing)
""" """
spec_path = self._spec_path(ctx)
if not spec_path:
return
# 1) Install build dependencies
self._install_build_dependencies(ctx, spec_path)
# 2) Build RPMs
# Use the full spec path, but run in the repo directory.
spec_basename = os.path.basename(spec_path)
build_cmd = f"rpmbuild -ba {spec_basename}"
run_command(build_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
# 3) Find built RPMs
rpms = self._find_built_rpms()
if not rpms: if not rpms:
print( print(
"[Warning] No RPM files found after rpmbuild. " "[Warning] No RPM files found after rpmbuild. "
@@ -148,13 +231,52 @@ class RpmSpecInstaller(BaseInstaller):
) )
return return
# 4) Install RPMs dnf = shutil.which("dnf")
if shutil.which("rpm") is None: yum = shutil.which("yum")
rpm = shutil.which("rpm")
if dnf is not None:
install_cmd = "sudo dnf install -y " + " ".join(rpms)
elif yum is not None:
install_cmd = "sudo yum install -y " + " ".join(rpms)
elif rpm is not None:
# Fallback: use rpm in upgrade mode so an existing older
# version is replaced instead of causing file conflicts.
install_cmd = "sudo rpm -Uvh " + " ".join(rpms)
else:
print( print(
"[Warning] rpm binary not found on PATH. " "[Warning] No suitable RPM installer (dnf/yum/rpm) found. "
"Cannot install built RPMs." "Cannot install built RPMs."
) )
return return
install_cmd = "sudo rpm -i " + " ".join(rpms)
run_command(install_cmd, cwd=ctx.repo_dir, preview=ctx.preview) run_command(install_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
def run(self, ctx: RepoContext) -> None:
"""
Build and install RPM-based packages.
Steps:
1. Prepare source tarball in ~/rpmbuild/SOURCES matching Name/Version
2. dnf/yum builddep <spec> (automatic build dependency installation)
3. rpmbuild -ba path/to/spec
4. Install built RPMs via dnf/yum (or rpm as fallback)
"""
spec_path = self._spec_path(ctx)
if not spec_path:
return
# 1) Prepare source tarball so rpmbuild finds Source0 in SOURCES.
self._prepare_source_tarball(ctx, spec_path)
# 2) Install build dependencies
self._install_build_dependencies(ctx, spec_path)
# 3) Build RPMs
spec_basename = os.path.basename(spec_path)
build_cmd = f"rpmbuild -ba {spec_basename}"
run_command(build_cmd, cwd=ctx.repo_dir, preview=ctx.preview)
# 4) Find and install built RPMs
rpms = self._find_built_rpms()
self._install_built_rpms(ctx, rpms)

View File

@@ -11,15 +11,28 @@ Strategy:
3. "pip" from PATH as last resort 3. "pip" from PATH as last resort
- If pyproject.toml exists: pip install . - If pyproject.toml exists: pip install .
All installation failures are treated as fatal errors (SystemExit). All installation failures are treated as fatal errors (SystemExit),
except when we explicitly skip the installer:
- If IN_NIX_SHELL is set, we assume Python is managed by Nix and
skip this installer entirely.
- If PKGMGR_DISABLE_PYTHON_INSTALLER=1 is set, the installer is
globally disabled (useful for CI or debugging).
""" """
from __future__ import annotations
import os import os
import sys import sys
from typing import TYPE_CHECKING
from pkgmgr.actions.repository.install.installers.base import BaseInstaller from pkgmgr.actions.repository.install.installers.base import BaseInstaller
from pkgmgr.core.command.run import run_command from pkgmgr.core.command.run import run_command
if TYPE_CHECKING:
from pkgmgr.actions.repository.install.context import RepoContext
from pkgmgr.actions.repository.install import InstallContext
class PythonInstaller(BaseInstaller): class PythonInstaller(BaseInstaller):
"""Install Python projects and dependencies via pip.""" """Install Python projects and dependencies via pip."""
@@ -27,19 +40,54 @@ class PythonInstaller(BaseInstaller):
# Logical layer name, used by capability matchers. # Logical layer name, used by capability matchers.
layer = "python" layer = "python"
def supports(self, ctx) -> bool: def _in_nix_shell(self) -> bool:
"""
Return True if we appear to be running inside a Nix dev shell.
Nix sets IN_NIX_SHELL in `nix develop` environments. In that case
the Python environment is already provided by Nix, so we must not
attempt an additional pip-based installation.
"""
return bool(os.environ.get("IN_NIX_SHELL"))
def supports(self, ctx: "RepoContext") -> bool:
""" """
Return True if this installer should handle the given repository. Return True if this installer should handle the given repository.
Only pyproject.toml is supported as the single source of truth Only pyproject.toml is supported as the single source of truth
for Python dependencies and packaging metadata. for Python dependencies and packaging metadata.
The installer is *disabled* when:
- IN_NIX_SHELL is set (Python managed by Nix dev shell), or
- PKGMGR_DISABLE_PYTHON_INSTALLER=1 is set.
""" """
# 1) Skip in Nix dev shells Python is managed by the flake/devShell.
if self._in_nix_shell():
print(
"[INFO] IN_NIX_SHELL detected; skipping PythonInstaller. "
"Python runtime is provided by the Nix dev shell."
)
return False
# 2) Optional global kill-switch.
if os.environ.get("PKGMGR_DISABLE_PYTHON_INSTALLER") == "1":
print(
"[INFO] PKGMGR_DISABLE_PYTHON_INSTALLER=1 "
"PythonInstaller is disabled."
)
return False
repo_dir = ctx.repo_dir repo_dir = ctx.repo_dir
return os.path.exists(os.path.join(repo_dir, "pyproject.toml")) return os.path.exists(os.path.join(repo_dir, "pyproject.toml"))
def _pip_cmd(self) -> str: def _pip_cmd(self) -> str:
""" """
Resolve the pip command to use. Resolve the pip command to use.
Order:
1) PKGMGR_PIP (explicit override)
2) sys.executable -m pip
3) plain "pip"
""" """
explicit = os.environ.get("PKGMGR_PIP", "").strip() explicit = os.environ.get("PKGMGR_PIP", "").strip()
if explicit: if explicit:
@@ -50,12 +98,23 @@ class PythonInstaller(BaseInstaller):
return "pip" return "pip"
def run(self, ctx) -> None: def run(self, ctx: "InstallContext") -> None:
""" """
Install Python project defined via pyproject.toml. Install Python project defined via pyproject.toml.
Any pip failure is propagated as SystemExit. Any pip failure is propagated as SystemExit.
""" """
# Extra guard in case run() is called directly without supports().
if self._in_nix_shell():
print(
"[INFO] IN_NIX_SHELL detected in PythonInstaller.run(); "
"skipping pip-based installation."
)
return
if not self.supports(ctx): # type: ignore[arg-type]
return
pip_cmd = self._pip_cmd() pip_cmd = self._pip_cmd()
pyproject = os.path.join(ctx.repo_dir, "pyproject.toml") pyproject = os.path.join(ctx.repo_dir, "pyproject.toml")

View File

@@ -2,28 +2,59 @@
set -euo pipefail set -euo pipefail
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Ensure Nix has access to a valid CA bundle (TLS trust store) # Detect and export a valid CA bundle so Nix, Git, curl and Python tooling
# can successfully perform HTTPS requests on all distros (Debian, Ubuntu,
# Fedora, RHEL, CentOS, etc.)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ -z "${NIX_SSL_CERT_FILE:-}" ]]; then detect_ca_bundle() {
if [[ -f /etc/ssl/certs/ca-certificates.crt ]]; then # Common CA bundle locations across major Linux distributions
# Debian/Ubuntu-style path local candidates=(
export NIX_SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt # Debian/Ubuntu
echo "[docker] Using CA bundle: ${NIX_SSL_CERT_FILE}" /etc/ssl/cert.pem # Some distros
elif [[ -f /etc/pki/tls/certs/ca-bundle.crt ]]; then /etc/pki/tls/certs/ca-bundle.crt # Fedora/RHEL/CentOS
# Fedora/RHEL/CentOS-style path /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem # CentOS/RHEL extracted bundle
export NIX_SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt /etc/ssl/ca-bundle.pem # Generic fallback
echo "[docker] Using CA bundle: ${NIX_SSL_CERT_FILE}" )
else
echo "[docker] WARNING: No CA bundle found for Nix (NIX_SSL_CERT_FILE not set)." for path in "${candidates[@]}"; do
echo "[docker] HTTPS access for Nix flakes may fail." if [[ -f "$path" ]]; then
fi echo "$path"
return 0
fi
done
return 1
}
# Use existing NIX_SSL_CERT_FILE if provided, otherwise auto-detect
CA_BUNDLE="${NIX_SSL_CERT_FILE:-}"
if [[ -z "${CA_BUNDLE}" ]]; then
CA_BUNDLE="$(detect_ca_bundle || true)"
fi
if [[ -n "${CA_BUNDLE}" ]]; then
# Export for Nix (critical)
export NIX_SSL_CERT_FILE="${CA_BUNDLE}"
# Export for Git, Python requests, curl, etc.
export SSL_CERT_FILE="${CA_BUNDLE}"
export REQUESTS_CA_BUNDLE="${CA_BUNDLE}"
export GIT_SSL_CAINFO="${CA_BUNDLE}"
echo "[docker] Using CA bundle: ${CA_BUNDLE}"
else
echo "[docker] WARNING: No CA certificate bundle found."
echo "[docker] HTTPS access for Nix flakes and other tools may fail."
fi fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "[docker] Starting package-manager container" echo "[docker] Starting package-manager container"
# Distro info for logging # ---------------------------------------------------------------------------
# Log distribution info
# ---------------------------------------------------------------------------
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091 # shellcheck disable=SC1091
. /etc/os-release . /etc/os-release
@@ -34,9 +65,9 @@ fi
echo "[docker] Using /src as working directory" echo "[docker] Using /src as working directory"
cd /src cd /src
# ------------------------------------------------------------ # ---------------------------------------------------------------------------
# DEV mode: build/install package-manager from current /src # DEV mode: rebuild package-manager from the mounted /src tree
# ------------------------------------------------------------ # ---------------------------------------------------------------------------
if [[ "${PKGMGR_DEV:-0}" == "1" ]]; then if [[ "${PKGMGR_DEV:-0}" == "1" ]]; then
echo "[docker] DEV mode enabled (PKGMGR_DEV=1)" echo "[docker] DEV mode enabled (PKGMGR_DEV=1)"
echo "[docker] Rebuilding package-manager from /src via scripts/installation/run-package.sh..." echo "[docker] Rebuilding package-manager from /src via scripts/installation/run-package.sh..."
@@ -49,9 +80,9 @@ if [[ "${PKGMGR_DEV:-0}" == "1" ]]; then
fi fi
fi fi
# ------------------------------------------------------------ # ---------------------------------------------------------------------------
# Hand-off to pkgmgr / arbitrary command # Hand off to pkgmgr or arbitrary command
# ------------------------------------------------------------ # ---------------------------------------------------------------------------
if [[ $# -eq 0 ]]; then if [[ $# -eq 0 ]]; then
echo "[docker] No arguments provided. Showing pkgmgr help..." echo "[docker] No arguments provided. Showing pkgmgr help..."
exec pkgmgr --help exec pkgmgr --help