"""Offline bundle builder for CadHubManage. Creates a wheelhouse and pre-built virtual environment for deployment to air-gapped Windows hosts. """ from __future__ import annotations import argparse import os import shutil import subprocess import sys from pathlib import Path def run(command: list[str], *, cwd: Path | None = None) -> None: subprocess.check_call(command, cwd=cwd) def ensure_empty_dir(path: Path) -> None: if path.exists(): if not path.is_dir(): raise ValueError(f"路径 {path} 已存在且不是目录") shutil.rmtree(path) path.mkdir(parents=True, exist_ok=True) def build_wheelhouse(requirements: Path, wheelhouse: Path, python: str) -> None: cmd = [ python, "-m", "pip", "download", "-r", str(requirements), "-d", str(wheelhouse), ] run(cmd) def create_virtualenv(project_root: Path, venv_dir: Path, python: str) -> None: run([python, "-m", "venv", str(venv_dir)]) pip_executable = venv_dir / ("Scripts" if os.name == "nt" else "bin") / "pip" wheelhouse = project_root / "dist" / "wheelhouse" cmd = [ str(pip_executable), "install", "--no-index", "--find-links", str(wheelhouse), "-r", str(project_root / "requirements.txt"), ] run(cmd) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="构建离线部署所需资源") parser.add_argument( "--project-root", default=Path(__file__).resolve().parents[1], type=Path, help="项目根目录", ) parser.add_argument( "--requirements", default=None, type=Path, help="requirements.txt 路径,默认为项目根目录下的 requirements.txt", ) parser.add_argument( "--wheelhouse", default=None, type=Path, help="离线 wheel 包存放目录,默认 dist/wheelhouse", ) parser.add_argument( "--venv-dir", default=None, type=Path, help="预构建虚拟环境的输出目录,默认 .offline-venv", ) parser.add_argument( "--python", default=sys.executable, help="用于构建的 Python 解释器路径", ) parser.add_argument( "--skip-wheelhouse", action="store_true", help="跳过 wheelhouse 构建,仅重建虚拟环境", ) parser.add_argument( "--skip-venv", action="store_true", help="跳过虚拟环境构建,仅更新 wheelhouse", ) return parser.parse_args() def main() -> None: args = parse_args() project_root = args.project_root.resolve() requirements = (args.requirements or project_root / "requirements.txt").resolve() if not requirements.exists(): raise FileNotFoundError(f"未找到依赖清单: {requirements}") wheelhouse = (args.wheelhouse or project_root / "dist" / "wheelhouse").resolve() venv_dir = (args.venv_dir or project_root / ".offline-venv").resolve() if not args.skip_wheelhouse: ensure_empty_dir(wheelhouse) build_wheelhouse(requirements, wheelhouse, args.python) if not args.skip_venv: ensure_empty_dir(venv_dir) create_virtualenv(project_root, venv_dir, args.python) if __name__ == "__main__": main()