2026-07-06 16:36:27 +02:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""build_archive.py
|
|
|
|
|
|
|
|
|
|
Assembles ``llamacppctl-installable.tar.gz`` directly on the local machine
|
|
|
|
|
from the llamacppctl project files, and verifies that all required files and
|
|
|
|
|
declared dependencies are present and consistent before/after packaging.
|
|
|
|
|
|
|
|
|
|
This script is intentionally self-contained (standard library only, plus an
|
|
|
|
|
optional import of ``tomllib``/``tomli`` purely for dependency verification)
|
|
|
|
|
so it can be run directly, without first installing the llamacppctl package
|
|
|
|
|
itself.
|
|
|
|
|
|
|
|
|
|
Usage
|
|
|
|
|
-----
|
|
|
|
|
python3 build_archive.py
|
|
|
|
|
python3 build_archive.py --project-dir /path/to/llamacppctl --output /path/to/llamacppctl-installable.tar.gz
|
|
|
|
|
python3 build_archive.py --skip-tests # skip running pytest before packaging
|
|
|
|
|
python3 build_archive.py --skip-pip-check # skip the pip dependency dry-run
|
|
|
|
|
|
|
|
|
|
Exit codes
|
|
|
|
|
----------
|
|
|
|
|
0 archive built and verified successfully
|
|
|
|
|
1 a required file was missing, a test failed, or verification failed
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tarfile
|
|
|
|
|
import venv
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
# Required file manifest.
|
|
|
|
|
#
|
|
|
|
|
# Every path here is relative to the project directory and MUST exist before
|
|
|
|
|
# packaging. This is the "verifies that all dependencies are correctly
|
|
|
|
|
# included" contract: the archive is only built if every one of these is
|
|
|
|
|
# present, and re-opened afterwards to confirm every one of them made it
|
|
|
|
|
# into the tarball unmodified.
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
REQUIRED_FILES = [
|
|
|
|
|
"pyproject.toml",
|
|
|
|
|
"README.md",
|
2026-07-07 10:36:14 +02:00
|
|
|
"LICENSE",
|
|
|
|
|
"CHANGELOG.md",
|
2026-07-06 16:36:27 +02:00
|
|
|
"requirements.txt",
|
|
|
|
|
"requirements-dev.txt",
|
|
|
|
|
"llama.cpp.config.example",
|
|
|
|
|
"docs/SECURITY_AND_OPERATIONS.md",
|
|
|
|
|
"man/llamacppctl.1",
|
|
|
|
|
"src/llamacppctl/__init__.py",
|
|
|
|
|
"src/llamacppctl/py.typed",
|
|
|
|
|
"src/llamacppctl/cli.py",
|
|
|
|
|
"src/llamacppctl/schema.py",
|
|
|
|
|
"src/llamacppctl/config.py",
|
|
|
|
|
"src/llamacppctl/prompt_io.py",
|
|
|
|
|
"src/llamacppctl/docker_ops.py",
|
|
|
|
|
"src/llamacppctl/http_ops.py",
|
|
|
|
|
"src/llamacppctl/lock_ops.py",
|
|
|
|
|
"src/llamacppctl/actions.py",
|
|
|
|
|
"src/llamacppctl/main.py",
|
|
|
|
|
"tests/conftest.py",
|
|
|
|
|
"tests/test_prompt_sources.py",
|
|
|
|
|
"tests/test_prompt_files.py",
|
|
|
|
|
"tests/test_prompt_urls.py",
|
|
|
|
|
"tests/test_config.py",
|
|
|
|
|
"tests/test_cli.py",
|
|
|
|
|
"tests/test_docker_ops.py",
|
|
|
|
|
"tests/test_http_ops.py",
|
|
|
|
|
"tests/test_lock_ops.py",
|
docs: bring docs/ and build_archive.py up to date
Die docs/ waren gegenüber den neuen Features veraltet.
- SECURITY_AND_OPERATIONS.md: Netzwerk-Exposition (Loopback-Default,
--expose/--api-key inkl. 401-Verhalten der Endpunkte), DNS-Pinning,
--start/--change unter Lock + --force, --change validate-before-remove,
Chat-Parameter (max_tokens/chat_temperature/--stream), ${ENV}-Expansion,
--check-Exit-Codes, smoke.sh, ChatReply; Fehler-Tabelle erweitert
(401, Trunkierung, Docker-health vs. HTTP-OK).
- How_to_use.md: Abschnitt zu den neuen Optionen.
- Archiv-Report: Testzahl (130) und Dateiliste aktualisiert.
- build_archive.py: scripts/ in INCLUDE_DIRS (sonst fehlt smoke.sh im Archiv),
test_actions.py/How_to_use.md/smoke.sh in REQUIRED_FILES aufgenommen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:32:46 +02:00
|
|
|
"tests/test_actions.py",
|
2026-07-07 11:20:57 +02:00
|
|
|
"tests/test_build_archive.py",
|
docs: bring docs/ and build_archive.py up to date
Die docs/ waren gegenüber den neuen Features veraltet.
- SECURITY_AND_OPERATIONS.md: Netzwerk-Exposition (Loopback-Default,
--expose/--api-key inkl. 401-Verhalten der Endpunkte), DNS-Pinning,
--start/--change unter Lock + --force, --change validate-before-remove,
Chat-Parameter (max_tokens/chat_temperature/--stream), ${ENV}-Expansion,
--check-Exit-Codes, smoke.sh, ChatReply; Fehler-Tabelle erweitert
(401, Trunkierung, Docker-health vs. HTTP-OK).
- How_to_use.md: Abschnitt zu den neuen Optionen.
- Archiv-Report: Testzahl (130) und Dateiliste aktualisiert.
- build_archive.py: scripts/ in INCLUDE_DIRS (sonst fehlt smoke.sh im Archiv),
test_actions.py/How_to_use.md/smoke.sh in REQUIRED_FILES aufgenommen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:32:46 +02:00
|
|
|
"scripts/smoke.sh",
|
2026-07-07 11:17:41 +02:00
|
|
|
"docs/BEDIENUNGSANLEITUNG.md",
|
|
|
|
|
"docs/INSTALL_FROM_ARCHIVE.md",
|
2026-07-06 16:36:27 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Top-level directories to include wholesale (in addition to REQUIRED_FILES),
|
|
|
|
|
# so that any file added later without updating REQUIRED_FILES is still
|
|
|
|
|
# packaged. REQUIRED_FILES remains the source of truth for *verification*.
|
docs: bring docs/ and build_archive.py up to date
Die docs/ waren gegenüber den neuen Features veraltet.
- SECURITY_AND_OPERATIONS.md: Netzwerk-Exposition (Loopback-Default,
--expose/--api-key inkl. 401-Verhalten der Endpunkte), DNS-Pinning,
--start/--change unter Lock + --force, --change validate-before-remove,
Chat-Parameter (max_tokens/chat_temperature/--stream), ${ENV}-Expansion,
--check-Exit-Codes, smoke.sh, ChatReply; Fehler-Tabelle erweitert
(401, Trunkierung, Docker-health vs. HTTP-OK).
- How_to_use.md: Abschnitt zu den neuen Optionen.
- Archiv-Report: Testzahl (130) und Dateiliste aktualisiert.
- build_archive.py: scripts/ in INCLUDE_DIRS (sonst fehlt smoke.sh im Archiv),
test_actions.py/How_to_use.md/smoke.sh in REQUIRED_FILES aufgenommen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:32:46 +02:00
|
|
|
INCLUDE_DIRS = ["src", "tests", "docs", "man", "scripts"]
|
2026-07-06 16:36:27 +02:00
|
|
|
INCLUDE_FILES = [
|
|
|
|
|
"pyproject.toml",
|
|
|
|
|
"README.md",
|
2026-07-07 10:36:14 +02:00
|
|
|
"LICENSE",
|
|
|
|
|
"CHANGELOG.md",
|
2026-07-06 16:36:27 +02:00
|
|
|
"requirements.txt",
|
|
|
|
|
"requirements-dev.txt",
|
|
|
|
|
"llama.cpp.config.example",
|
|
|
|
|
"build_archive.py",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
EXCLUDE_DIR_NAMES = {"__pycache__", ".pytest_cache", ".venv", "venv", ".git", "*.egg-info"}
|
|
|
|
|
EXCLUDE_SUFFIXES = {".pyc", ".pyo"}
|
|
|
|
|
|
|
|
|
|
ARCHIVE_ROOT_NAME = "llamacppctl" # top-level directory name inside the tarball
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BuildError(RuntimeError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def log(msg: str) -> None:
|
|
|
|
|
print(f"[build_archive] {msg}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verify_required_files(project_dir: Path) -> None:
|
|
|
|
|
log("Verifying required files are present...")
|
|
|
|
|
missing = []
|
|
|
|
|
for rel in REQUIRED_FILES:
|
|
|
|
|
p = project_dir / rel
|
|
|
|
|
if not p.is_file():
|
|
|
|
|
missing.append(rel)
|
|
|
|
|
if missing:
|
|
|
|
|
raise BuildError(
|
|
|
|
|
"Missing required files, refusing to build archive:\n "
|
|
|
|
|
+ "\n ".join(missing)
|
|
|
|
|
)
|
|
|
|
|
log(f" All {len(REQUIRED_FILES)} required files present.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verify_dependencies_declared(project_dir: Path) -> list:
|
2026-07-07 11:17:41 +02:00
|
|
|
"""Parses pyproject.toml and returns the declared runtime dependency
|
|
|
|
|
specifiers.
|
2026-07-06 16:36:27 +02:00
|
|
|
|
2026-07-07 11:17:41 +02:00
|
|
|
pyproject.toml is the single source of truth for dependencies
|
|
|
|
|
(requirements.txt merely installs the package), so there is no requirements
|
|
|
|
|
mirror to cross-check. Raises BuildError if none are declared.
|
2026-07-06 16:36:27 +02:00
|
|
|
"""
|
2026-07-07 11:17:41 +02:00
|
|
|
log("Reading declared runtime dependencies from pyproject.toml...")
|
2026-07-06 16:36:27 +02:00
|
|
|
pyproject_path = project_dir / "pyproject.toml"
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import tomllib # Python 3.11+
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
try:
|
|
|
|
|
import tomli as tomllib # type: ignore
|
|
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
tomllib = None
|
|
|
|
|
|
|
|
|
|
if tomllib is not None:
|
|
|
|
|
with open(pyproject_path, "rb") as f:
|
|
|
|
|
data = tomllib.load(f)
|
|
|
|
|
deps = data.get("project", {}).get("dependencies", [])
|
|
|
|
|
else:
|
|
|
|
|
# Fallback: minimal manual extraction, good enough for verification
|
|
|
|
|
# purposes when neither tomllib nor tomli is available.
|
|
|
|
|
log(" (tomllib/tomli unavailable, falling back to manual parse)")
|
|
|
|
|
deps = []
|
|
|
|
|
in_deps = False
|
|
|
|
|
for line in pyproject_path.read_text(encoding="utf-8").splitlines():
|
|
|
|
|
stripped = line.strip()
|
|
|
|
|
if stripped.startswith("dependencies"):
|
|
|
|
|
in_deps = True
|
|
|
|
|
if "[" in stripped and "]" in stripped:
|
|
|
|
|
inner = stripped.split("[", 1)[1].rsplit("]", 1)[0]
|
|
|
|
|
deps = [d.strip().strip('"').strip("'") for d in inner.split(",") if d.strip()]
|
|
|
|
|
in_deps = False
|
|
|
|
|
continue
|
|
|
|
|
if in_deps:
|
|
|
|
|
if "]" in stripped:
|
|
|
|
|
in_deps = False
|
|
|
|
|
stripped = stripped.split("]", 1)[0]
|
|
|
|
|
cleaned = stripped.strip().rstrip(",").strip('"').strip("'")
|
|
|
|
|
if cleaned:
|
|
|
|
|
deps.append(cleaned)
|
|
|
|
|
|
|
|
|
|
if not deps:
|
|
|
|
|
raise BuildError("No runtime dependencies found in pyproject.toml [project.dependencies]")
|
|
|
|
|
|
|
|
|
|
log(f" pyproject.toml dependencies: {deps}")
|
|
|
|
|
return deps
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_pip_dependency_check(project_dir: Path, deps: list) -> None:
|
|
|
|
|
"""Creates a throwaway virtualenv and does a real `pip install` of just
|
|
|
|
|
the declared dependencies (not the project itself) to confirm they are
|
|
|
|
|
resolvable from PyPI. This is a stronger guarantee than a --dry-run
|
|
|
|
|
against an environment that might already happen to have them cached."""
|
|
|
|
|
log("Verifying dependencies are installable (throwaway virtualenv)...")
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="llamacppctl-depcheck-") as tmp:
|
|
|
|
|
venv_dir = Path(tmp) / "venv"
|
|
|
|
|
venv.EnvBuilder(with_pip=True, clear=True).create(venv_dir)
|
|
|
|
|
pip_bin = venv_dir / "bin" / "pip"
|
|
|
|
|
if not pip_bin.exists():
|
|
|
|
|
pip_bin = venv_dir / "Scripts" / "pip.exe" # Windows fallback
|
|
|
|
|
|
|
|
|
|
cmd = [str(pip_bin), "install", "--quiet", *deps]
|
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
raise BuildError(
|
|
|
|
|
"Dependency installability check failed:\n"
|
|
|
|
|
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
|
|
|
)
|
|
|
|
|
log(" All declared dependencies installed successfully in an isolated environment.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_tests(project_dir: Path) -> None:
|
|
|
|
|
log("Running test suite (pytest) before packaging...")
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
[sys.executable, "-m", "pytest", "-q"],
|
|
|
|
|
cwd=str(project_dir),
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
print(result.stdout)
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
print(result.stderr, file=sys.stderr)
|
|
|
|
|
raise BuildError(f"Test suite failed (exit code {result.returncode}); refusing to package.")
|
|
|
|
|
log(" Test suite passed.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _tar_filter(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo | None:
|
|
|
|
|
parts = Path(tarinfo.name).parts
|
|
|
|
|
for part in parts:
|
|
|
|
|
if part in EXCLUDE_DIR_NAMES or part.endswith(".egg-info"):
|
|
|
|
|
return None
|
|
|
|
|
if Path(tarinfo.name).suffix in EXCLUDE_SUFFIXES:
|
|
|
|
|
return None
|
|
|
|
|
# Normalize metadata for reproducibility.
|
|
|
|
|
tarinfo.uid = 0
|
|
|
|
|
tarinfo.gid = 0
|
|
|
|
|
tarinfo.uname = ""
|
|
|
|
|
tarinfo.gname = ""
|
|
|
|
|
return tarinfo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_tarball(project_dir: Path, output_path: Path) -> None:
|
|
|
|
|
log(f"Building archive: {output_path}")
|
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
with tarfile.open(output_path, "w:gz") as tar:
|
|
|
|
|
for rel in INCLUDE_FILES:
|
|
|
|
|
src = project_dir / rel
|
|
|
|
|
if src.is_file():
|
|
|
|
|
tar.add(src, arcname=f"{ARCHIVE_ROOT_NAME}/{rel}", filter=_tar_filter)
|
|
|
|
|
|
|
|
|
|
for dirname in INCLUDE_DIRS:
|
|
|
|
|
src_dir = project_dir / dirname
|
|
|
|
|
if src_dir.is_dir():
|
|
|
|
|
tar.add(src_dir, arcname=f"{ARCHIVE_ROOT_NAME}/{dirname}", filter=_tar_filter)
|
|
|
|
|
|
|
|
|
|
log(f" Archive written: {output_path} ({output_path.stat().st_size:,} bytes)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verify_tarball(output_path: Path) -> None:
|
|
|
|
|
log("Re-opening archive to verify contents...")
|
|
|
|
|
with tarfile.open(output_path, "r:gz") as tar:
|
|
|
|
|
names = set(tar.getnames())
|
|
|
|
|
|
|
|
|
|
missing = []
|
|
|
|
|
for rel in REQUIRED_FILES:
|
|
|
|
|
expected = f"{ARCHIVE_ROOT_NAME}/{rel}"
|
|
|
|
|
if expected not in names:
|
|
|
|
|
missing.append(expected)
|
|
|
|
|
|
|
|
|
|
if missing:
|
|
|
|
|
raise BuildError(
|
|
|
|
|
"Archive verification failed, required files missing from tarball:\n "
|
|
|
|
|
+ "\n ".join(missing)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
log(f" Verified {len(REQUIRED_FILES)} required files are present in the archive.")
|
|
|
|
|
log(f" Total archive members: {len(names)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def smoke_test_install(output_path: Path) -> None:
|
|
|
|
|
"""Extracts the tarball into a temp dir, installs it in a throwaway
|
|
|
|
|
virtualenv, and confirms the `llamacppctl` console script works via
|
|
|
|
|
--print-effective-config and --dry-run (no Docker daemon required for
|
|
|
|
|
either)."""
|
|
|
|
|
log("Smoke-testing installation from the built archive...")
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="llamacppctl-smoketest-") as tmp:
|
|
|
|
|
tmp_path = Path(tmp)
|
|
|
|
|
with tarfile.open(output_path, "r:gz") as tar:
|
|
|
|
|
tar.extractall(tmp_path)
|
|
|
|
|
|
|
|
|
|
extracted_project = tmp_path / ARCHIVE_ROOT_NAME
|
|
|
|
|
venv_dir = tmp_path / "venv"
|
|
|
|
|
venv.EnvBuilder(with_pip=True, clear=True).create(venv_dir)
|
|
|
|
|
pip_bin = venv_dir / "bin" / "pip"
|
|
|
|
|
llamacppctl_bin = venv_dir / "bin" / "llamacppctl"
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
[str(pip_bin), "install", "--quiet", str(extracted_project)],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
raise BuildError(
|
|
|
|
|
"Smoke-test install failed:\n"
|
|
|
|
|
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
|
|
|
|
)
|
|
|
|
|
log(" Package installed successfully from the extracted archive.")
|
|
|
|
|
|
|
|
|
|
config_example = extracted_project / "llama.cpp.config.example"
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
str(llamacppctl_bin),
|
|
|
|
|
"--print-effective-config",
|
|
|
|
|
"--config", str(config_example),
|
2026-07-07 09:53:43 +02:00
|
|
|
"--profile", "qwen35b",
|
2026-07-06 16:36:27 +02:00
|
|
|
"--start",
|
|
|
|
|
"--dry-run",
|
|
|
|
|
],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
print(result.stdout)
|
|
|
|
|
|
|
|
|
|
docker_unavailable = "docker is not available on PATH" in (result.stdout + result.stderr)
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0 and not docker_unavailable:
|
|
|
|
|
print(result.stderr, file=sys.stderr)
|
|
|
|
|
raise BuildError(
|
|
|
|
|
f"Smoke test invocation failed unexpectedly (exit code {result.returncode})."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if docker_unavailable:
|
|
|
|
|
log(
|
|
|
|
|
" Console script and argument/config resolution work end-to-end; "
|
|
|
|
|
"exited cleanly at the expected Docker-unavailable check "
|
|
|
|
|
"(no Docker daemon in this environment -- this is not a packaging defect)."
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
log(" --print-effective-config / --dry-run smoke test succeeded.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main(argv: list | None = None) -> int:
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--project-dir",
|
|
|
|
|
default=str(Path(__file__).resolve().parent),
|
|
|
|
|
help="Path to the llamacppctl project directory (default: this script's directory)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--output",
|
|
|
|
|
default=None,
|
|
|
|
|
help="Output path for the tar.gz (default: <project-dir>/../llamacppctl-installable.tar.gz)",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument("--skip-tests", action="store_true", help="Skip running pytest before packaging")
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--skip-pip-check",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Skip the throwaway-virtualenv dependency installability check",
|
|
|
|
|
)
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
"--skip-smoke-test",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Skip extracting + installing the built archive as a final smoke test",
|
|
|
|
|
)
|
|
|
|
|
args = parser.parse_args(argv)
|
|
|
|
|
|
|
|
|
|
project_dir = Path(args.project_dir).resolve()
|
|
|
|
|
output_path = (
|
|
|
|
|
Path(args.output).resolve()
|
|
|
|
|
if args.output
|
|
|
|
|
else project_dir.parent / "llamacppctl-installable.tar.gz"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
verify_required_files(project_dir)
|
|
|
|
|
deps = verify_dependencies_declared(project_dir)
|
|
|
|
|
|
|
|
|
|
if not args.skip_pip_check:
|
|
|
|
|
run_pip_dependency_check(project_dir, deps)
|
|
|
|
|
else:
|
|
|
|
|
log("Skipping pip dependency installability check (--skip-pip-check).")
|
|
|
|
|
|
|
|
|
|
if not args.skip_tests:
|
|
|
|
|
run_tests(project_dir)
|
|
|
|
|
else:
|
|
|
|
|
log("Skipping test suite (--skip-tests).")
|
|
|
|
|
|
|
|
|
|
build_tarball(project_dir, output_path)
|
|
|
|
|
verify_tarball(output_path)
|
|
|
|
|
|
|
|
|
|
if not args.skip_smoke_test:
|
|
|
|
|
smoke_test_install(output_path)
|
|
|
|
|
else:
|
|
|
|
|
log("Skipping install smoke test (--skip-smoke-test).")
|
|
|
|
|
|
|
|
|
|
except BuildError as exc:
|
|
|
|
|
print(f"\n[build_archive] BUILD FAILED: {exc}", file=sys.stderr)
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
log("")
|
|
|
|
|
log("Build and verification complete.")
|
|
|
|
|
log(f"Archive: {output_path}")
|
|
|
|
|
log(f"Size: {output_path.stat().st_size:,} bytes")
|
|
|
|
|
log("")
|
|
|
|
|
log("To install on the target machine:")
|
|
|
|
|
log(f" tar xzf {output_path.name}")
|
|
|
|
|
log(f" cd {ARCHIVE_ROOT_NAME}")
|
|
|
|
|
log(" python3 -m venv .venv && source .venv/bin/activate")
|
|
|
|
|
log(" pip install .")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
sys.exit(main())
|