Alpine Docker cross-compile for Windows¶
This builds a Windows openFPGALoader.exe from Linux using an Alpine-based Docker image.
The image installs Alpine's MinGW-w64 compiler and builds the Windows target dependencies
zlib, libusb, hidapi, and libftdi1 into /opt/x86_64-w64-mingw32.
Windows host prerequisite¶
This is a Linux-container build. It cannot run while Docker Desktop is in
Windows-container mode because alpine:3.24.2 has no Windows image manifest.
Use Docker Desktop with the WSL2 backend and Linux containers. From PowerShell, verify the engine before building:
.\deploy\scripts\verify-docker-linux-engine.ps1
The expected output starts with:
Docker engine: linux/amd64
If the check reports windows/amd64, enable Use the WSL 2 based engine in
Docker Desktop Settings > General, then choose Switch to Linux containers
from the Docker Desktop tray menu. The Compose service also declares
platform: linux/amd64 so an incompatible engine fails early with a clear
platform error.
Build¶
docker compose -f docker-compose.cross-windows.yml build
Build and package openFPGALoader¶
docker compose -f docker-compose.cross-windows.yml run --rm windows-cross
Artifacts are written to:
dist/docker-windows/
The package contains the install tree plus any non-system DLLs imported by the EXE. The wrapper also requests static linking for the GCC/libstdc++ runtime so the Windows package is as self-contained as possible.
Clean rebuild¶
docker compose -f docker-compose.cross-windows.yml run --rm windows-cross /src/deploy/scripts/docker-cross-windows.sh --clean
Pass extra CMake flags¶
CMAKE_EXTRA_ARGS="-DENABLE_CMSISDAP=OFF -DENABLE_FTDIPP=OFF -DENABLE_UDEV=OFF -DENABLE_LIBGPIOD=OFF" \
docker compose -f docker-compose.cross-windows.yml run --rm windows-cross
Notes¶
- This is for
x86_64-w64-mingw32Windows binaries. - The Alpine image uses the stable Alpine
3.24.2release because current Alpine packaging exposes the MinGW-w64 compiler there. - Runtime tests cannot execute the Windows EXE inside this container unless Wine is added. The wrapper only checks that the EXE exists and then packages it.
Notes¶
The Dockerfile downloads zlib from the official GitHub release tarball first and falls back to zlib.net/fossils. Do not use https://zlib.net/zlib-<version>.tar.gz; zlib moves older point releases out of the web root after newer releases, which causes 404 failures.
The compose file sets pull_policy: build so Docker Compose does not try to pull the local image name from Docker Hub before building it.
Notes on dependency builds¶
The Alpine image intentionally builds libusb from the upstream release tarball with
./configure --host=x86_64-w64-mingw32 instead of CMake. The GitHub source archive for
libusb does not contain a root CMakeLists.txt, while the official release tarball
contains the generated configure script needed for cross-compilation.
Console output on Windows¶
The Docker wrapper forces the final Windows executable to use the Windows console/CUI subsystem:
-static -static-libgcc -static-libstdc++ -Wl,--subsystem,console
This is required because openFPGALoader is a CLI program. If the executable is linked as a Windows GUI subsystem app, it may run from PowerShell/cmd.exe but --help, --version, and errors appear silent because stdout/stderr are detached. The wrapper verifies the PE subsystem with x86_64-w64-mingw32-objdump -p and fails the package step if it sees Windows GUI.
To rebuild after changing linker flags, use --clean or remove build-docker-windows, because CMake caches linker flags in the build directory.
Silent executable troubleshooting¶
If openFPGALoader.exe --help opens and exits with no visible output, check these two things first:
dumpbin /headers .\openFPGALoader.exe | findstr /i subsystem
Expected: Windows CUI. If it says Windows GUI, rebuild with --clean.
Then check imported DLLs from inside Docker:
x86_64-w64-mingw32-objdump -p /src/dist/docker-windows/install/bin/openFPGALoader.exe | grep "DLL Name"
The wrapper copies all non-system DLL imports into dist/docker-windows/install/bin. Missing runtime DLLs can make a Windows command-line program appear silent because it exits before main() runs.
2026-10 deploy layout¶
The current Compose definition uses a single repository bind mount at /src.
Portable output is written under /src/dist/docker-windows; there is no nested
/out bind mount. This avoids Docker Desktop/WSL2 path-creation failures.
When act runs on Windows, the workflow probes both daemon-visible host paths:
/host_mnt/<drive>/... and /run/desktop/mnt/host/<drive>/..., then exports
CROSS_HOST_SRC. The probe uses alpine:3.24.2.
Docker Desktop / act performance and diagnostics¶
The cross-build keeps the write-heavy CMake/Ninja build tree and install staging
tree in the named openfpgaloader-cross-work Docker volume at /work. Only the
source tree and final package output use the Windows host bind mount. This avoids
large numbers of small writes through Docker Desktop's Windows/WSL filesystem
bridge.
CI and act invoke docker compose run with --no-TTY. The wrapper prints
[windows-cross] checkpoints before directory setup, dependency validation, CMake
configuration, compilation, installation, version resolution, and ZIP creation.
If a local run stops making progress, the last checkpoint identifies the blocked
stage.
Windows line endings (CRLF)¶
All repository shell scripts must use LF line endings. .gitattributes enforces
*.sh text eol=lf for new checkouts, including Git for Windows with
core.autocrlf=true.
If an existing Windows worktree fails at startup with an error similar to:
: invalid option nameocker-cross-windows.sh: line 2: set: pipefail
then the script was already materialized as CRLF. Normalize the shell scripts once before rerunning Compose. The Docker image itself does not need rebuilding for a bind-mounted script line-ending correction.
Embedded Xilinx Platform Cable driver package¶
The Windows cross job first builds the canonical
xilinx-platform-cable-windows.zip from the pinned libwdi/XPCU sources. Compose
mounts its output directory read-only at /opt/xpcu. The cross-packaging wrapper
verifies the accompanying SHA-256 file and embeds both files unchanged at:
share/openFPGALoader/drivers/
xilinx-platform-cable-windows.zip
xilinx-platform-cable-windows.zip.sha256
The portable Windows ZIP is not considered valid unless those files are present. The installer job consumes the embedded archive from the portable ZIP; it does not build or download a second independent XPCU package.
For act on Docker Desktop, both the XPCU build and the main cross-build probe the
daemon-visible Windows checkout (/host_mnt/<drive>/... or
/run/desktop/mnt/host/<drive>/...) before creating runtime bind mounts. This
prevents empty libwdi /src mounts and the characteristic exit-127 failure.