velxio/Dockerfile.standalone

298 lines
13 KiB
Docker
Raw Normal View History

# ---- Stage 0: QEMU .so + ROM binaries ----
# The Velxio runtime needs libqemu-xtensa.so + libqemu-riscv32.so (the
# QEMU shared libraries that simulate ESP32 / ESP32-S3 / ESP32-C3) plus
# the matching boot ROM blobs. Three sources, tried in order:
#
# 1. Local prebuilt files at prebuilt/qemu/<file>.
# Drop your own compiled .so files in there (see docs/BUILD-QEMU.md
# for the full build-from-source guide) and they win — no network
# access required.
#
# 2. velxio.dev gated download endpoint, when VELXIO_LICENSE_KEY is set.
# Free personal-tier keys at https://velxio.dev/license/signup.
#
# 3. Build fails with a clear error telling you which of the two paths
# to pick.
#
# Backward compatibility: the old QEMU_RELEASE_URL ARG is still accepted
# so forks pointing at a private mirror of the binaries keep working.
FROM ubuntu:22.04 AS qemu-provider
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ARG TARGETARCH
ARG VELXIO_LICENSE_KEY=
ARG VELXIO_BINARY_BASE_URL=https://velxio.dev/api/pro/license/downloads
# Legacy escape hatch — set this to keep using the old GitHub Release
# mirror (or any other CDN you proxy from). When set, takes precedence
# over the gated path.
ARG QEMU_RELEASE_URL=
# Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep)
RUN mkdir -p /qemu
COPY prebuilt/qemu/ /qemu/
# Resolve which fetch path the build will use and fail-fast with a
# friendly message if neither prebuilt files nor a key were provided.
RUN cd /qemu \
&& have_libs=1 && for base in libqemu-xtensa libqemu-riscv32; do \
[ -f "${base}.so" ] || have_libs=0 ; \
done \
&& if [ "$have_libs" = "1" ]; then \
echo "[qemu-provider] using local prebuilt/qemu/ files — no download" ; \
elif [ -n "${QEMU_RELEASE_URL}" ]; then \
echo "[qemu-provider] using legacy QEMU_RELEASE_URL: ${QEMU_RELEASE_URL}" ; \
elif [ -n "${VELXIO_LICENSE_KEY}" ]; then \
echo "[qemu-provider] using velxio.dev gated download with provided license key" ; \
else \
echo "" ; \
echo "ERROR: Velxio docker build needs the QEMU runtime libraries." ; \
echo "" ; \
echo "Pick one:" ; \
echo " a) Free personal key from https://velxio.dev/license/signup ," ; \
echo " then re-build with --build-arg VELXIO_LICENSE_KEY=vlx_personal_..." ; \
echo " b) Build QEMU yourself (see docs/BUILD-QEMU.md) and drop the .so" ; \
echo " files plus the three esp32*-rom.bin files into prebuilt/qemu/." ; \
echo "" ; \
exit 1 ; \
fi
# Download arch-specific .so and arch-independent ROM files.
# The gated endpoint serves the same byte-for-byte content as the legacy
# GitHub Release path; asset paths just drop the .so extension since the
# license module's manifest carries the real filename.
RUN cd /qemu \
&& for base in libqemu-xtensa libqemu-riscv32; do \
f="${base}.so" ; \
if [ -f "$f" ]; then \
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
elif [ -n "${QEMU_RELEASE_URL}" ]; then \
echo "Downloading ${base}-${TARGETARCH}.so → $f (legacy URL) ..." ; \
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/${base}-${TARGETARCH}.so" ; \
else \
echo "Downloading ${base}-${TARGETARCH}$f (velxio.dev) ..." ; \
curl -fSL -o "$f" "${VELXIO_BINARY_BASE_URL}/${base}-${TARGETARCH}?key=${VELXIO_LICENSE_KEY}" ; \
fi ; \
done \
&& for f in esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \
asset="${f%.bin}" ; \
if [ -f "$f" ]; then \
echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
elif [ -n "${QEMU_RELEASE_URL}" ]; then \
echo "Downloading $f (legacy URL) ..." ; \
curl -fSL -o "$f" "${QEMU_RELEASE_URL}/$f" ; \
else \
echo "Downloading $asset$f (velxio.dev) ..." ; \
curl -fSL -o "$f" "${VELXIO_BINARY_BASE_URL}/${asset}?key=${VELXIO_LICENSE_KEY}" ; \
fi ; \
done \
&& ls -lh /qemu/
# ---- Stage 0.5: ESP-IDF toolchain for ESP32 compilation ----
FROM ubuntu:22.04 AS espidf-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
git wget flex bison gperf python3 python3-pip python3-venv \
cmake ninja-build ccache libffi-dev libssl-dev \
libusb-1.0-0 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install ESP-IDF 4.4.7 (matches Arduino ESP32 core 2.0.17 / lcgamboa QEMU ROM)
RUN git clone -b v4.4.7 --recursive --depth=1 --shallow-submodules \
https://github.com/espressif/esp-idf.git /opt/esp-idf
WORKDIR /opt/esp-idf
# Install toolchains for esp32 (Xtensa) and esp32c3 (RISC-V) only
RUN ./install.sh esp32,esp32c3
# Clean up large unnecessary files to reduce image size
RUN rm -rf .git docs examples \
&& find /root/.espressif -name '*.tar.*' -delete 2>/dev/null || true
# Install Arduino-as-component for full Arduino API support in ESP-IDF builds
RUN git clone --branch 2.0.17 --depth=1 --recursive --shallow-submodules \
https://github.com/espressif/arduino-esp32.git /opt/arduino-esp32 \
&& rm -rf /opt/arduino-esp32/.git
refactor: rename wokwi-libs/ → third-party/ The directory grew well beyond Wokwi-only contents: it now hosts lcgamboa's QEMU fork (qemu-lcgamboa), Espressif's esp32-camera, the ngspice WASM build, fritzing-parts, picowi, an alternative QEMU (qemu-esp32), the 100_Days_100_IoT_Projects examples repo, and Wokwi's own avr8js/rp2040js/wokwi-elements/wokwi-features/wokwi-boards. "wokwi-libs" was misleading — half the contents have nothing to do with Wokwi. "third-party/" is the standard convention for vendored external dependencies. Mechanical changes: Path rename: wokwi-libs/ → third-party/ update-wokwi-libs.bat → update-third-party.bat docs/WOKWI_LIBS.md → docs/THIRD_PARTY.md Submodule reconfiguration: .gitmodules — 4 path= and section names updated .git/modules/wokwi-libs/ → .git/modules/third-party/ each submodule's .git file rewired to ../../.git/modules/third-party/<name> Reference updates (~80 files): vite.config.ts aliases, Dockerfile COPY paths, GH Actions workflow steps, build_qemu_*.sh, all docs/* and test/*/autosearch/* entries that mention the path, package-lock.json file: dependencies, .gitignore patterns, sitemap.xml + index.html SEO blurbs, scripts/generate-component-*, .dockerignore, .idea/vcs.xml. Bulk replaced both `wokwi-libs/` (path) and bare `wokwi-libs` (textual mentions in docs/comments). Verified: - npx tsc -b --noEmit produces no new errors related to these paths - vite.config.ts aliases now point at ../third-party/avr8js etc. - All 4 git submodules (avr8js, rp2040js, wokwi-elements, wokwi-features) are linked under third-party/ with their worktrees re-populated and config files referencing the new path - `grep -r wokwi-libs` returns zero hits outside node_modules, .vite, frontend/dist, third-party/ (upstream submodule contents), *.pyc caches, and *.dll.pre-camera rollback binaries Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:58:57 +07:00
# ---- Stage 1: Build frontend and third-party ----
FROM node:20 AS frontend-builder
WORKDIR /app
fix(install): unblock self-hosting + drop forced wokwi clones Resolves several install pain points reported by users (#108, #120) and removes the obligatory upstream-clone step that confused contributors and slowed down every Docker build. Install fixes: - nginx: server_name → catch-all default_server, drop Debian's stock site so reverse-proxied users no longer get the "Welcome to nginx" page. - entrypoint: auto-generate SECRET_KEY at first boot, persisted under data/.secret_key. backend/.env is now optional in docker-compose.yml. - backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on some Python builds — caused uvicorn startup failures on WSL). Wokwi libs come from npm: - @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in frontend/package.json. Vite aliases removed. - Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements / wokwi-boards. Frontend stage is just COPY + npm install + build:docker. - Board SVGs vendored under frontend/public/boards/ (10 deduped against existing files, 2 truly new). third-party/wokwi-* clones become reference- only credits — generate-component-metadata.ts skips gracefully when absent. Production config split out: - docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf, update-third-party.bat removed. Production deployment lives in its own repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups + pinned upstream commit). Verified locally: 1161 frontend tests pass, build:docker completes clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:04:11 +07:00
# avr8js, rp2040js and @wokwi/elements are pulled directly from the npm
# registry (see frontend/package.json) — no upstream git clones needed.
# Board SVGs live in frontend/public/boards/, component SVGs in
# frontend/public/component-svgs/, and components-metadata.json is committed.
COPY frontend/ frontend/
COPY scripts/ scripts/
WORKDIR /app/frontend
# Lock files aren't committed in this repo (they're gitignored) — see the
# note in .gitignore. The `rm -f` below is defense-in-depth in case
# someone runs `docker build .` from a tree where a local lock exists.
RUN rm -f package-lock.json \
&& npm install --include=optional \
&& npm run build:docker
# ---- Stage 2: Final Production Image ----
FROM python:3.12-slim
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
# Install system dependencies, nginx, and QEMU runtime.
#
# qemu-system-arm + qemu-utils provide qemu-system-aarch64 and qemu-img,
# both invoked by app/services/qemu_manager.py for Raspberry Pi 3
# simulation. They were missing for the entire 2024-2026 stretch when
# Pi 3 simulation was advertised but broken — see docs/BOOT_IMAGES.md.
# ~200 MB added to the image; trade-off is "Pi 3 actually works".
#
# Other libs (libglib2.0-0, libgcrypt20, libslirp0, libpixman-1-0,
# libfdt1) used to be needed only as runtime deps for libqemu-xtensa.so;
# they're still needed but now also pulled in transitively by
# qemu-system-arm.
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
nginx \
libglib2.0-0 \
libgcrypt20 \
libslirp0 \
libpixman-1-0 \
libfdt1 \
cmake \
ninja-build \
libusb-1.0-0 \
git \
perf(espidf): drop in ccache for ESP32 compiles (~10× warm speedup) Cold first compile per container is unchanged (cache empty). Subsequent compiles drop from ~5-7 minutes to ~30-60 seconds because every ESP-IDF base object (FreeRTOS, lwIP, esp_wifi, libsodium, soc, hal, …) hits the cache. The user's BMP280 example, which hangs on cold compile, completes near-instantly on the second attempt. Why a transparent cache is safe: ccache hashes the preprocessed source + flags + compiler. A cache hit only happens when the input is byte-for-byte identical to a prior compile. Different sketches with different libraries still get correct cache misses; there is no path where one project's output contaminates another. Changes - Dockerfile.standalone: install ccache, set CCACHE_DIR=/var/cache/ccache, IDF_CCACHE_ENABLE=1, configure 2 GB cap with compression. Compression (level 6) cuts cache disk usage by ~40% with negligible CPU overhead. - docker-compose.yml: named volume `ccache:/var/cache/ccache` so the cache survives `docker compose up -d --build` (without it, every image rebuild discards the cache). - backend/app/services/espidf_compiler.py: pass `-DCCACHE_ENABLE=1` to cmake when IDF_CCACHE_ENABLE is truthy. ESP-IDF's project.cmake (`set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)` on line 374) is what actually wires ccache in; without the cmake -D flag the env var alone has no effect because we don't go through idf.py. Escape hatch: set IDF_CCACHE_ENABLE=0 in compose env to disable without rebuilding the image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:44:29 +07:00
ccache \
feat(sim): boot_images module + Pi 3 emulation restored Pi 3 simulation had been broken since at least April 2026 (51 fail-events / 24h per docs/PI3_EMULATION_BROKEN.md). Two distinct defects compounded: 1. qemu_manager.py hard-coded paths for kernel8.img, a device-tree blob (under a DOS 8.3 short name!), and a 5.4 GiB Raspberry Pi OS SD image — none of which shipped in the repo or were pulled at image build. 2. qemu-system-arm + qemu-utils were missing from the Docker image entirely, so even with the boot files in place QEMU couldn't launch. Add both to Dockerfile.standalone (~200 MB). The architecture fix is a new `app.services.boot_images` module: * Manifest-driven (boot_images/manifest.json, versioned in repo, declares SHA256 + size for each file, supports an optional `compressed.{encoding,sha256,size_bytes}` block for assets shipped as .zst). * `BootImageProvider` materialises files lazily, atomically (temp + rename), verifies SHA256 pre- AND post-decompression, caches under /var/cache/velxio/boot-images, serialises concurrent get() calls per image set via asyncio.Lock. * `AssetDownloader` Protocol with two impls: - `LicenseGatedDownloader` — same flow ESP32 / RISC-V QEMU libs use (VELXIO_BINARY_BASE_URL + VELXIO_LICENSE_KEY). - `LocalDirectoryDownloader` — for tests + in-prod use where the licence-module storage is already on the same filesystem (saves the loopback HTTP roundtrip on a 1.4 GiB blob). * `build_downloader_from_env()` picks one — local-dir wins if both sets of env vars are present, so the prod box short-circuits to direct disk reads automatically. * Lifespan hook in qemu_manager.py pre-warms the cache on container boot so first-time user requests don't pay the 30-60 s download + decompress latency. Adding a future board kind (Pi 4 / Pi 5) is now: upload assets via upload-binary.sh, append an entry to manifest.json, register a lifespan pre-warm in the new board's service module. Zero edits to provider.py / downloader.py. Manifest entries for raspberry-pi-3: kernel8.img 9 695 883 bytes (uncompressed) bcm2710-rpi-3-b.dtb 34 687 bytes (uncompressed) raspios-trixie-armhf.img 5 729 419 264 bytes raw / 1 488 002 803 bytes .zst on wire (zstd -19) source: 2026-04-21 build from raspberrypi.com Tests: 21 new unit tests covering manifest parsing, integrity helpers, both downloaders, and the provider's idempotent / concurrent / integrity / decompression / warmup paths. In-process FakeDownloader keeps the suite under 1 s and httpx-free. Docs: new docs/BOOT_IMAGES.md describes the architecture, on-disk layout, named-volume operation, and the procedure for adding a new image set.
2026-05-16 10:41:01 +07:00
qemu-system-arm \
qemu-utils \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir packaging
# Install arduino-cli into /usr/local/bin directly (avoids touching /bin)
RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \
| BINDIR=/usr/local/bin sh
# Only install arduino-cli binary here. Core installation (arduino:avr,
# rp2040:rp2040) is done at first boot by entrypoint.sh and persisted
# in the mounted /root/.arduino15 volume.
# ESP32 compilation uses ESP-IDF instead of arduino-cli.
WORKDIR /app
# Data directory for persistent SQLite database (mounted as a volume at runtime)
RUN mkdir -p /app/data
# Install Python backend dependencies
COPY backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy backend application code
COPY backend/app/ ./app/
feat: persist multi-board projects + add auto-save The project save/load pipeline only persisted a single `board_type`, so multi-board workspaces silently lost every board except the active one on save, and wires referencing the dropped boards' IDs orphaned to the canvas corner on reload. An audit of the production backup found 74/306 projects (24%) with at least one orphaned wire and 174/301 non-trivial projects whose code was still the default Blink template — strong signal that users save once and never re-save. Backend - Add `boards_json` column on `projects` with idempotent ALTER TABLE in the lifespan migration list. - New `FileGroup` schema + `file_groups` array on ProjectCreate/Update/Response. Legacy `files`/`code` kept for back-compat. - `project_files.py` now uses `{pid}/{groupId}/{filename}` subdirs via `read_groups`/`write_groups`. Legacy flat layouts are auto-promoted on read; legacy single-list `files` only updates the active group, leaving other boards' files intact. - `_persist_files_from_body` honors file_groups → files → code priority. Frontend - `useSimulatorStore.addBoard` accepts an optional `explicitId` so saved board IDs can be restored verbatim (wires reference IDs literally). - New `loadProjectState({boards, fileGroups, components, wires, activeBoardId})` action: tears down current boards, recreates from the payload, restores file groups atomically, recalculates wire positions on the next frame, and refreshes the Interconnect. - `useEditorStore.replaceFileGroups` for atomic multi-group restore. - `SaveProjectModal` and `ProjectByIdPage`/`ProjectPage` now go through `buildSavePayload` / `buildLoadPayload` (handles pre-backfill projects by synthesising a default board from `board_type`). Auto-save (#useAutoSaveProject hook) - 2.5s debounced silent PUT triggered ONLY when an authenticated user has a `currentProject` with a UUID. State hash detects real changes vs. UI-only churn; baseline is reset on project load so the just-loaded state isn't immediately re-saved. - `beforeunload` flush via `fetch keepalive: true` (supports PUT + credentials, survives unload). - Compact status indicator in `AppHeader` (idle/dirty/saving/saved/error). Backfill script (one-off, idempotent) - `backend/scripts/backfill_boards_2026_05.py` populates `boards_json` for legacy projects. Heuristic per project, based on which board IDs the wires reference: Case A — wires only ref 'arduino-uno' but board_type ≠ uno: rename id→board_type and rewrite wire endpoints. Case B — single-board normal: keep verbatim. Case C — multi-board: recreate one board per distinct ref, infer kind by stripping trailing -N suffix. Also moves any flat files into the active board's group subdir. Stdlib-only, runs from host or `docker exec`. Docker - `Dockerfile.standalone` now copies `backend/scripts/` into the image so the backfill is callable via `docker exec velxio-app python /app/scripts/backfill_boards_2026_05.py --apply`. Verified locally on the restored production backup (363 projects): 33 Case A, 316 Case B, 14 Case C, 135 wire endpoints renamed, 0 orphans. Re-running the script after apply skips all 363 (idempotent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:43:33 +07:00
# One-off maintenance scripts (e.g. backfill_boards_2026_05). Pure stdlib —
# run with: docker exec velxio-app python /app/scripts/<script> --apply
COPY backend/scripts/ ./scripts/
fix(install): unblock self-hosting + drop forced wokwi clones Resolves several install pain points reported by users (#108, #120) and removes the obligatory upstream-clone step that confused contributors and slowed down every Docker build. Install fixes: - nginx: server_name → catch-all default_server, drop Debian's stock site so reverse-proxied users no longer get the "Welcome to nginx" page. - entrypoint: auto-generate SECRET_KEY at first boot, persisted under data/.secret_key. backend/.env is now optional in docker-compose.yml. - backend: add greenlet>=3.0.0 (SQLAlchemy async dep that was missing on some Python builds — caused uvicorn startup failures on WSL). Wokwi libs come from npm: - @wokwi/elements 1.9.2, avr8js 0.21.0, rp2040js 1.3.2 are pinned in frontend/package.json. Vite aliases removed. - Dockerfile.standalone no longer clones avr8js / rp2040js / wokwi-elements / wokwi-boards. Frontend stage is just COPY + npm install + build:docker. - Board SVGs vendored under frontend/public/boards/ (10 deduped against existing files, 2 truly new). third-party/wokwi-* clones become reference- only credits — generate-component-metadata.ts skips gracefully when absent. Production config split out: - docker-compose.prod.yml, deploy/nginx.prod.conf, nginx-host-velxio*.conf, update-third-party.bat removed. Production deployment lives in its own repo: https://github.com/velxio/velxio-prod (host nginx + HTTPS + backups + pinned upstream commit). Verified locally: 1161 frontend tests pass, build:docker completes clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:04:11 +07:00
# Setup Nginx configuration. Remove Debian's stock site so it doesn't shadow
# ours as the default_server (was Issue #108: users behind reverse proxies got
# the "Welcome to nginx" page because the stock site claimed default_server).
RUN rm -f /etc/nginx/sites-enabled/default
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# Copy built frontend assets from builder stage
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
# Copy and configure entrypoint script (fix Windows CRLF → LF)
COPY docker/entrypoint.sh /app/entrypoint.sh
RUN sed -i 's/\r$//' /app/entrypoint.sh && chmod +x /app/entrypoint.sh
# ── ESP32 emulation: pre-built QEMU .so + ROM binaries ──────────────────────
# Downloaded from GitHub Release (public — no access to qemu-lcgamboa needed)
# libqemu-xtensa.so → ESP32 / ESP32-S3 (Xtensa LX6/LX7)
# libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC)
# esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine
# esp32c3-rom.bin → ROM image required by esp32c3-picsimlab machine
# NOTE: ROM files must live in the same directory as the .so (worker passes -L
# to QEMU pointing at os.path.dirname(lib_path))
RUN mkdir -p /app/lib
COPY --from=qemu-provider /qemu/ /app/lib/
# Activate ESP32 emulation
# QEMU_ESP32_LIB → Xtensa library (ESP32, ESP32-S3)
# QEMU_RISCV32_LIB → RISC-V library (ESP32-C3 and variants)
ENV QEMU_ESP32_LIB=/app/lib/libqemu-xtensa.so
ENV QEMU_RISCV32_LIB=/app/lib/libqemu-riscv32.so
# ── ESP-IDF toolchain for ESP32 compilation ──────────────────────────────────
# Copied from espidf-builder stage: IDF framework + cross-compiler toolchains
COPY --from=espidf-builder /opt/esp-idf /opt/esp-idf
COPY --from=espidf-builder /root/.espressif /root/.espressif
COPY --from=espidf-builder /opt/arduino-esp32 /opt/arduino-esp32
ENV IDF_PATH=/opt/esp-idf
ENV IDF_TOOLS_PATH=/root/.espressif
ENV ARDUINO_ESP32_PATH=/opt/arduino-esp32
perf(espidf): drop in ccache for ESP32 compiles (~10× warm speedup) Cold first compile per container is unchanged (cache empty). Subsequent compiles drop from ~5-7 minutes to ~30-60 seconds because every ESP-IDF base object (FreeRTOS, lwIP, esp_wifi, libsodium, soc, hal, …) hits the cache. The user's BMP280 example, which hangs on cold compile, completes near-instantly on the second attempt. Why a transparent cache is safe: ccache hashes the preprocessed source + flags + compiler. A cache hit only happens when the input is byte-for-byte identical to a prior compile. Different sketches with different libraries still get correct cache misses; there is no path where one project's output contaminates another. Changes - Dockerfile.standalone: install ccache, set CCACHE_DIR=/var/cache/ccache, IDF_CCACHE_ENABLE=1, configure 2 GB cap with compression. Compression (level 6) cuts cache disk usage by ~40% with negligible CPU overhead. - docker-compose.yml: named volume `ccache:/var/cache/ccache` so the cache survives `docker compose up -d --build` (without it, every image rebuild discards the cache). - backend/app/services/espidf_compiler.py: pass `-DCCACHE_ENABLE=1` to cmake when IDF_CCACHE_ENABLE is truthy. ESP-IDF's project.cmake (`set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)` on line 374) is what actually wires ccache in; without the cmake -D flag the env var alone has no effect because we don't go through idf.py. Escape hatch: set IDF_CCACHE_ENABLE=0 in compose env to disable without rebuilding the image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:44:29 +07:00
# ── ccache for ESP-IDF compiles ──────────────────────────────────────────────
# ESP-IDF's build system honours IDF_CCACHE_ENABLE=1 and routes every C/C++
# compile through ccache. Cold first compile per container is unchanged
# (cache is empty), but the second and subsequent compiles drop from
# ~5-7 minutes to ~30-60 seconds because every ESP-IDF base object
# (FreeRTOS, lwIP, esp_wifi, libsodium, …) hits the cache.
#
# Cache lives at /var/cache/ccache. Mount as a docker volume in
# docker-compose.yml so the cache survives `docker compose up -d --build`.
# Without the volume, the cache rebuilds itself on first compile after each
# image rebuild — still better than no cache.
ENV CCACHE_DIR=/var/cache/ccache
ENV IDF_CCACHE_ENABLE=1
perf(compile): dedup, concurrency limits, and persistent build dir for ESP-IDF Three coordinated fixes that together close the "ESP-IDF compile takes 5-7 min every time" gap and prevent the failure mode where a user clicking compile multiple times spawns six ninja processes that peel each other apart on a modest VPS. What was wrong - /compile/start generated a fresh uuid4 every call, so 6 clicks = 6 independent builds racing each other. Saw load average 30 on the prod VPS during a real BMP280 attempt today. - No concurrency limit anywhere; asyncio.create_task() fired without gating. - ccache was wired in last week (PR #149) but reported 18,350 cacheable calls and **0 hits** because the build dir was a fresh tempfile.TemporaryDirectory(prefix='espidf_') per compile. The random /tmp/espidf_<random>/ path baked into -I and -fmacro-prefix-map flags → different command line every compile → ccache hash miss every time. What this PR does 1. Job deduplication (`backend/app/api/routes/compile.py`) - New `_job_key(files, board_fqbn)` returns SHA-256 of normalised file names + contents + board. Order-independent. - New `JOB_BY_KEY: dict[str, str]` indexes hash → job_id. - `compile_start` checks JOB_BY_KEY before spawning a new task; if a job for this exact content is already pending or running, returns the existing job_id (logs `[compile] dedup hit — reusing job <id>`). - `_purge_expired_jobs` evicts both COMPILE_JOBS and JOB_BY_KEY, keeping the index consistent. Edge case where two jobs share a key (old finished, new running) is handled — only evict the key entry if it still points at the purged job. 2. Concurrency control (`backend/app/api/routes/compile.py`) - `_COMPILE_SEMAPHORE = asyncio.Semaphore(2)` global cap on simultaneous compiles. - `_target_lock(board_fqbn)` returns a per-target asyncio.Lock so concurrent compiles to the SAME board (sharing the persistent build dir) serialise. Different boards still run in parallel up to the semaphore cap. - `_compile_job` acquires sema → per-target lock → flips state to `running` → calls `_run_compile`. Pending state now accurately reflects "queued waiting for resources". 3. Persistent build dir (`backend/app/services/espidf_compiler.py`) - New `_prepare_persistent_project_dir(idf_target)` materialises `/var/lib/velxio-build/<target>/project/` from the template on first use; on subsequent compiles it wipes only `main/` and `user_libs/` (the per-compile parts) and leaves `build/` alone so ninja's incremental cache + ccache .o files survive. - Toolchain version sentinel (`.idf_version`) wipes the whole target dir if the ESP-IDF or arduino-esp32 version changes — cached objects from the old toolchain are no longer ABI-compatible. - `compile()` is now a thin dispatcher: persistent path or fallback to the legacy `tempfile.TemporaryDirectory()` flow. The actual build logic was extracted into `_compile_in_dir()` so both paths share one implementation, no duplication. - Escape hatch: `VELXIO_PERSISTENT_BUILD_DIR=0` env var falls back to the tempfile path without rebuilding the image. Critical for production safety. 4. ccache normalisation (`Dockerfile.standalone`) - + `ENV CCACHE_BASEDIR=/var/lib/velxio-build` makes ccache canonicalise absolute paths under that prefix when computing the cache key. Robustens hits against any future subdir rearrangement. 5. Docker compose (`docker-compose.yml`) - + named volume `velxio-build:/var/lib/velxio-build` so the persistent build dir survives `docker compose up -d --build`. - + env `VELXIO_PERSISTENT_BUILD_DIR=1` (default ON; users disable without rebuilding). Expected impact - Cold first compile per container per target: unchanged (~5-7 min). - Same sketch re-compiled: ~2-5 s (everything cached). - Different sketch, same target: ~5-30 s (only user code + new lib steps rebuild; ESP-IDF base hits cache). - Different sketch with new libraries: ~30-90 s (new lib component compiles; rest hits cache). - Concurrent clicks on same example: 1 build, others poll the same job_id. No more six-ninja meltdown. Tests - `test/backend/unit/test_compile_dedup.py` covers `_job_key` stability + variance and `_purge_expired_jobs` consistency (including the "two jobs share a key" edge case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:33:11 +07:00
# CCACHE_BASEDIR makes ccache treat absolute paths under this prefix as
# relative when computing the cache key. Combined with the persistent
fix(docker): VOLUME defaults so standalone runs get the perf caches too A user reported on Discord that an ESP32 Blink compile on a fresh `docker run -d ghcr.io/.../velxio:master` (no -v flags) takes 8-9 minutes — every single time. Same hardware that the local project checkout flies through in 5-30 seconds with the new persistent build dir + ccache pipeline. Root cause: `docker run` without `-v` mounts gets nothing persistent. ccache + persistent build dir live in /var/cache/ccache and /var/lib/velxio-build, both wiped on every `docker rm` (which the user explicitly did when troubleshooting). docker-compose users get the volumes via the compose file; standalone users got nothing because the Dockerfile didn't declare them. This PR closes that gap. Dockerfile.standalone - VOLUME ["/app/data", "/root/.arduino15", "/root/Arduino", "/var/cache/ccache", "/var/lib/velxio-build"] Anonymous volumes are now created automatically when the user runs the image without -v. They survive `docker stop`/`docker start`/ `docker rm` (only `docker rm -v` or `docker volume prune` removes them). Users can still pass `-v` for named volumes — explicit mounts always win over the VOLUME directive. - Replace `ccache --set-config max_size 8G` (RUN, written to /var/cache/ccache/ccache.conf which the volume mount masks at runtime) with `ENV CCACHE_MAXSIZE=8G` etc. — env vars override any conf-file value on every ccache invocation, so the 8 GB cap actually applies at runtime regardless of what's in the volume. README.md - Update both the quick-start docker run and the detailed self-host section to include all five volumes. - Add a note explaining what each volume is for and that without them, cold compile times are 5-7 min vs the 5-30 s warm path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:04:16 +07:00
# /var/lib/velxio-build/<target>/ build dir, this lets ccache hit across
# compiles even though some flags (-I, -fmacro-prefix-map) embed absolute
# paths into the command line.
perf(compile): dedup, concurrency limits, and persistent build dir for ESP-IDF Three coordinated fixes that together close the "ESP-IDF compile takes 5-7 min every time" gap and prevent the failure mode where a user clicking compile multiple times spawns six ninja processes that peel each other apart on a modest VPS. What was wrong - /compile/start generated a fresh uuid4 every call, so 6 clicks = 6 independent builds racing each other. Saw load average 30 on the prod VPS during a real BMP280 attempt today. - No concurrency limit anywhere; asyncio.create_task() fired without gating. - ccache was wired in last week (PR #149) but reported 18,350 cacheable calls and **0 hits** because the build dir was a fresh tempfile.TemporaryDirectory(prefix='espidf_') per compile. The random /tmp/espidf_<random>/ path baked into -I and -fmacro-prefix-map flags → different command line every compile → ccache hash miss every time. What this PR does 1. Job deduplication (`backend/app/api/routes/compile.py`) - New `_job_key(files, board_fqbn)` returns SHA-256 of normalised file names + contents + board. Order-independent. - New `JOB_BY_KEY: dict[str, str]` indexes hash → job_id. - `compile_start` checks JOB_BY_KEY before spawning a new task; if a job for this exact content is already pending or running, returns the existing job_id (logs `[compile] dedup hit — reusing job <id>`). - `_purge_expired_jobs` evicts both COMPILE_JOBS and JOB_BY_KEY, keeping the index consistent. Edge case where two jobs share a key (old finished, new running) is handled — only evict the key entry if it still points at the purged job. 2. Concurrency control (`backend/app/api/routes/compile.py`) - `_COMPILE_SEMAPHORE = asyncio.Semaphore(2)` global cap on simultaneous compiles. - `_target_lock(board_fqbn)` returns a per-target asyncio.Lock so concurrent compiles to the SAME board (sharing the persistent build dir) serialise. Different boards still run in parallel up to the semaphore cap. - `_compile_job` acquires sema → per-target lock → flips state to `running` → calls `_run_compile`. Pending state now accurately reflects "queued waiting for resources". 3. Persistent build dir (`backend/app/services/espidf_compiler.py`) - New `_prepare_persistent_project_dir(idf_target)` materialises `/var/lib/velxio-build/<target>/project/` from the template on first use; on subsequent compiles it wipes only `main/` and `user_libs/` (the per-compile parts) and leaves `build/` alone so ninja's incremental cache + ccache .o files survive. - Toolchain version sentinel (`.idf_version`) wipes the whole target dir if the ESP-IDF or arduino-esp32 version changes — cached objects from the old toolchain are no longer ABI-compatible. - `compile()` is now a thin dispatcher: persistent path or fallback to the legacy `tempfile.TemporaryDirectory()` flow. The actual build logic was extracted into `_compile_in_dir()` so both paths share one implementation, no duplication. - Escape hatch: `VELXIO_PERSISTENT_BUILD_DIR=0` env var falls back to the tempfile path without rebuilding the image. Critical for production safety. 4. ccache normalisation (`Dockerfile.standalone`) - + `ENV CCACHE_BASEDIR=/var/lib/velxio-build` makes ccache canonicalise absolute paths under that prefix when computing the cache key. Robustens hits against any future subdir rearrangement. 5. Docker compose (`docker-compose.yml`) - + named volume `velxio-build:/var/lib/velxio-build` so the persistent build dir survives `docker compose up -d --build`. - + env `VELXIO_PERSISTENT_BUILD_DIR=1` (default ON; users disable without rebuilding). Expected impact - Cold first compile per container per target: unchanged (~5-7 min). - Same sketch re-compiled: ~2-5 s (everything cached). - Different sketch, same target: ~5-30 s (only user code + new lib steps rebuild; ESP-IDF base hits cache). - Different sketch with new libraries: ~30-90 s (new lib component compiles; rest hits cache). - Concurrent clicks on same example: 1 build, others poll the same job_id. No more six-ninja meltdown. Tests - `test/backend/unit/test_compile_dedup.py` covers `_job_key` stability + variance and `_purge_expired_jobs` consistency (including the "two jobs share a key" edge case). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:33:11 +07:00
ENV CCACHE_BASEDIR=/var/lib/velxio-build
fix(docker): VOLUME defaults so standalone runs get the perf caches too A user reported on Discord that an ESP32 Blink compile on a fresh `docker run -d ghcr.io/.../velxio:master` (no -v flags) takes 8-9 minutes — every single time. Same hardware that the local project checkout flies through in 5-30 seconds with the new persistent build dir + ccache pipeline. Root cause: `docker run` without `-v` mounts gets nothing persistent. ccache + persistent build dir live in /var/cache/ccache and /var/lib/velxio-build, both wiped on every `docker rm` (which the user explicitly did when troubleshooting). docker-compose users get the volumes via the compose file; standalone users got nothing because the Dockerfile didn't declare them. This PR closes that gap. Dockerfile.standalone - VOLUME ["/app/data", "/root/.arduino15", "/root/Arduino", "/var/cache/ccache", "/var/lib/velxio-build"] Anonymous volumes are now created automatically when the user runs the image without -v. They survive `docker stop`/`docker start`/ `docker rm` (only `docker rm -v` or `docker volume prune` removes them). Users can still pass `-v` for named volumes — explicit mounts always win over the VOLUME directive. - Replace `ccache --set-config max_size 8G` (RUN, written to /var/cache/ccache/ccache.conf which the volume mount masks at runtime) with `ENV CCACHE_MAXSIZE=8G` etc. — env vars override any conf-file value on every ccache invocation, so the 8 GB cap actually applies at runtime regardless of what's in the volume. README.md - Update both the quick-start docker run and the detailed self-host section to include all five volumes. - Add a note explaining what each volume is for and that without them, cold compile times are 5-7 min vs the 5-30 s warm path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:04:16 +07:00
# Cache cap + compression set as env vars (rather than via `ccache
# --set-config` at image-build time) because $CCACHE_DIR is a docker volume:
# anything written into /var/cache/ccache during the RUN step is masked at
# runtime by the volume mount. ccache reads CCACHE_MAXSIZE / CCACHE_COMPRESS
# / CCACHE_COMPRESSLEVEL on every invocation and they override any conf-file
# value, so the cap actually applies at runtime.
ENV CCACHE_MAXSIZE=8G
ENV CCACHE_COMPRESS=1
ENV CCACHE_COMPRESSLEVEL=6
fix(docker): VOLUME defaults so standalone runs get the perf caches too A user reported on Discord that an ESP32 Blink compile on a fresh `docker run -d ghcr.io/.../velxio:master` (no -v flags) takes 8-9 minutes — every single time. Same hardware that the local project checkout flies through in 5-30 seconds with the new persistent build dir + ccache pipeline. Root cause: `docker run` without `-v` mounts gets nothing persistent. ccache + persistent build dir live in /var/cache/ccache and /var/lib/velxio-build, both wiped on every `docker rm` (which the user explicitly did when troubleshooting). docker-compose users get the volumes via the compose file; standalone users got nothing because the Dockerfile didn't declare them. This PR closes that gap. Dockerfile.standalone - VOLUME ["/app/data", "/root/.arduino15", "/root/Arduino", "/var/cache/ccache", "/var/lib/velxio-build"] Anonymous volumes are now created automatically when the user runs the image without -v. They survive `docker stop`/`docker start`/ `docker rm` (only `docker rm -v` or `docker volume prune` removes them). Users can still pass `-v` for named volumes — explicit mounts always win over the VOLUME directive. - Replace `ccache --set-config max_size 8G` (RUN, written to /var/cache/ccache/ccache.conf which the volume mount masks at runtime) with `ENV CCACHE_MAXSIZE=8G` etc. — env vars override any conf-file value on every ccache invocation, so the 8 GB cap actually applies at runtime regardless of what's in the volume. README.md - Update both the quick-start docker run and the detailed self-host section to include all five volumes. - Add a note explaining what each volume is for and that without them, cold compile times are 5-7 min vs the 5-30 s warm path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 04:04:16 +07:00
RUN mkdir -p /var/cache/ccache /var/lib/velxio-build /root/Arduino
# ── Persistent paths ────────────────────────────────────────────────────────
# Declaring these as VOLUMEs means `docker run` (without explicit -v) creates
# anonymous volumes for them — they survive `docker stop`/`docker start` and
# even `docker rm`. Without this, every container restart wipes the ccache
# and persistent ESP-IDF build dir, so every compile is cold (~5-7 min on
# modest hardware) instead of the warm-cache 5-30s we measured on prod.
#
# Users SHOULD pass `-v velxio-X:/path` for each of these to get named
# volumes (easier to inspect / back up than anonymous ones), but the
# anonymous default is a sensible fallback.
#
# /app/data — SQLite DB + project files + auto-generated SECRET_KEY
# /root/.arduino15 — arduino-cli config + installed cores
# /root/Arduino — Library Manager-installed Arduino libraries
# /var/cache/ccache — ccache cache (ESP-IDF compiles)
# /var/lib/velxio-build — persistent ESP-IDF build dir (one subdir per target)
VOLUME ["/app/data", "/root/.arduino15", "/root/Arduino", "/var/cache/ccache", "/var/lib/velxio-build"]
perf(espidf): drop in ccache for ESP32 compiles (~10× warm speedup) Cold first compile per container is unchanged (cache empty). Subsequent compiles drop from ~5-7 minutes to ~30-60 seconds because every ESP-IDF base object (FreeRTOS, lwIP, esp_wifi, libsodium, soc, hal, …) hits the cache. The user's BMP280 example, which hangs on cold compile, completes near-instantly on the second attempt. Why a transparent cache is safe: ccache hashes the preprocessed source + flags + compiler. A cache hit only happens when the input is byte-for-byte identical to a prior compile. Different sketches with different libraries still get correct cache misses; there is no path where one project's output contaminates another. Changes - Dockerfile.standalone: install ccache, set CCACHE_DIR=/var/cache/ccache, IDF_CCACHE_ENABLE=1, configure 2 GB cap with compression. Compression (level 6) cuts cache disk usage by ~40% with negligible CPU overhead. - docker-compose.yml: named volume `ccache:/var/cache/ccache` so the cache survives `docker compose up -d --build` (without it, every image rebuild discards the cache). - backend/app/services/espidf_compiler.py: pass `-DCCACHE_ENABLE=1` to cmake when IDF_CCACHE_ENABLE is truthy. ESP-IDF's project.cmake (`set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)` on line 374) is what actually wires ccache in; without the cmake -D flag the env var alone has no effect because we don't go through idf.py. Escape hatch: set IDF_CCACHE_ENABLE=0 in compose env to disable without rebuilding the image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:44:29 +07:00
# Install ESP-IDF Python dependencies using the final image's Python
# The requirements.txt has version constraints required by ESP-IDF 4.4.x
RUN grep -v 'esp-windows-curses' /opt/esp-idf/requirements.txt \
| pip install --no-cache-dir -r /dev/stdin
EXPOSE 80
CMD ["/app/entrypoint.sh"]