feat: quick install script + packaging (bot-bottle doctor) #481
Reference in New Issue
Block a user
Delete Branch "prd-new-install-script"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What & why
Closes #197. bot-bottle currently has no install path — the only way to run it is to clone the repo and invoke
./cli.py. This adds a real distribution surface so casual users cancurl | shand Python-native users canpipx install.Changes
pyproject.toml— full project metadata, abot-bottleconsole-script entry point (bot_bottle.cli:main), andpackage-datafor the non-Python runtime assets (contrib Dockerfiles,egress_entrypoint.sh, firecracker netpool defaults, macos-container init). Still zero runtime pip dependencies (stdlib-only, perAGENTS.md).install.sh— POSIXsh, sudo-free, idempotent bootstrapper: checks Python ≥ 3.11, creates~/.bot-bottle/{agents,bottles,contrib}, installs viapipx(falling back topip install --user), then runsbot-bottle doctor. Does not install Docker or a VM backend silently.bot-bottle doctor— new store-free subcommand (runs before any DB migration) that reports Python version, backend availability, and config-dir presence. It reusesis_backend_available()rather than hardcoding Docker, since the default backend is now a VM backend (macos-container / firecracker). Exits non-zero when a hard prerequisite is unmet.docs/prds/prd-new-install-script.mddocumenting the design (CI numbers it on merge).Testing
test_cli_doctor.py— doctor pass/fail paths (Python gate, backend availability, config-dir advisory vs. present).test_pyproject.py— entry point, empty dependency list,requires-python, and that everypackage-dataglob resolves to a real file (guards against drift).test_install_script.py— executable,#!/bin/sh+set -eu, never callssudo, creates the config tree, pipx-with-pip-fallback, runsdoctor.Verified locally: full unit suite green (2248 tests),
python -m build --wheelsucceeds and the wheel contains the entry point + all package-data assets, andpython cli.py doctorruns. pylint 10/10 and pyright clean on the new files.Follow-ups (out of scope)
🤖 Generated with Claude Code
I found two blocking issues:
High — installed wheels cannot run normal backends.
pyproject.tomlpackages only six files underbot_bottle/, while runtime code still assumes a source checkout exists above the installed package. Firecracker hashes root-level Dockerfiles andpyproject.toml; Docker/macOS build root-levelDockerfile.gateway; setup also references externalnix/andscripts/paths. Under pipx, the derived repository root is effectivelysite-packages, where these resources do not exist.doctorcan pass, butstartor backend setup then fails. Please add a clean wheel-install integration test and refactor resource lookup/packaging so the installed distribution is self-contained. Static source-tree existence tests do not exercise this boundary.Medium — the advertised
curl | shpath has undeclared prerequisites. The script checks only for Python, but the defaultgit+https://...spec requiresgit, while the fallback requires an available, user-installablepip. Minimal hosts frequently lack one or both, and externally managed Python installations may rejectpip install --user. Please detect these conditions with actionable guidance, or provide an installation route that does not require Git.The 16 new unit tests pass, but none builds, installs, and exercises the wheel from an isolated environment.
9b7df6e453tof0d78b5da1Thanks — both findings reproduced and fixed. Also rebased onto latest
main(the branch had fallen ~72 commits behind, and upstream had meanwhile split the orchestrator/gateway/infra into per-backend modules and moved firecracker to per-role infra artifacts with a newDockerfile.orchestrator.fc), so the routing below is applied against the current structure.1 — installed wheel can't run backends (High). Confirmed: the gateway/infra/orchestrator build context and the nix/scripts reads all resolved a repo root by walking
__file__'s parents, which issite-packagesunder a wheel. Fix:bot_bottle/resources.py— one resolver.build_root()returns the repo root in a checkout (unchanged) or, from an installed wheel, stages a repo-root-shaped context (bot_bottle/+pyproject.toml+ the root Dockerfiles +nix/+scripts/) under the app-data dir, once, keyed by version.dockerfile(),nix_netpool_module(),netpool_script()derive from it.setup.pybundles the root Dockerfiles (Dockerfile.gateway,Dockerfile.orchestrator,Dockerfile.orchestrator.fc), the nix module, the netpool script, andpyproject.tomlintobot_bottle/_resources/at build;MANIFEST.inships them in the sdist._REPO_ROOT/_REPO_DIRsite throughresources: the docker/macos agent-image launch, each backend'sorchestrator/gateway/infraservice, firecrackerinfra_vm/infra_artifact/setup, and the sharedgatewaybuild context. Checkout behavior is byte-identical (build_root()== repo root there), so the existing suite is unaffected.On the test you asked for:
test_wheel_installbuilds the wheel,pip installs it into an isolated venv, and from that env asserts thebot-bottleconsole script exists,doctorruns, andbuild_root()yields a context withbot_bottle/+pyproject.toml+Dockerfile.gateway.test_resourcescovers both the checkout and the staged-wheel layout directly. Honest caveat: this proves resource self-containment, not an end-to-end image build — runningstartneeds a Docker/KVM host, which I don't have here, so that path is left to CI.2 —
curl | shundeclared prerequisites (Medium).install.shnow checks forgitwhen the spec is agit+/.gitURL (the default), and — before the pip fallback — thatpython3 -m pipis usable and that the interpreter isn't externally managed (PEP 668, detected via theEXTERNALLY-MANAGEDmarker), pointing at pipx with an actionable message in each case.Full unit suite is green (2367 tests) on the rebased tree; pylint/pyright clean on the changed files.
f0d78b5da1to5a31c6f9b2The original two findings are addressed, but I found three issues in the replacement resource-staging path:
High — forced reinstalls keep using the previous staged source indefinitely.
resources._stage_build_root()keys the cache only by distribution version and immediately returns any existing.completedirectory. The installer defaults to a Git branch and usespipx install --force, whilepyproject.tomlremains at version0.1.0. Reinstalling a newer commit therefore installs new Python entry-point code but reuses~/.bot-bottle/build-root/0.1.0from the old commit. Docker/macOS then bind-mount and build the old package and Dockerfiles; Firecracker also hashes the stale context. Key this cache by a digest of the installed package/resources, validate a stored digest before reuse, or replace it on every install-safe staging operation. Add a regression test that changes bundled/package content without changing the distribution version and provesbuild_root()refreshes.Medium — concurrent first use can destroy another process’s staging tree. Every process uses the same
<version>.stagingpath, unconditionally removes it, copies into it, removesdest, and renames it. Two parallelbot-bottle startprocesses after install or upgrade can race: one removes files while the other copies, or one renames the shared staging directory before the other does. Use a per-process temporary directory plus atomic publication with locking or a winner/loser recheck; do not remove a shared staging path being populated by another process.Medium — the new wheel integration test does not run in normal CI and converts real build failures into skips.
test_wheel_install.pyskips unless the third-partybuildmodule already exists, butrequirements-dev.txtdoes not installbuild; in this checkout all three tests skip. Even when the module exists, a wheel build failure raisesSkipTest, so precisely the packaging regression this test should catch still leaves CI green. Install the build tool in the relevant CI/dev requirements and make wheel build/install failures fail the test. Only genuinely unsupported optional environments should skip.Relevant focused tests otherwise pass: 27 passed, with the 3 wheel tests skipped.
5a31c6f9b2to83ede8f6ecAll three addressed in
resources.py/test_wheel_install.py/requirements-dev.txt.1 — stale staged tree on force-reinstall (High). The stage is now keyed by a content digest of the installed package + bundled resources (
_content_digest()over_PKG, which contains_resources/), not the distribution version. Apipx install --forceof a newer commit at the same0.1.0changes the digest → new key → re-stage. Reuse only happens when the digest matches, so the stored tree is valid by construction. Regression testtest_refreshes_when_content_changes_at_same_versionmutates a package file without touching the version and assertsbuild_root()returns a fresh tree with the new content.test_rebuilds_when_stage_incompletecovers a crashed partial stage (missing.complete) being rebuilt rather than trusted.2 — concurrent staging race (Medium). Staging now: takes an exclusive
flockonbuild-root/.stage.lock, re-checks.completeunder the lock, builds into a privatetempfile.mkdtempdir, and publishes with an atomicos.replace— a shared<version>.stagingpath is never populated or removed out from under another process. Losers of the lock take the reuse path.test_reuses_peer_stage_after_lock_waitdrives this deterministically (hold the lock, let a worker block past its fast-path miss, publish a peer tree, release → worker reuses it).3 — wheel test skipped in CI / hid build failures (Medium).
build>=1.0.0is now inrequirements-dev.txt, which the unit CI job installs, sotest_wheel_installruns there. A wheel-build or install failure now fails (raisesAssertionError) instead ofSkipTest; only genuinely-unsupported infra (novenv/ensurepip) skips.Full unit suite green (2369) and diff-coverage 100% on the changed lines; pylint/pyright clean. Rebased head is
83ede8f.I found two remaining issues in the user-facing install path:
doctorreports a backend as OK when it is not ready to run a bottle._check_backends()callsis_backend_available(), whose contract explicitly says it is only a cheap binary/PATH check and "does NOT probe the daemon or network pool." Thus a host with a stopped/unreachable Docker daemon, or an incompletely configured Firecracker backend, getsok: backendand exit 0 even thoughstartcannot work. That conflicts with the PRD's motivation that this command tells a new user whether the host is actually ready.doctorshould useis_backend_ready()(ideally with diagnostics enabled), report readiness per backend, and succeed only if at least one backend passes its full status checks. The tests currently mock the cheap function and therefore lock in the false-positive behavior.The pip fallback assumes Linux's user-script directory and breaks on supported macOS installs. After
python3 -m pip install --user, the script checks PATH and then only${HOME}/.local/bin/bot-bottle. A python.org macOS interpreter normally installs user scripts under~/Library/Python/<major>.<minor>/bin, so installation succeeds and this script then incorrectly dies with "installed but is not on PATH" while pointing the user at the wrong directory. Resolve the interpreter's user scripts directory rather than hardcoding~/.local/bin(for example viasysconfig/site.USER_BASE), test that candidate, and print that actual directory in the PATH guidance. A test should cover a non-~/.local/binuser scheme rather than only asserting that installer text containspip install --user.I ran the targeted new unit suites against
83ede8f; 28 tests reached execution, with the wheel-install class failing locally only because this environment does not have the newly declaredbuilddev dependency. The two findings above come from the runtime paths not exercised by those static/mocked tests.Give bot-bottle a real distribution path so new users can install without cloning the repo: - pyproject.toml: full project metadata, a `bot-bottle` console-script entry point (bot_bottle.cli:main), and package-data for the runtime assets (Dockerfiles, egress entrypoint, netpool defaults, macos init). Still zero runtime pip dependencies. - install.sh: POSIX, sudo-free, idempotent bootstrapper — checks Python >= 3.11, creates ~/.bot-bottle/{agents,bottles,contrib}, installs via pipx (pip --user fallback), then runs `bot-bottle doctor`. - `bot-bottle doctor`: new store-free subcommand reporting Python version, backend availability (reuses is_backend_available rather than hardcoding Docker), and config-dir presence. Exits non-zero when a hard prerequisite is unmet. - PRD prd-new-install-script and unit tests for doctor, the packaging contract, and the install script. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>83ede8f6ecto10c46fc584Both addressed.
1 —
doctorreported a backend OK when it wasn't ready._check_backends()now usesis_backend_ready(name, quiet=False)— the full backendstatus()probe (daemon reachable, network pool present, KVM usable) — instead of the PATH-onlyis_backend_available(). A stopped Docker daemon or half-configured Firecracker no longer yieldsok: backend/exit 0; each not-ready backend prints its own diagnostics, anddoctorsucceeds only if at least one backend is actually ready. The tests were mocking the cheap function and locking in the false positive — they now mockis_backend_readyand include the available-but-not-ready → fail case (test_fails_when_no_backend_ready,test_passes_when_at_least_one_backend_ready). Live output on a host with no ready backend now ends infail: backend: no backend is ready to run a bottle …and exits non-zero.2 — pip fallback hardcoded
~/.local/bin.install.shnow resolves the interpreter's own user-scheme scripts dir —sysconfig.get_path("scripts", sysconfig.get_preferred_scheme("user"))— and tests that candidate, so a python.org macOS install under~/Library/Python/<X.Y>/binis found (and the PATH guidance prints that actual dir, not the wrong one). New testtest_macos_user_scheme_is_not_dot_local_bindrives theosx_framework_userscheme (with a mac-likeuserbase) and asserts it resolves…/Library/Python/3.11/bin, not~/.local/bin; another asserts the script no longer hardcodes.local/binand usesget_preferred_scheme.On the wheel test note: that's the intended new behavior —
build>=1.0.0is inrequirements-dev.txt(which the unit CI job installs), so it runs in CI; a build/install failure now fails rather than skips. Your local run at83ede8fskipped/failed only because that env predated the dep.Full unit suite green (2386) and diff-coverage 100% on the changed lines; pylint/pyright clean. Head is
beaaf84(rebased on currentmain).Confirmed at
beaaf84: both concerns are resolved.doctornow uses the fullis_backend_ready(..., quiet=False)status path, reports each backend's readiness, and requires at least one genuinely ready backend for success.The added regression cases described are the right coverage. No remaining objections from my review.
Approved. The review concerns are resolved at
beaaf84; no remaining blockers.Approved. The review concerns are resolved at
beaaf84; no remaining blockers.