Source code for scitex_container.apptainer._freeze

#!/usr/bin/env python3
# Timestamp: "2026-02-25"
# File: src/scitex_container/apptainer/_freeze.py
"""Extract pinned versions from a built SIF for reproducibility."""

from __future__ import annotations

import logging
import subprocess
from pathlib import Path

from scitex_container._compat import supports_return_as

from ._utils import detect_container_cmd

logger = logging.getLogger(__name__)


[docs] @supports_return_as def freeze( sif_path: str | Path, output_dir: str | Path | None = None, ) -> dict[str, Path]: """Extract pinned versions from a built SIF. Parameters ---------- sif_path : str or Path Path to the .sif file. output_dir : str or Path, optional Directory for lock files. Defaults to same dir as .sif. Returns ------- dict[str, Path] Mapping of lock file type to path: {pip, dpkg, node}. Raises ------ FileNotFoundError If SIF file or container command not found. """ sif_path = Path(sif_path) if not sif_path.exists(): raise FileNotFoundError(f"SIF not found: {sif_path}") cmd = detect_container_cmd() out_dir = Path(output_dir) if output_dir else sif_path.parent out_dir.mkdir(parents=True, exist_ok=True) lock_files = {} # pip freeze pip_lock = out_dir / "requirements-lock.txt" logger.info("Extracting pip freeze...") result = subprocess.run( [cmd, "exec", str(sif_path), "pip", "freeze"], capture_output=True, text=True, ) if result.returncode == 0: pip_lock.write_text(result.stdout) lock_files["pip"] = pip_lock # dpkg packages dpkg_lock = out_dir / "dpkg-lock.txt" logger.info("Extracting dpkg packages...") result = subprocess.run( [cmd, "exec", str(sif_path), "dpkg-query", "-W", "-f=${Package}=${Version}\n"], capture_output=True, text=True, ) if result.returncode == 0: dpkg_lock.write_text(result.stdout) lock_files["dpkg"] = dpkg_lock # npm global packages node_lock = out_dir / "node-lock.txt" logger.info("Extracting npm packages...") result = subprocess.run( [cmd, "exec", str(sif_path), "npm", "list", "-g", "--depth=0", "--json"], capture_output=True, text=True, ) if result.returncode == 0: node_lock.write_text(result.stdout) lock_files["node"] = node_lock logger.info("Freeze complete: %d lock files", len(lock_files)) return lock_files
# EOF