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:
@@ -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}")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user