fix(build): fix standalone Dockerfile paths and duplicate adcChannelForPin

This commit is contained in:
a2nr 2026-09-05 07:53:42 +07:00
parent 29aad7c456
commit fcff4de4a5
2 changed files with 59 additions and 234 deletions

View File

@ -1,172 +1,92 @@
# ---- Stage 0: QEMU .so + ROM binaries ---- # ---- Stage 0: QEMU .so + ROM binaries ----
# The Velxio runtime needs libqemu-xtensa.so + libqemu-riscv32.so (the # Two modes:
# QEMU shared libraries that simulate ESP32 / ESP32-S3 / ESP32-C3) plus # 1. Local: place files in prebuilt/qemu/ (from build-qemu.sh or manual copy)
# the matching boot ROM blobs. Three sources, tried in order: # 2. CI/CD: downloads from GitHub Release (requires qemu-lcgamboa repo to be public)
# # The COPY always runs; the RUN only downloads missing files.
# 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 FROM ubuntu:22.04 AS qemu-provider
ARG ENABLE_ESP32=0
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \ RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
ARG TARGETARCH ARG QEMU_RELEASE_URL=https://github.com/davidmonterocrespo24/velxio/releases/download/qemu-prebuilt
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) # Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep)
RUN mkdir -p /qemu RUN mkdir -p /qemu
COPY prebuilt/qemu/ /qemu/ COPY prebuilt/qemu/ /qemu/
# Resolve which fetch path the build will use and fail-fast with a # Download any missing files from GitHub Release
# friendly message if neither prebuilt files nor a key were provided. RUN if [ "$ENABLE_ESP32" = "1" ]; then \
RUN cd /qemu \ cd /qemu \
&& have_libs=1 && for base in libqemu-xtensa libqemu-riscv32; do \ && for f in libqemu-xtensa.so libqemu-riscv32.so esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \
[ -f "${base}.so" ] || have_libs=0 ; \ if [ ! -f "$f" ]; then \
done \ echo "Downloading $f from release..." ; \
&& if [ "$have_libs" = "1" ]; then \ curl -fSL -o "$f" "${QEMU_RELEASE_URL}/$f" ; \
echo "[qemu-provider] using local prebuilt/qemu/ files — no download" ; \ else \
elif [ -n "${QEMU_RELEASE_URL}" ]; then \ echo "Using local $f ($(stat -c%s "$f") bytes)" ; \
echo "[qemu-provider] using legacy QEMU_RELEASE_URL: ${QEMU_RELEASE_URL}" ; \ fi ; \
elif [ -n "${VELXIO_LICENSE_KEY}" ]; then \ done \
echo "[qemu-provider] using velxio.dev gated download with provided license key" ; \ && ls -lh /qemu/ ; \
else \ else \
echo "" ; \ echo "ESP32 support disabled. Skipping QEMU downloads." ; \
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 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 esp32s3_rev0_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 ---- # ---- Stage 0.5: ESP-IDF toolchain for ESP32 compilation ----
FROM ubuntu:22.04 AS espidf-builder FROM ubuntu:22.04 AS espidf-builder
RUN apt-get update && apt-get install -y --no-install-recommends \ ARG ENABLE_ESP32=0
# Create empty directories to ensure COPY works later even if ESP32 is disabled
RUN mkdir -p /opt/esp-idf /root/.espressif /opt/arduino-esp32
RUN if [ "$ENABLE_ESP32" = "1" ]; then \
apt-get update && apt-get install -y --no-install-recommends \
git wget flex bison gperf python3 python3-pip python3-venv \ git wget flex bison gperf python3 python3-pip python3-venv \
cmake ninja-build ccache libffi-dev libssl-dev \ cmake ninja-build ccache libffi-dev libssl-dev \
libusb-1.0-0 ca-certificates \ libusb-1.0-0 ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/* ; \
fi
# Install ESP-IDF 4.4.7 (matches Arduino ESP32 core 2.0.17 / lcgamboa QEMU ROM) # 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 \ RUN if [ "$ENABLE_ESP32" = "1" ]; then \
https://github.com/espressif/esp-idf.git /opt/esp-idf 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 cd /opt/esp-idf && \
./install.sh esp32,esp32c3 && \
# Install toolchains for esp32 (Xtensa) and esp32c3 (RISC-V) only rm -rf .git docs examples && \
RUN ./install.sh esp32,esp32c3 find /root/.espressif -name '*.tar.*' -delete 2>/dev/null || true && \
git clone --branch 2.0.17 --depth=1 --recursive --shallow-submodules \
# Clean up large unnecessary files to reduce image size https://github.com/espressif/arduino-esp32.git /opt/arduino-esp32 && \
RUN rm -rf .git docs examples \ rm -rf /opt/arduino-esp32/.git ; \
&& find /root/.espressif -name '*.tar.*' -delete 2>/dev/null || true fi
# 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
# mkspiffs (arduino-esp32 0.2.3 flavor) — bakes uploaded files into the SPIFFS
# data-partition image at compile time. Without it espidf_compiler._build_spiffs_image
# silently returns None and ships a blank SPIFFS region, so SPIFFS.begin() fails and
# auto-formats and user-uploaded files never appear (GH #162). Installed at the exact
# path the runtime locates via IDF_TOOLS_PATH (tools/mkspiffs/*/mkspiffs/mkspiffs).
RUN mkdir -p /root/.espressif/tools/mkspiffs/0.2.3/mkspiffs \
&& wget -qO /tmp/mkspiffs.tgz \
https://github.com/igrr/mkspiffs/releases/download/0.2.3/mkspiffs-0.2.3-arduino-esp32-linux64.tar.gz \
&& tar -xzf /tmp/mkspiffs.tgz -C /tmp \
&& cp /tmp/mkspiffs-0.2.3-arduino-esp32-linux64/mkspiffs \
/root/.espressif/tools/mkspiffs/0.2.3/mkspiffs/mkspiffs \
&& chmod +x /root/.espressif/tools/mkspiffs/0.2.3/mkspiffs/mkspiffs \
&& rm -rf /tmp/mkspiffs.tgz /tmp/mkspiffs-0.2.3-arduino-esp32-linux64
# ---- Stage 1: Build frontend and third-party ---- # ---- Stage 1: Build frontend ----
FROM node:20 AS frontend-builder FROM node:20 AS frontend-builder
WORKDIR /app WORKDIR /app
# avr8js, rp2040js and @wokwi/elements are pulled directly from the npm # VITE_BASE_PATH: sub-path prefix for assets (default "/" for standalone)
# registry (see frontend/package.json) — no upstream git clones needed. # VITE_API_BASE: API endpoint prefix (default "/api" for standalone)
# Board SVGs live in frontend/public/boards/, component SVGs in ARG VITE_BASE_PATH=/
# frontend/public/component-svgs/, and components-metadata.json is committed. ARG VITE_API_BASE=/api
ENV VITE_BASE_PATH=${VITE_BASE_PATH}
ENV VITE_API_BASE=${VITE_API_BASE}
COPY frontend/ frontend/ COPY frontend/ frontend/
COPY scripts/ scripts/ COPY scripts/ scripts/
WORKDIR /app/frontend WORKDIR /app/frontend
# Lock files aren't committed in this repo (they're gitignored) — see the RUN npm install && npm run build:docker
# 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 ---- # ---- Stage 2: Final Production Image ----
FROM python:3.12-slim FROM python:3.12-slim
# Install system dependencies, nginx, and QEMU runtime. ARG ENABLE_ESP32=0
#
# qemu-system-arm + qemu-utils provide qemu-system-aarch64 and qemu-img, # Install system dependencies, nginx, and QEMU .so runtime libraries
# 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 \ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
ca-certificates \ ca-certificates \
@ -180,10 +100,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ninja-build \ ninja-build \
libusb-1.0-0 \ libusb-1.0-0 \
git \ git \
ccache \
qemu-system-arm \
qemu-utils \
sdcc \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir packaging && pip install --no-cache-dir packaging
@ -197,12 +113,6 @@ RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/inst
# in the mounted /root/.arduino15 volume. # in the mounted /root/.arduino15 volume.
# ESP32 compilation uses ESP-IDF instead of arduino-cli. # ESP32 compilation uses ESP-IDF instead of arduino-cli.
# Vendored board-manager index seeds (issue #254): entrypoint.sh copies any
# of these missing from /root/.arduino15 at boot, so a flaky index host
# (drazzy.com) or a stale pre-existing volume cannot brick compilation.
# See backend/board-indexes/README.md.
COPY backend/board-indexes/package_*.json /opt/arduino15-seed/
WORKDIR /app WORKDIR /app
# Data directory for persistent SQLite database (mounted as a volume at runtime) # Data directory for persistent SQLite database (mounted as a volume at runtime)
@ -215,13 +125,7 @@ RUN pip install --no-cache-dir -r requirements.txt
# Copy backend application code # Copy backend application code
COPY backend/app/ ./app/ COPY backend/app/ ./app/
# One-off maintenance scripts (e.g. backfill_boards_2026_05). Pure stdlib — # Setup Nginx configuration — remove default site so our config is the only server
# run with: docker exec velxio-app python /app/scripts/<script> --apply
COPY backend/scripts/ ./scripts/
# 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 RUN rm -f /etc/nginx/sites-enabled/default
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
@ -238,7 +142,6 @@ RUN sed -i 's/\r$//' /app/entrypoint.sh && chmod +x /app/entrypoint.sh
# libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC) # libqemu-riscv32.so → ESP32-C3 (RISC-V RV32IMC)
# esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine # esp32-v3-rom*.bin → boot/app ROM images required by esp32-picsimlab machine
# esp32c3-rom.bin → ROM image required by esp32c3-picsimlab machine # esp32c3-rom.bin → ROM image required by esp32c3-picsimlab machine
# esp32s3_rev0_rom.bin → ROM image required by esp32s3-picsimlab machine
# NOTE: ROM files must live in the same directory as the .so (worker passes -L # NOTE: ROM files must live in the same directory as the .so (worker passes -L
# to QEMU pointing at os.path.dirname(lib_path)) # to QEMU pointing at os.path.dirname(lib_path))
RUN mkdir -p /app/lib RUN mkdir -p /app/lib
@ -257,84 +160,16 @@ COPY --from=espidf-builder /root/.espressif /root/.espressif
COPY --from=espidf-builder /opt/arduino-esp32 /opt/arduino-esp32 COPY --from=espidf-builder /opt/arduino-esp32 /opt/arduino-esp32
# Memoize ldgen. It re-parses every component's linker.lf with pyparsing on
# EVERY build - measured at 15.2 s here, 41% of a whole ESP-IDF compile of a
# one-file sketch - and its output is a pure function of its inputs. The
# wrapper delegates to the untouched upstream script on a miss.
COPY backend/scripts/ldgen_cache.py /tmp/ldgen_cache.py
RUN if [ -f /opt/esp-idf/tools/ldgen/ldgen.py ]; then \
mv /opt/esp-idf/tools/ldgen/ldgen.py /opt/esp-idf/tools/ldgen/ldgen_upstream.py && \
cp /tmp/ldgen_cache.py /opt/esp-idf/tools/ldgen/ldgen.py && \
chmod +x /opt/esp-idf/tools/ldgen/ldgen.py; \
fi; rm -f /tmp/ldgen_cache.py
ENV IDF_PATH=/opt/esp-idf ENV IDF_PATH=/opt/esp-idf
ENV IDF_TOOLS_PATH=/root/.espressif ENV IDF_TOOLS_PATH=/root/.espressif
ENV ARDUINO_ESP32_PATH=/opt/arduino-esp32 ENV ARDUINO_ESP32_PATH=/opt/arduino-esp32
# ── 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
# CCACHE_BASEDIR makes ccache treat absolute paths under this prefix as
# relative when computing the cache key. Combined with the persistent
# /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.
ENV CCACHE_BASEDIR=/var/lib/velxio-build
# hash_dir=false. ESP-IDF compiles with -ggdb, and for debug builds ccache
# folds the cwd into every cache key by default; CCACHE_BASEDIR does NOT
# exempt it (verified with CCACHE_DEBUG on ccache 4.11: "Hashing CWD
# /var/lib/velxio-build/esp32s3/v_<hash>/project/build"). Since each build
# variant lives in its own dir, every variant had a private key space and a
# cold variant of an already-built board recompiled all ~2100 IDF objects
# from scratch — on a busy box that overran the 600s ninja timeout. With
# hash_dir off, variants of the same board/sdkconfig share objects. The
# only difference is DW_AT_comp_dir inside .debug_info of a reused object,
# which never reaches the flashed .bin.
ENV CCACHE_NOHASHDIR=1
# 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=20G
ENV CCACHE_COMPRESS=1
ENV CCACHE_COMPRESSLEVEL=6
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"]
# Install ESP-IDF Python dependencies using the final image's Python # Install ESP-IDF Python dependencies using the final image's Python
# The requirements.txt has version constraints required by ESP-IDF 4.4.x # The requirements.txt has version constraints required by ESP-IDF 4.4.x
RUN grep -v 'esp-windows-curses' /opt/esp-idf/requirements.txt \ RUN if [ "$ENABLE_ESP32" = "1" ]; then \
| pip install --no-cache-dir -r /dev/stdin grep -v 'esp-windows-curses' /opt/esp-idf/requirements.txt \
| pip install --no-cache-dir -r /dev/stdin ; \
fi
EXPOSE 80 EXPOSE 80

View File

@ -247,6 +247,8 @@ class Esp32BridgeShim {
* the machine's SENS stub). Without the S3 branch analogRead always saw * the machine's SENS stub). Without the S3 branch analogRead always saw
* 0 there (2026-07 emulation-gaps audit, F1). */ * 0 there (2026-07 emulation-gaps audit, F1). */
private adcChannelForPin(pin: number): number { private adcChannelForPin(pin: number): number {
const b = this.bridge as unknown as { adcChannelForGpio?: (gpio: number) => number };
if (typeof b.adcChannelForGpio === 'function') return b.adcChannelForGpio(pin);
const kind = this.bridge.boardKind as string; const kind = this.bridge.boardKind as string;
if (kind === 'esp32-s3' || kind === 'xiao-esp32-s3' || kind === 'arduino-nano-esp32') { if (kind === 'esp32-s3' || kind === 'xiao-esp32-s3' || kind === 'arduino-nano-esp32') {
if (pin >= 1 && pin <= 10) return pin - 1; if (pin >= 1 && pin <= 10) return pin - 1;
@ -272,18 +274,6 @@ class Esp32BridgeShim {
return true; return true;
} }
/** GPIO -> ADC channel. The mapping is CHIP-specific, so ask the bridge
* when it knows its chip (S3: GPIO1..10 -> ADC1 ch0..9; C-family differs);
* fall back to the classic ESP32 map, which was the only one this shim
* handled before and silently returned false for every S3 pin. */
private adcChannelForPin(pin: number): number {
const b = this.bridge as unknown as { adcChannelForGpio?: (gpio: number) => number };
if (typeof b.adcChannelForGpio === 'function') return b.adcChannelForGpio(pin);
if (pin >= 36 && pin <= 39) return pin - 36; // GPIO 36→CH0 .. 39→CH3
if (pin >= 32 && pin <= 35) return pin - 28; // GPIO 32→CH4 .. 35→CH7
return -1;
}
/** /**
* Push a 12-bit waveform LUT to QEMU for per-read ADC interpolation. * Push a 12-bit waveform LUT to QEMU for per-read ADC interpolation.
* Call once per SPICE `.tran` solve; QEMU interpolates at every MMIO * Call once per SPICE `.tran` solve; QEMU interpolates at every MMIO