From b1da14aeab31a5d6a491046635d8859e624ffb01 Mon Sep 17 00:00:00 2001 From: cmjang Date: Wed, 4 Nov 2026 01:13:23 -0500 Subject: [PATCH] fix: stop downloading the browser runtime from a private release 0.4.0 pinned a download URL to this repository's GitHub releases, but the repository is private, so a fresh install could not obtain the browser runtime and every `botlab run` / `build --site` failed after 404s. The browser runtime belongs to the BotLab frontend: it is built from a BotLab checkout and published there. The Python package now only consumes it and never hosts or downloads it by default: - `browser_runtime_url()` reads `BOTLAB_RUNTIME_URL` and returns None when it is unset, so there is no compiled-in fallback address. - A missing runtime stops the command with the two local installs (`ui sync --botlab `, `ui fetch --archive `) instead of an opaque download error; `ensure_browser_runtime()` no longer wraps that message in a second layer. - A configured mirror still works through `BOTLAB_RUNTIME_URL` or `ui fetch --url`, and download failures suggest the local paths. - README states the ownership boundary and stops pinning a version in the examples. Version 0.4.1. --- README.md | 58 ++++++++++++++++++---------------- pyproject.toml | 2 +- src/botlab/__init__.py | 2 +- src/botlab/_browser_runtime.py | 32 +++++++++++-------- src/botlab/_cli.py | 2 +- tests/test_browser_runtime.py | 16 +++++++++- uv.lock | 2 +- 7 files changed, 68 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 9825f98..d224e1d 100644 --- a/README.md +++ b/README.md @@ -151,12 +151,12 @@ project( | Command | Description | | --- | --- | -| `botlab run [DIR]` | Run in the browser: serve the complete editor (the platform web UI) from live sources; the browser executes the project and edits land on reload. `--native` executes locally instead (local MuJoCo + ONNX Runtime) with the browser as a read-only `?ws=` viewer. Neither writes a zip unless `--out` is provided. Supports `--verbose`, `--no-browser`, `--host`, and `--port`. | +| `botlab run [DIR]` | Run in the browser: serve the complete editor (the platform web UI) from live sources; the browser executes the project and edits land on reload. Requires the installed browser runtime (see [Display frontend and browser runtime](#display-frontend-and-browser-runtime)). `--native` executes locally instead (local MuJoCo + ONNX Runtime) with the browser as a read-only `?ws=` viewer. Neither writes a zip unless `--out` is provided. Supports `--verbose`, `--no-browser`, `--host`, and `--port`. | | `botlab build [DIR]` | Export `release.zip` to the project directory or a path selected with `--out`. The output uses the same format as BotLab's hosted `/d//` demos. `--site ` additionally assembles a self-contained static site (see below). | | `botlab validate [DIR]` | Collect declarations, compile the plan, and build the release in memory without starting a server. | | `botlab run [DIR] --headless` | Native headless run without the viewer server. Supports custom Python controllers and requires the `runtime` extra. | | `botlab ui sync --botlab ` | Synchronize the lightweight display frontend from `dist-viewer/` and cache the full browser runtime from `dist/` for local development. | -| `botlab ui fetch` | Fetch the version-matched browser runtime when needed; `--archive ` imports an offline archive and `--url ` selects an explicit download source. | +| `botlab ui fetch` | Install the browser runtime built by the BotLab frontend: `--archive ` imports an offline archive, `--url ` downloads from a mirror (or set `BOTLAB_RUNTIME_URL`). | | `botlab ui export --botlab --out ` | Export the separate version-matched browser runtime distribution from a BotLab build. | | `botlab ui status` | Show the status of the bundled UI snapshot. | | `botlab docs` | Print where the bundled docs (component reference, decision table, error guide) live in this install. | @@ -277,26 +277,29 @@ native MuJoCo process. The browser renders the scene and lets you orbit and zoom the camera; physics and inference remain in CPython. Native preview, `validate`, and plain `build` do not fetch a browser runtime. -Plain `run`, a served `run --out` preview, and `build --site` use a separate -full browser runtime archive. On first use the CLI fetches the archive pinned -to the installed package version from the matching GitHub release, verifies -its manifest and file hashes, and installs it in the user cache. Later uses -work offline. Download, version, or integrity errors stop the command and name -the failing archive; execution does not silently switch to another runtime. -The generated static site includes the needed WASM so visitors need no Python -installation and make no runtime download through the CLI. - -For offline machines, import the exact matching archive first: +Plain `run`, a served `run --out` preview, and `build --site` need a separate +full browser runtime archive. That archive is built and distributed by the +BotLab frontend, not by this package: the CLI never ships it and does not +download it unless a mirror is configured. Install it from a BotLab checkout or +from an archive the frontend team produced: ```bash -botlab ui fetch --archive botlab-browser-runtime-0.4.0.zip +botlab ui sync --botlab ../BotLab +botlab ui fetch --archive botlab-browser-runtime-.zip ``` -`botlab ui fetch --url ` allows a mirror. The default cache is -`${XDG_CACHE_HOME:-~/.cache}/botlab/browser/`; -`BOTLAB_RUNTIME_CACHE_DIR` overrides the browser cache root. The optional -runtime stays outside the installed Python package. `botlab ui status` shows -the bundled display and cached browser runtime separately. +The installed archive is verified against its manifest and file hashes and then +cached for offline use. Missing, version-mismatched, or corrupt runtimes stop +the command and name what to install; execution never silently switches to +another runtime. The generated static site includes the needed WASM so visitors +need no Python installation and make no runtime download through the CLI. + +`botlab ui fetch --url ` accepts an explicit mirror, and +`BOTLAB_RUNTIME_URL` sets the default download mirror for a team that hosts one. +The default cache is `${XDG_CACHE_HOME:-~/.cache}/botlab/browser/`; +`BOTLAB_RUNTIME_CACHE_DIR` overrides the browser cache root. The runtime stays +outside the installed Python package. `botlab ui status` shows the bundled +display and cached browser runtime separately. To generate both distributions from the source checkouts: @@ -307,17 +310,16 @@ npm run build npm run build:viewer # In botlab-python: botlab ui sync --botlab ../BotLab -botlab ui export --botlab ../BotLab --out tmp/botlab-browser-runtime-0.4.0.zip +botlab ui export --botlab ../BotLab --out tmp/botlab-browser-runtime-.zip uv build --wheel --out-dir tmp/core-wheel ``` The generated display frontend is ignored by Git and bundled in the core -wheel. The exported runtime archive is a separate release asset named -`botlab-browser-runtime-.zip`; attach it to the matching -`v` GitHub release alongside publishing the Python wheel. Until that -asset is published, use `ui sync`, `ui fetch --archive`, or an explicit mirror. -Runtime archives exclude built-in demo apps, templates, and shared models; -project assets continue to be served from `/project/`. +wheel. The exported runtime archive is the frontend distribution named +`botlab-browser-runtime-.zip`: the BotLab frontend publishes it and +`botlab-python` only consumes it (`ui sync`, `ui fetch --archive`, or a +configured mirror URL). Runtime archives exclude built-in demo apps, templates, +and shared models; project assets continue to be served from `/project/`. ## Runtime model @@ -386,9 +388,9 @@ its real synchronized display frontend. Verify the separate browser runtime archive used for website assembly too. From this checkout, with `uv` on PATH: ```bash -python scripts/verify_wheel.py dist/pypi-0.4.0/botlab_python-0.4.0-py3-none-any.whl \ - --browser-runtime tmp/botlab-browser-runtime-0.4.0.zip \ - --out tmp/wheel-acceptance-0.4.0 +python scripts/verify_wheel.py dist/pypi-/botlab_python--py3-none-any.whl \ + --browser-runtime tmp/botlab-browser-runtime-.zip \ + --out tmp/wheel-acceptance- ``` Use a new output directory for each run. The command installs that wheel with diff --git a/pyproject.toml b/pyproject.toml index e45ac89..bbd6918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "botlab-python" -version = "0.4.0" +version = "0.4.1" description = "Python-first robot simulation and control in the browser." readme = "README.md" requires-python = ">=3.11" diff --git a/src/botlab/__init__.py b/src/botlab/__init__.py index 7fe4835..b1941af 100644 --- a/src/botlab/__init__.py +++ b/src/botlab/__init__.py @@ -194,4 +194,4 @@ "where", ] -__version__ = "0.4.0" +__version__ = "0.4.1" diff --git a/src/botlab/_browser_runtime.py b/src/botlab/_browser_runtime.py index 49e7cdc..a42eee5 100644 --- a/src/botlab/_browser_runtime.py +++ b/src/botlab/_browser_runtime.py @@ -25,11 +25,14 @@ _EXCLUDED = {"d", "models", "templates"} -def browser_runtime_url() -> str: - return ( - "https://github.com/OpenLegged/botlab-python/releases/download/" - f"v{__version__}/botlab-browser-runtime-{__version__}.zip" - ) +def browser_runtime_url() -> str | None: + """Optional download mirror; this package never ships or hosts the runtime. + + The browser runtime is built and distributed by the BotLab frontend. Point + ``BOTLAB_RUNTIME_URL`` at a mirror when automatic download is convenient; + otherwise install the archive produced by that checkout. + """ + return os.environ.get("BOTLAB_RUNTIME_URL") or None def browser_runtime_cache_dir() -> Path: @@ -229,6 +232,14 @@ def fetch_browser_runtime(*, url: str | None = None, archive: Path | None = None if archive is not None: return install_browser_runtime(archive) source_url = url or browser_runtime_url() + if source_url is None: + raise BotLabError( + f"Browser runtime v{__version__} is not installed and no download source is configured. " + "The runtime is built by the BotLab frontend, not shipped with this package: use " + "`botlab ui sync --botlab ` or " + "`botlab ui fetch --archive `, " + "or set BOTLAB_RUNTIME_URL to download from a mirror." + ) cache_root = browser_runtime_cache_dir().parent cache_root.mkdir(parents=True, exist_ok=True) fd, path = tempfile.mkstemp(prefix=".download-", suffix=".zip", dir=cache_root) @@ -246,7 +257,8 @@ def fetch_browser_runtime(*, url: str | None = None, archive: Path | None = None except (urllib.error.URLError, TimeoutError, OSError) as error: raise BotLabError( f"Could not download browser runtime v{__version__} from {source_url}: {error}. " - "Use `botlab ui fetch --archive ` for an offline archive." + "Use `botlab ui sync --botlab ` or " + "`botlab ui fetch --archive ` instead." ) from error return install_browser_runtime(download) finally: @@ -257,10 +269,4 @@ def ensure_browser_runtime() -> Path: target = browser_runtime_cache_dir() if _validate_cache(target): return target - try: - return fetch_browser_runtime() - except BotLabError as error: - raise BotLabError( - f"Browser runtime v{__version__} is unavailable: {error} " - "Run `botlab ui fetch --archive ` or `botlab ui sync --botlab `." - ) from error + return fetch_browser_runtime() diff --git a/src/botlab/_cli.py b/src/botlab/_cli.py index c4ed5eb..0723dfa 100644 --- a/src/botlab/_cli.py +++ b/src/botlab/_cli.py @@ -572,7 +572,7 @@ def build_parser() -> argparse.ArgumentParser: ui.add_argument("--botlab", default="~/Project/BotLab", help="path to a BotLab checkout") ui.add_argument("--out", help="versioned runtime archive output for ui export") ui.add_argument("--archive", help="offline runtime archive to install for ui fetch") - ui.add_argument("--url", help="runtime archive URL override for ui fetch") + ui.add_argument("--url", help="download the runtime archive from this mirror URL") ui.set_defaults(func=_cmd_ui) target = subparsers.add_parser("target", help="manage HIL target devices") diff --git a/tests/test_browser_runtime.py b/tests/test_browser_runtime.py index 78594ff..aec09fe 100644 --- a/tests/test_browser_runtime.py +++ b/tests/test_browser_runtime.py @@ -113,6 +113,8 @@ def test_ensure_downloads_once_and_reuses_verified_cache(tmp_path: Path, monkeyp from botlab import _browser_runtime monkeypatch.setenv("BOTLAB_RUNTIME_CACHE_DIR", str(tmp_path / "cache")) + mirror = "https://mirror.example.test/botlab/browser-runtime.zip" + monkeypatch.setenv("BOTLAB_RUNTIME_URL", mirror) _dist(tmp_path) archive = tmp_path / "runtime.zip" export_browser_runtime(tmp_path / "BotLab", archive) @@ -127,7 +129,19 @@ def fake_open(url, timeout): assert (target / "assets/mujoco.wasm").read_bytes() == b"runtime" assert ensure_browser_runtime() == target assert len(downloaded) == 1 - assert f"v{__version__}" in downloaded[0][0] + assert downloaded[0][0] == mirror + + +def test_missing_runtime_without_a_source_names_both_local_installs(tmp_path: Path, monkeypatch): + monkeypatch.setenv("BOTLAB_RUNTIME_CACHE_DIR", str(tmp_path / "cache")) + monkeypatch.delenv("BOTLAB_RUNTIME_URL", raising=False) + with pytest.raises(BotLabError) as caught: + ensure_browser_runtime() + message = str(caught.value) + assert f"v{__version__}" in message + assert "ui sync --botlab" in message + assert "ui fetch --archive" in message + assert "BOTLAB_RUNTIME_URL" in message def test_failed_cache_swap_restores_previous_runtime(tmp_path: Path, monkeypatch): diff --git a/uv.lock b/uv.lock index a1cf001..77ab7ce 100644 --- a/uv.lock +++ b/uv.lock @@ -210,7 +210,7 @@ wheels = [ [[package]] name = "botlab-python" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "pyyaml" },