Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 30 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/` demos. `--site <dir>` 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 <path>` | 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 <file>` imports an offline archive and `--url <url>` selects an explicit download source. |
| `botlab ui fetch` | Install the browser runtime built by the BotLab frontend: `--archive <file>` imports an offline archive, `--url <url>` downloads from a mirror (or set `BOTLAB_RUNTIME_URL`). |
| `botlab ui export --botlab <path> --out <file.zip>` | 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. |
Expand Down Expand Up @@ -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-<version>.zip
```

`botlab ui fetch --url <url>` allows a mirror. The default cache is
`${XDG_CACHE_HOME:-~/.cache}/botlab/browser/<version>`;
`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 <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/<version>`;
`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:

Expand All @@ -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-<version>.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-<version>.zip`; attach it to the matching
`v<version>` 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-<version>.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

Expand Down Expand Up @@ -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-<version>/botlab_python-<version>-py3-none-any.whl \
--browser-runtime tmp/botlab-browser-runtime-<version>.zip \
--out tmp/wheel-acceptance-<version>
```

Use a new output directory for each run. The command installs that wheel with
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion src/botlab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,4 @@
"where",
]

__version__ = "0.4.0"
__version__ = "0.4.1"
32 changes: 19 additions & 13 deletions src/botlab/_browser_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 <BotLab checkout>` or "
"`botlab ui fetch --archive <botlab-browser-runtime.zip>`, "
"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)
Expand All @@ -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 <path>` for an offline archive."
"Use `botlab ui sync --botlab <BotLab checkout>` or "
"`botlab ui fetch --archive <botlab-browser-runtime.zip>` instead."
) from error
return install_browser_runtime(download)
finally:
Expand All @@ -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 <path>` or `botlab ui sync --botlab <checkout>`."
) from error
return fetch_browser_runtime()
2 changes: 1 addition & 1 deletion src/botlab/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 15 additions & 1 deletion tests/test_browser_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading