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
46 changes: 33 additions & 13 deletions .github/workflows/macos-latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ name: Publish macos-latest (Wine 9)
# `macos-latest` tag stays pinned to Wine 9 until upstream #58084 ships.
#
# Tags published on every run:
# - devel-YYYYMMDD-wine-<major.minor> (immutable, audit trail)
# - macos-latest (rolling pointer, can be overridden)
# - devel-YYYYMMDD-wine-<major.minor> (immutable, audit trail — always pushed)
# - <rolling_tag> (rolling pointer; only pushed when
# publish_rolling is true)
#
# Run via the Actions tab → "Run workflow", optionally overriding the
# Wine version pin (e.g. to bump to a newer 9.x point release).
# Run via the Actions tab → "Run workflow". Toggle "publish_rolling" off
# to publish only the dated devel tag — useful for trial builds that
# should not move the macos-latest pointer.

on:
workflow_dispatch:
Expand All @@ -22,9 +24,14 @@ on:
description: "Debian package version pin for wine-stable (e.g. 9.0.0.0~bookworm-1)"
required: true
default: "9.0.0.0~bookworm-1"
publish_rolling:
description: "Also publish the rolling tag (uncheck for trial builds)"
type: boolean
required: false
default: true
rolling_tag:
description: "Rolling tag to publish alongside the dated devel tag"
required: true
description: "Rolling tag name (only used when publish_rolling is checked)"
required: false
default: "macos-latest"

jobs:
Expand All @@ -42,9 +49,24 @@ jobs:
WINE_SHORT=$(echo "${{ inputs.wine_version }}" | cut -d. -f1-2)
DEVEL_TAG="devel-${DATE}-wine-${WINE_SHORT}"
IMAGE=aanas0sayed/docker-ltspice
echo "devel=${IMAGE}:${DEVEL_TAG}" >> "$GITHUB_OUTPUT"
echo "rolling=${IMAGE}:${{ inputs.rolling_tag }}" >> "$GITHUB_OUTPUT"
echo "Will publish: ${IMAGE}:${DEVEL_TAG} and ${IMAGE}:${{ inputs.rolling_tag }}"
# The dated devel tag is always pushed; the rolling pointer is
# only included when the publish_rolling toggle is true. The
# tag list is emitted via GITHUB_OUTPUT's heredoc form so the
# next step can pass it straight to docker/build-push-action.
{
echo "list<<EOF"
echo "${IMAGE}:${DEVEL_TAG}"
if [ "${{ inputs.publish_rolling }}" = "true" ]; then
echo "${IMAGE}:${{ inputs.rolling_tag }}"
fi
echo "EOF"
} >> "$GITHUB_OUTPUT"

if [ "${{ inputs.publish_rolling }}" = "true" ]; then
echo "Will publish: ${IMAGE}:${DEVEL_TAG} and ${IMAGE}:${{ inputs.rolling_tag }}"
else
echo "Will publish: ${IMAGE}:${DEVEL_TAG} (no rolling tag)"
fi

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
Expand Down Expand Up @@ -72,15 +94,13 @@ jobs:
chmod +x test.sh
./test.sh docker-ltspice:macos-ci

- name: Push image with both tags
- name: Push image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64
push: true
tags: |
${{ steps.tags.outputs.devel }}
${{ steps.tags.outputs.rolling }}
tags: ${{ steps.tags.outputs.list }}
build-args: |
WINE_BRANCH=stable
WINE_VERSION=${{ inputs.wine_version }}
Expand Down
15 changes: 10 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,26 @@ RUN groupadd -g 1000 wineuser \
# WINEPREFIX deliberately points into /tmp (tmpfs in callers) — the
# entrypoint materialises it from the on-image template on every fresh
# container start. The build-time prefix lives at /opt/wineprefix-template.
# DISPLAY is intentionally NOT set here — the entrypoint uses an unset
# DISPLAY as the signal "no caller-supplied X server, start a private
# Xvfb on :99." Setting it as an image-level ENV would defeat that
# signal: the entrypoint can't distinguish image-default from
# caller-provided.
ENV HOME=/home/wineuser \
WINEPREFIX=/tmp/wine-prefix \
WINEDEBUG=-all \
DISPLAY=:99
WINEDEBUG=-all

# ── 7. Install LTspice into a build-time prefix (as wineuser) ─────────────
USER wineuser
WORKDIR /home/wineuser
RUN Xvfb :99 -screen 0 1024x768x24 & \
sleep 2 \
&& WINEPREFIX=/home/wineuser/.wine WINEDLLOVERRIDES="mscoree,mshtml=" \
wineboot --init \
&& DISPLAY=:99 WINEPREFIX=/home/wineuser/.wine \
WINEDLLOVERRIDES="mscoree,mshtml=" wineboot --init \
&& WINEPREFIX=/home/wineuser/.wine wineserver --wait \
&& wget -q -O /tmp/LTspice64.msi https://ltspice.analog.com/software/LTspice64.msi \
&& WINEPREFIX=/home/wineuser/.wine wine msiexec /i /tmp/LTspice64.msi /quiet /norestart \
&& DISPLAY=:99 WINEPREFIX=/home/wineuser/.wine \
wine msiexec /i /tmp/LTspice64.msi /quiet /norestart \
&& WINEPREFIX=/home/wineuser/.wine wineserver --wait \
&& rm /tmp/LTspice64.msi \
&& rm -rf /tmp/.wine-* /tmp/wine-* \
Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,10 @@ docker run --rm -it \
aanas0sayed/docker-ltspice:macos-latest
```

> [!NOTE]
> **First-run Xvfb failure:** On the first `docker run`, Xvfb may fail to start (`ERROR: Xvfb exited unexpectedly`). This is a known issue on macOS. Simply run `ltspice` from the shell prompt and it will work — the container is still usable after the entrypoint error. This only occurs with X11 forwarding.

---

## Troubleshooting

- **Xvfb fails on first run (macOS):** The entrypoint prints `ERROR: Xvfb exited unexpectedly` on the first container start on macOS. This is a known issue — the container is still usable. Run `ltspice` from the shell and it will work normally.
- **File not found / path errors:** LTspice runs inside Wine, so paths must use the Wine `Z:` drive (which maps to `/` on the container). For example, a netlist mounted at `/sim/circuit.net` should be passed as `Z:\\sim\\circuit.net`.
- **Permission denied writing the log/raw file:** the container is non-root (uid 1000 by default). If your bind-mount directory isn't writable by that uid, either pass `--user=$(id -u):$(id -g)` so the in-container uid matches the directory owner, or `chmod` the directory so uid 1000 can write to it.
- **Slower first call inside a container:** on every fresh `docker run`, the entrypoint copies the Wine prefix template into `/tmp/wine-prefix` (~150 MB). This is sub-second on tmpfs but adds a small fixed cost per container start.
Expand Down
71 changes: 41 additions & 30 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
# owned by the current uid (required: Wine refuses to use a prefix
# not owned by the running uid).
# 2. Prime Wine so first-run service init completes without an X server.
# 3. Start Xvfb on DISPLAY :99 so subsequent Wine calls have a display.
# 3. Either start a local Xvfb on :99 (headless / batch mode), or
# respect the caller's DISPLAY if X11 forwarding was set up.
# 4. exec the user's command (bash by default).
#
# Runs as the unprivileged wineuser (uid 1000) by default. Also tolerates
Expand All @@ -15,7 +16,11 @@

set -e

XVFB_DISPLAY="${DISPLAY:-:99}"
# Caller-supplied DISPLAY (e.g. `-e DISPLAY=host.docker.internal:0` for X11
# forwarding from XQuartz). Empty if the user didn't pass one — in which
# case we run a private Xvfb on :99.
USER_DISPLAY="${DISPLAY:-}"
XVFB_DISPLAY=":99"
DISPLAY_NUM="${XVFB_DISPLAY#:}"
XVFB_RESOLUTION="${XVFB_RESOLUTION:-1024x768x16}"

Expand Down Expand Up @@ -44,38 +49,44 @@ fi

LTSPICE_EXE="${WINEPREFIX}/drive_c/Program Files/ADI/LTspice/LTspice.exe"

# ── 2. Prime Wine (no X server yet) ───────────────────────────────────────
# ── 2. Prime Wine against a guaranteed-non-existent display ───────────────
# The first wine invocation after a fresh container start triggers internal
# service/init work (winebth, shell32, etc.). If an X server IS available,
# those services try to create windows and block forever. Running LTspice
# once with DISPLAY pointing to a non-existent server makes them fail fast
# and complete their init. The second run then works cleanly.
echo "[entrypoint] Priming Wine (display ${XVFB_DISPLAY}, no X yet)..."
export DISPLAY="${XVFB_DISPLAY}"
wine "$LTSPICE_EXE" -b 2>/dev/null || true
wineserver --wait 2>/dev/null || true
# those services try to create windows and either block forever or finish
# in a half-initialised state that breaks the next GUI launch. Pin
# DISPLAY to :99 here — Xvfb hasn't started yet, so the connection fails
# fast and the services complete their non-GUI init cleanly. We override
# any caller-supplied DISPLAY for this single step only.
echo "[entrypoint] Priming Wine (no X yet)..."
DISPLAY=":99" wine "$LTSPICE_EXE" -b 2>/dev/null || true
DISPLAY=":99" wineserver --wait 2>/dev/null || true
echo "[entrypoint] Wine primed."

# ── 3. Start Xvfb ─────────────────────────────────────────────────────────
rm -f "/tmp/.X${DISPLAY_NUM}-lock" "/tmp/.X11-unix/X${DISPLAY_NUM}" 2>/dev/null || true
echo "[entrypoint] Starting Xvfb on ${XVFB_DISPLAY} (${XVFB_RESOLUTION})"
Xvfb "${XVFB_DISPLAY}" -screen 0 "${XVFB_RESOLUTION}" -nolisten tcp 2>/dev/null &
XVFB_PID=$!

# Wait for the X server socket to appear (max 10 s)
for i in $(seq 1 20); do
if [ -e "/tmp/.X11-unix/X${DISPLAY_NUM}" ]; then
echo "[entrypoint] Xvfb ready (PID ${XVFB_PID})."
break
fi
if ! kill -0 "$XVFB_PID" 2>/dev/null; then
echo "[entrypoint] ERROR: Xvfb exited unexpectedly!" >&2
break
fi
sleep 0.5
done

export DISPLAY="${XVFB_DISPLAY}"
# ── 3. Pick the runtime DISPLAY ───────────────────────────────────────────
# If the caller supplied DISPLAY (X11 forwarding, e.g. host.docker.internal:0
# from XQuartz on macOS), use it as-is and skip Xvfb. Otherwise start a
# private Xvfb on :99 for headless / batch operation.
if [ -n "$USER_DISPLAY" ]; then
echo "[entrypoint] Using caller-provided DISPLAY=${USER_DISPLAY} (skipping Xvfb)"
export DISPLAY="$USER_DISPLAY"
else
rm -f "/tmp/.X${DISPLAY_NUM}-lock" "/tmp/.X11-unix/X${DISPLAY_NUM}" 2>/dev/null || true
echo "[entrypoint] Starting Xvfb on ${XVFB_DISPLAY} (${XVFB_RESOLUTION})"
Xvfb "${XVFB_DISPLAY}" -screen 0 "${XVFB_RESOLUTION}" -nolisten tcp 2>/dev/null &
XVFB_PID=$!
for i in $(seq 1 20); do
if [ -e "/tmp/.X11-unix/X${DISPLAY_NUM}" ]; then
echo "[entrypoint] Xvfb ready (PID ${XVFB_PID})."
break
fi
if ! kill -0 "$XVFB_PID" 2>/dev/null; then
echo "[entrypoint] ERROR: Xvfb exited unexpectedly!" >&2
break
fi
sleep 0.5
done
export DISPLAY="${XVFB_DISPLAY}"
fi

# ── 4. Hand off to the user's command ──────────────────────────────────────
exec "$@"
Loading