5a31c6f9b2
tracker-policy-pr / check-pr (pull_request) Successful in 16s
test / unit (pull_request) Successful in 44s
test / integration-docker (pull_request) Successful in 43s
lint / lint (push) Successful in 1m0s
test / integration-firecracker (pull_request) Successful in 3m54s
test / coverage (pull_request) Successful in 25s
test / publish-infra (pull_request) Has been skipped
Addresses the review on PR #481. Self-contained wheel (review point 1): the gateway/infra/orchestrator images build from a context that must hold bot_bottle/, pyproject.toml, and the root-level Dockerfiles. Modules previously located these by walking __file__ to the repo root, so an installed wheel (package in site-packages, no repo root) passed `doctor` but failed `start`. - Add bot_bottle/resources.py: build_root() returns the repo root in a checkout (unchanged) or a staged copy from the wheel's bundled _resources/ otherwise; dockerfile()/nix_netpool_module()/ netpool_script() derive from it. - setup.py bundles the root Dockerfiles, nix module, netpool script, and pyproject.toml into bot_bottle/_resources/ at build; MANIFEST.in ships them in the sdist. - Route every _REPO_ROOT/_REPO_DIR call site (docker/macos launch, macos infra, firecracker infra_vm/infra_artifact/setup, orchestrator lifecycle/gateway) through resources. Checkout behavior is unchanged. install.sh prerequisites (review point 2): check for git when installing a git+ spec, and — before the pip fallback — that pip is usable and the interpreter isn't externally managed (PEP 668), pointing at pipx. Tests: test_resources covers checkout + staged-wheel layouts; test_wheel_install builds the wheel, installs it into an isolated venv, and asserts `doctor` runs and build_root() yields a valid context. Running `start` end-to-end still needs a Docker/KVM host (CI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
5.0 KiB
Python
132 lines
5.0 KiB
Python
"""Locate build-time resources whether bot-bottle runs from a source
|
|
checkout or an installed wheel.
|
|
|
|
The gateway / infra / orchestrator images are built from a Docker (or Apple
|
|
`container`) build context that must contain the `bot_bottle` package,
|
|
`pyproject.toml`, and the root-level Dockerfiles as siblings. In a source
|
|
checkout that context is simply the repo root, one level above the package.
|
|
An installed wheel has no repo root: the same root-level files are shipped
|
|
inside the package under ``bot_bottle/_resources/`` (see ``setup.py``), and a
|
|
repo-root-shaped build context is staged on demand into the app-data dir.
|
|
|
|
``build_root()`` is the single source of truth — it returns a directory laid
|
|
out like a repo root (has ``bot_bottle/``, ``pyproject.toml``, the
|
|
Dockerfiles, ``nix/``, ``scripts/``). Every caller that needs a build
|
|
context, a Dockerfile path, the nix netpool module, or the netpool script
|
|
derives from it, so checkout and wheel installs share one downstream path.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from .paths import bot_bottle_root
|
|
|
|
_PKG = Path(__file__).resolve().parent # …/bot_bottle
|
|
_CHECKOUT_ROOT = _PKG.parent # repo root in a checkout
|
|
_BUNDLED = _PKG / "_resources" # wheel-shipped copies
|
|
|
|
# Root-level files bundled into the wheel under ``_resources/`` (paths are
|
|
# relative to the checkout root, and preserved verbatim under ``_resources/``
|
|
# and in the staged build root). ``setup.py`` copies exactly this set; keep
|
|
# the two lists in sync (``test_resources`` guards that every entry exists).
|
|
BUNDLED_RESOURCES: tuple[str, ...] = (
|
|
"pyproject.toml",
|
|
"Dockerfile.gateway",
|
|
"Dockerfile.orchestrator",
|
|
"Dockerfile.orchestrator.fc",
|
|
"nix/firecracker-netpool.nix",
|
|
"scripts/firecracker-netpool.sh",
|
|
)
|
|
|
|
# Present at a checkout root, never in a bare installed package — the cheap
|
|
# tell for which layout we're in.
|
|
_CHECKOUT_MARKER = "Dockerfile.gateway"
|
|
|
|
|
|
class ResourceError(RuntimeError):
|
|
"""Build resources are missing from the install (corrupt/partial wheel)."""
|
|
|
|
|
|
def is_source_checkout() -> bool:
|
|
"""True when running from a source tree (the root Dockerfiles sit beside
|
|
the package); False from an installed wheel."""
|
|
return (_CHECKOUT_ROOT / _CHECKOUT_MARKER).is_file()
|
|
|
|
|
|
def build_root() -> Path:
|
|
"""A directory shaped like a repo root: ``bot_bottle/``, ``pyproject.toml``,
|
|
the root Dockerfiles, ``nix/``, and ``scripts/``.
|
|
|
|
A checkout returns the repo root itself (no copying). An installed wheel
|
|
returns a staged copy under the app-data dir, materialized once and reused
|
|
(keyed by distribution version)."""
|
|
if is_source_checkout():
|
|
return _CHECKOUT_ROOT
|
|
return _stage_build_root()
|
|
|
|
|
|
def dockerfile(name: str) -> Path:
|
|
"""Absolute path to a root-level Dockerfile, e.g. ``Dockerfile.gateway``."""
|
|
return build_root() / name
|
|
|
|
|
|
def nix_netpool_module() -> Path:
|
|
"""Absolute path to the firecracker netpool NixOS module."""
|
|
return build_root() / "nix" / "firecracker-netpool.nix"
|
|
|
|
|
|
def netpool_script() -> Path:
|
|
"""Absolute path to the firecracker netpool bring-up script."""
|
|
return build_root() / "scripts" / "firecracker-netpool.sh"
|
|
|
|
|
|
def _dist_version() -> str:
|
|
"""Installed distribution version, for keying the staged build root. Falls
|
|
back to ``dev`` when metadata is unavailable (e.g. running from a tree)."""
|
|
try:
|
|
from importlib.metadata import PackageNotFoundError, version
|
|
try:
|
|
return version("bot-bottle")
|
|
except PackageNotFoundError:
|
|
return "dev"
|
|
except Exception: # pragma: no cover - importlib.metadata is always present
|
|
return "dev"
|
|
|
|
|
|
def _stage_build_root() -> Path:
|
|
"""Materialize a repo-root-shaped build context from the installed wheel's
|
|
bundled resources. Idempotent and cached on disk by version."""
|
|
if not _BUNDLED.is_dir():
|
|
raise ResourceError(
|
|
"bot-bottle build resources are missing from this install "
|
|
f"(expected {_BUNDLED}). Reinstall the package."
|
|
)
|
|
dest = bot_bottle_root() / "build-root" / _dist_version()
|
|
if (dest / ".complete").is_file():
|
|
return dest
|
|
|
|
staging = dest.with_name(dest.name + ".staging")
|
|
shutil.rmtree(staging, ignore_errors=True)
|
|
staging.mkdir(parents=True, exist_ok=True)
|
|
|
|
# The package itself, minus caches and the bundled-resource copies, so the
|
|
# staged ``bot_bottle/`` matches a checkout's (keeps the firecracker
|
|
# infra-artifact content hash stable across checkout and wheel installs).
|
|
shutil.copytree(
|
|
_PKG,
|
|
staging / "bot_bottle",
|
|
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "_resources"),
|
|
)
|
|
# The bundled root files, restored to their checkout-relative layout.
|
|
for rel in BUNDLED_RESOURCES:
|
|
dst = staging / rel
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(_BUNDLED / rel, dst)
|
|
|
|
shutil.rmtree(dest, ignore_errors=True)
|
|
staging.rename(dest)
|
|
(dest / ".complete").write_text("")
|
|
return dest
|