Add gated LTspice engine via Wine 11.10
Wire mcltspice into the backend image: WineHQ 11.10 (matching the dev host), i386 multiarch, Mesa software GL, a build-time Wine prefix seeded with the LTspice.ini first-run config, and an entrypoint that starts Xvfb. The LTspice install (exe/lib/examples) mounts from the host; the engine reads LTSPICE_DIR. Gated for now: LTspice v26 stalls at graphics init under headless Wine in the slim image (runs fine on a full desktop). The mount + LTSPICE_DIR are commented in docker-compose.prod.yml so the engine fails fast as 'unavailable' rather than hanging. ngspice is unaffected.
This commit is contained in:
parent
db953de183
commit
1ec22c82dc
3
.gitignore
vendored
3
.gitignore
vendored
@ -41,6 +41,9 @@ docker-compose.override.yml
|
|||||||
# Notebook user data (tracked separately)
|
# Notebook user data (tracked separately)
|
||||||
notebooks/user/
|
notebooks/user/
|
||||||
|
|
||||||
|
# LTspice install -- transferred to prod host via rsync, not tracked (~182MB)
|
||||||
|
/ltspice/
|
||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.coverage
|
.coverage
|
||||||
|
|||||||
@ -1,11 +1,43 @@
|
|||||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS base
|
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS base
|
||||||
|
|
||||||
# System dependencies -- ngspice is required for simulation
|
# System dependencies:
|
||||||
# NOTE: LTspice requires Wine + a Windows LTspice install, which is only
|
# ngspice -- native SPICE engine
|
||||||
# available on the dev host (not in this container). The backend gracefully
|
# wine -- runs LTspice (a Windows x86-64 binary) in batch mode
|
||||||
# degrades -- LTspice engine returns UnsupportedEngineError in Docker.
|
# xvfb -- headless X display; LTspice opens an X connection even with -b
|
||||||
|
# The LTspice binaries themselves are NOT in the image -- they are mounted from
|
||||||
|
# the host at /opt/ltspice-src (see docker-compose.prod.yml). Without that mount
|
||||||
|
# the LTspice engine degrades gracefully and ngspice still works.
|
||||||
|
# Debian's Wine (8.0) is too old for LTspice v26 -- it opens a GUI event loop
|
||||||
|
# instead of running -b headless. Use WineHQ 11.10 to match the dev host, where
|
||||||
|
# LTspice runs fine. i386 multiarch is required (Wine pulls 32-bit components
|
||||||
|
# even for the x86-64 LTspice.exe). All four packages are pinned to the same
|
||||||
|
# version because the WineHQ repo sometimes carries mismatched amd64/i386
|
||||||
|
# versions, and the wine-devel metapackage demands an exact-version match.
|
||||||
|
RUN dpkg --add-architecture i386 && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
ngspice xvfb xauth wget gnupg ca-certificates && \
|
||||||
|
mkdir -pm755 /etc/apt/keyrings && \
|
||||||
|
wget -qO- https://dl.winehq.org/wine-builds/winehq.key | gpg --dearmor -o /etc/apt/keyrings/winehq-archive.key && \
|
||||||
|
wget -qNP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/debian/dists/bookworm/winehq-bookworm.sources && \
|
||||||
|
apt-get update && \
|
||||||
|
apt-get install -y --install-recommends \
|
||||||
|
winehq-devel=11.10~bookworm-1 wine-devel=11.10~bookworm-1 \
|
||||||
|
wine-devel-amd64=11.10~bookworm-1 wine-devel-i386=11.10~bookworm-1 && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# WineHQ installs to /opt/wine-devel/bin; put it on PATH so `wine` resolves
|
||||||
|
# (mcltspice's runner invokes the bare `wine` command).
|
||||||
|
ENV PATH="/opt/wine-devel/bin:${PATH}"
|
||||||
|
|
||||||
|
# LTspice v26 blocks on graphics init unless libGL/libEGL are loadable. The
|
||||||
|
# slim image ships neither, so add Mesa software rendering (llvmpipe) -- Xvfb
|
||||||
|
# has no GPU, so this is the fallback the dev host reaches via DRI3 anyway.
|
||||||
|
# Separate layer to keep the heavy WineHQ layer cached.
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends ngspice && \
|
apt-get install -y --no-install-recommends \
|
||||||
|
libgl1 libglx-mesa0 libgl1-mesa-dri libegl1 libegl-mesa0 \
|
||||||
|
libvulkan1 mesa-vulkan-drivers && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -21,7 +53,7 @@ FROM base AS dev
|
|||||||
# Install in editable mode (source mounted via docker-compose volume)
|
# Install in editable mode (source mounted via docker-compose volume)
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
uv pip install --system -e ".[dev]"
|
uv pip install --system -e ".[dev,ltspice]"
|
||||||
|
|
||||||
# Copy source for initial build (overridden by volume mount in dev)
|
# Copy source for initial build (overridden by volume mount in dev)
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
@ -40,20 +72,50 @@ ENV UV_COMPILE_BYTECODE=1
|
|||||||
# Install dependencies first (no source yet -- better caching)
|
# Install dependencies first (no source yet -- better caching)
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
uv pip install --system .
|
uv pip install --system ".[ltspice]"
|
||||||
|
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
|
|
||||||
# Re-install with source to get the package registered
|
# Re-install with source to get the package registered
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
uv pip install --system .
|
uv pip install --system ".[ltspice]"
|
||||||
|
|
||||||
# Run as non-root
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|
||||||
|
# Run as non-root. /opt/ltspice is the writable LTSPICE_DIR (holds the Wine
|
||||||
|
# prefix); /opt/ltspice-src is the read-only mountpoint for the host binaries.
|
||||||
RUN useradd --create-home --shell /bin/bash spicebook && \
|
RUN useradd --create-home --shell /bin/bash spicebook && \
|
||||||
mkdir -p /app/notebooks/user /app/notebooks/examples && \
|
mkdir -p /app/notebooks/user /app/notebooks/examples /opt/ltspice /opt/ltspice-src && \
|
||||||
chown -R spicebook:spicebook /app/notebooks
|
chown -R spicebook:spicebook /app/notebooks /opt/ltspice
|
||||||
USER spicebook
|
USER spicebook
|
||||||
|
|
||||||
|
# Build a fresh Wine prefix with this image's own Wine version (avoids the
|
||||||
|
# version-mismatch hazard of shipping a prefix built by a different Wine).
|
||||||
|
# LTspice batch mode needs no Mono/Gecko, so disable them to skip the download.
|
||||||
|
ENV WINEARCH=win64 \
|
||||||
|
WINEPREFIX=/opt/ltspice/.wine \
|
||||||
|
WINEDEBUG=-all \
|
||||||
|
WINEDLLOVERRIDES="mscoree=;mshtml="
|
||||||
|
# Keep one Xvfb up for the whole init -- xvfb-run tears the display down the
|
||||||
|
# moment wineboot returns, leaving wineserver's lingering processes to spin on
|
||||||
|
# a dead X server (fatal XIO errors, hung build). Explicit display + timeouts
|
||||||
|
# make the step deterministic and unable to hang.
|
||||||
|
RUN Xvfb :99 -screen 0 1024x768x16 -nolisten tcp >/tmp/xvfb-build.log 2>&1 & \
|
||||||
|
sleep 2 && \
|
||||||
|
DISPLAY=:99 timeout 90 wineboot --init && \
|
||||||
|
DISPLAY=:99 timeout 30 wineserver -w; \
|
||||||
|
wineserver -k 2>/dev/null; pkill Xvfb 2>/dev/null; \
|
||||||
|
rm -f /tmp/.X99-lock /tmp/.X11-unix/X99; true
|
||||||
|
|
||||||
|
# Seed LTspice's settings file so batch (-b) runs skip the one-time first-run
|
||||||
|
# consent dialog (which otherwise blocks even in batch mode and the sim hangs).
|
||||||
|
# Captured from a broken-in install; LastRunVersion/CaptureAnalytics are the
|
||||||
|
# lines that matter. Without this the LTspice engine times out on every run.
|
||||||
|
COPY --chown=spicebook:spicebook ltspice-config/LTspice.ini \
|
||||||
|
/opt/ltspice/.wine/drive_c/users/spicebook/AppData/Roaming/LTspice.ini
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
||||||
CMD ["python", "-m", "uvicorn", "spicebook.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
CMD ["python", "-m", "uvicorn", "spicebook.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|||||||
34
backend/docker-entrypoint.sh
Normal file
34
backend/docker-entrypoint.sh
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Wire the host-mounted LTspice binaries into the writable LTSPICE_DIR (which
|
||||||
|
# holds the Wine prefix baked at image build), start a virtual display for
|
||||||
|
# Wine, then hand off to the CMD.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
src=/opt/ltspice-src
|
||||||
|
dst=${LTSPICE_DIR:-/opt/ltspice}
|
||||||
|
|
||||||
|
# The LTspice install is mounted read-only from the host; symlink the three
|
||||||
|
# things mcltspice's config references (exe, lib, examples) next to the prefix.
|
||||||
|
if [ -d "$src" ]; then
|
||||||
|
for item in LTspice.exe lib examples; do
|
||||||
|
if [ -e "$src/$item" ]; then
|
||||||
|
ln -sfn "$src/$item" "$dst/$item"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# LTspice opens an X connection even in batch (-b) mode, so give Wine a
|
||||||
|
# headless display. Clear any stale lock first -- the build-time Xvfb on the
|
||||||
|
# same display can leave /tmp/.X99-lock baked into the image, which would make
|
||||||
|
# a naive "already running?" check skip startup (one container = one Xvfb).
|
||||||
|
if [ -n "$DISPLAY" ]; then
|
||||||
|
rm -f "/tmp/.X${DISPLAY#:}-lock"
|
||||||
|
Xvfb "$DISPLAY" -screen 0 1024x768x16 -nolisten tcp >/tmp/xvfb.log 2>&1 &
|
||||||
|
# Give the server a moment to come up before the app can ask for it.
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
[ -e "/tmp/.X11-unix/X${DISPLAY#:}" ] && break
|
||||||
|
sleep 0.3
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
BIN
backend/ltspice-config/LTspice.ini
Normal file
BIN
backend/ltspice-config/LTspice.ini
Normal file
Binary file not shown.
@ -6,6 +6,16 @@ services:
|
|||||||
target: prod
|
target: prod
|
||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000"
|
||||||
|
# LTspice engine is GATED. The backend image ships Wine 11.10 + the prefix,
|
||||||
|
# and the host carries ./ltspice (exe/lib/examples), but LTspice v26 stalls
|
||||||
|
# at graphics init under headless Wine in this slim image (works on the dev
|
||||||
|
# desktop). Without LTSPICE_DIR the engine reports "unavailable" and fails
|
||||||
|
# fast instead of hanging. To resume debugging, uncomment the two blocks:
|
||||||
|
# volumes:
|
||||||
|
# - ./ltspice:/opt/ltspice-src:ro
|
||||||
|
# environment:
|
||||||
|
# - LTSPICE_DIR=/opt/ltspice
|
||||||
|
# - DISPLAY=:99
|
||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
- caddy
|
- caddy
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user