Skip to content

Commit bbe64cf

Browse files
committed
ci: Add Windows cross-build and native test jobs
1 parent a61b637 commit bbe64cf

7 files changed

Lines changed: 324 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,25 @@ jobs:
127127
export PATH="$(brew --prefix llvm)/bin:$PATH"
128128
CI_CONFIG="ci/configs/macos.bash" bash ci/scripts/ci.sh
129129
130+
test-windows-cross:
131+
runs-on: windows-latest
132+
name: test • windows cross
133+
needs: build
134+
# GitHub-hosted max, nix build is very slow without cache
135+
timeout-minutes: 360
136+
137+
steps:
138+
- uses: actions/checkout@v6
139+
140+
- name: Download Windows artifacts
141+
uses: actions/download-artifact@v8
142+
with:
143+
name: windows-cross-ucrt-build
144+
path: windows-cross-artifact
145+
146+
- name: Run Windows tests
147+
shell: pwsh
148+
run: ci/scripts/windows_native_test.ps1 -ArtifactRoot windows-cross-artifact
130149
build:
131150
runs-on: ubuntu-latest
132151

@@ -142,7 +161,7 @@ jobs:
142161
strategy:
143162
fail-fast: false
144163
matrix:
145-
config: [default, llvm, gnu32, sanitize, olddeps]
164+
config: [default, llvm, gnu32, sanitize, olddeps, windows]
146165

147166
name: build • ${{ matrix.config }}
148167

@@ -181,3 +200,18 @@ jobs:
181200
env:
182201
CI_CONFIG: ci/configs/${{ matrix.config }}.bash
183202
run: ci/scripts/run.sh
203+
204+
- name: Package Windows artifacts
205+
if: matrix.config == 'windows'
206+
env:
207+
CI_CONFIG: ci/configs/${{ matrix.config }}.bash
208+
run: |
209+
source "$CI_CONFIG"
210+
nix develop --ignore-environment --keep CI_CONFIG "${NIX_ARGS[@]+"${NIX_ARGS[@]}"}" -f shell.nix --command bash ci/scripts/windows_package.sh
211+
212+
- name: Upload Windows artifacts
213+
if: matrix.config == 'windows'
214+
uses: actions/upload-artifact@v7
215+
with:
216+
name: windows-cross-ucrt-build
217+
path: windows-cross-artifact

ci/configs/windows.bash

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
CI_DESC="CI job cross-compiling to Windows (MinGW UCRT) and testing with Wine"
2+
CI_DIR=build-windows
3+
# Cache the cross-toolchain closure to avoid rebuilding mingw + wine every run.
4+
CI_CACHE_NIX_STORE=true
5+
6+
# Wine needs a writable prefix and XDG_RUNTIME_DIR to talk to its services.
7+
# Pre-create the prefix outside of the nix shell so we can plumb it through
8+
# (the shell uses --ignore-environment).
9+
export WINEPREFIX="${WINEPREFIX:-$HOME/.wine-libmultiprocess}"
10+
export WINEARCH="${WINEARCH:-win64}"
11+
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
12+
mkdir -p "$WINEPREFIX"
13+
# Silence wine's verbose warnings about missing Windows features during tests.
14+
export WINEDEBUG=-all
15+
16+
NIX_ARGS=(
17+
--arg minimal true
18+
--arg enableWine true
19+
--arg crossPkgs 'import <nixpkgs> { crossSystem = { config = "x86_64-w64-mingw32"; libc = "ucrt"; }; }'
20+
# Wine stores its prefix under $HOME; preserve HOME so wineboot can initialize.
21+
--keep HOME
22+
--keep WINEPREFIX
23+
--keep WINEARCH
24+
--keep WINEDEBUG
25+
--keep XDG_RUNTIME_DIR
26+
)
27+
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wa,-mbig-obj"
28+
29+
# When sourced from inside the nix shell (during ci.sh), initialize wine and
30+
# pick up native capnp tools to use for build-time code generation. These
31+
# steps are no-ops when sourced from outside the shell (e.g. by run.sh).
32+
if command -v wineboot >/dev/null 2>&1; then
33+
wineboot --init >/dev/null 2>&1 || true
34+
fi
35+
CAPNP_NATIVE=$(command -v capnp 2>/dev/null || true)
36+
CAPNPC_CXX_NATIVE=$(command -v capnpc-c++ 2>/dev/null || true)
37+
38+
CMAKE_ARGS=(
39+
-G Ninja
40+
# Tell CMake we're targeting Windows so FindThreads picks Win32 threads
41+
# and other platform checks behave correctly.
42+
-DCMAKE_SYSTEM_NAME=Windows
43+
-DCMAKE_SYSTEM_PROCESSOR=x86_64
44+
# Run target-arch executables (mpgen, capnpc, mptest) through wine64.
45+
-DCMAKE_CROSSCOMPILING_EMULATOR=wine64
46+
# Avoid pulling in libgcc_s_seh-1.dll and libstdc++-6.dll at runtime.
47+
-DCMAKE_EXE_LINKER_FLAGS="-static-libgcc -static-libstdc++"
48+
# Use native capnp/capnpc-c++ for build-time code generation so cmake
49+
# doesn't try to exec target-arch .exe binaries directly.
50+
${CAPNP_NATIVE:+-DCAPNP_EXECUTABLE=$CAPNP_NATIVE}
51+
${CAPNPC_CXX_NATIVE:+-DCAPNPC_CXX_EXECUTABLE=$CAPNPC_CXX_NATIVE}
52+
)
53+
BUILD_ARGS=(-k 0)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
From a2deb0511dba79f90373c3dc5d45983a574bccfa Mon Sep 17 00:00:00 2001
2+
From: =?UTF-8?q?Bj=C3=B6rn=20Sch=C3=A4pers?= <bjoern@hazardy.de>
3+
Date: Sun, 27 Jul 2025 21:27:59 +0200
4+
Subject: [PATCH] KJ: Move cidr to kj-async
5+
6+
This is where it belongs.
7+
8+
Backported to v1.1.0 (CMake hunks only) so that on Windows the cidr.c++
9+
inet_pton/inet_ntop calls land in the kj-async DLL, which already links
10+
ws2_32. Without this fix, libkj.dll fails to link on mingw with
11+
"undefined reference to __imp_inet_pton".
12+
---
13+
c++/src/kj/CMakeLists.txt | 3 +--
14+
1 file changed, 1 insertion(+), 2 deletions(-)
15+
16+
diff --git a/c++/src/kj/CMakeLists.txt b/c++/src/kj/CMakeLists.txt
17+
--- a/c++/src/kj/CMakeLists.txt
18+
+++ b/c++/src/kj/CMakeLists.txt
19+
@@ -3,7 +3,6 @@
20+
21+
set(kj_sources_lite
22+
array.c++
23+
- cidr.c++
24+
list.c++
25+
common.c++
26+
debug.c++
27+
@@ -40,7 +39,6 @@ else()
28+
endif()
29+
30+
set(kj_headers
31+
- cidr.h
32+
common.h
33+
units.h
34+
memory.h
35+
@@ -125,6 +123,7 @@ set(kj-async_sources
36+
async-io-win32.c++
37+
async-io.c++
38+
async-io-unix.c++
39+
+ cidr.c++
40+
timer.c++
41+
)
42+
set(kj-async_headers
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
From: libmultiprocess CI <ci@invalid>
2+
Subject: [PATCH] kj: Treat ERROR_INVALID_FUNCTION from
3+
GetFileInformationByHandleEx as non-fatal
4+
5+
Newer Wine versions return ERROR_INVALID_FUNCTION (1) instead of
6+
ERROR_CALL_NOT_IMPLEMENTED (120) when an unsupported FileInfoClass is
7+
queried via NtQueryInformationFile. The existing fallback only matched
8+
ERROR_CALL_NOT_IMPLEMENTED, so cap'n proto's KJ filesystem layer
9+
threw an uncaught exception when running cross-compiled capnp.exe under
10+
recent Wine. Treat ERROR_INVALID_FUNCTION the same way: skip the sparse
11+
file space-usage query and continue.
12+
13+
Forwarded: not-needed (CI-only build)
14+
---
15+
c++/src/kj/filesystem-disk-win32.c++ | 3 +++
16+
1 file changed, 3 insertions(+)
17+
18+
--- a/c++/src/kj/filesystem-disk-win32.c++
19+
+++ b/c++/src/kj/filesystem-disk-win32.c++
20+
@@ -380,6 +380,9 @@
21+
case ERROR_CALL_NOT_IMPLEMENTED:
22+
// Probably WINE.
23+
break;
24+
+ case ERROR_INVALID_FUNCTION:
25+
+ // Probably newer WINE, which returns this instead of ERROR_CALL_NOT_IMPLEMENTED.
26+
+ break;
27+
case ERROR_INVALID_PARAMETER:
28+
// Probably VeraCrypt. See https://github.com/capnproto/capnproto/issues/2176
29+
break;

ci/scripts/windows_native_test.ps1

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
param(
2+
[Parameter(Mandatory = $true)]
3+
[string]$ArtifactRoot
4+
)
5+
6+
# Copyright (c) The Bitcoin Core developers
7+
# Distributed under the MIT software license, see the accompanying
8+
# file COPYING or https://opensource.org/license/mit/.
9+
10+
$ErrorActionPreference = "Stop"
11+
# Prevent PowerShell 7 from turning native-command stderr into a terminating
12+
# error before we get a chance to see it.
13+
$PSNativeCommandUseErrorActionPreference = $false
14+
15+
$artifactPath = (Resolve-Path $ArtifactRoot).Path
16+
$env:PATH = "$artifactPath;$env:PATH"
17+
18+
$mptest = Join-Path $artifactPath "test\mptest.exe"
19+
& $mptest 2>&1 | ForEach-Object { "$_" }
20+
$code = $LASTEXITCODE
21+
Write-Host ("mptest exit code: {0} (0x{0:X8})" -f $code)
22+
if ($code -ne 0) {
23+
exit $code
24+
}
25+
26+
$exampleDir = Join-Path $artifactPath "example"
27+
Push-Location $exampleDir
28+
try {
29+
$output = "2+2`nexit`n" | & ".\mpexample.exe" 2>&1 | Out-String
30+
Write-Host $output
31+
if ($LASTEXITCODE -ne 0) {
32+
exit $LASTEXITCODE
33+
}
34+
if ($output -notmatch "mpprinter:" -or $output -notmatch "Bye!") {
35+
throw "Unexpected mpexample output."
36+
}
37+
} finally {
38+
Pop-Location
39+
}

ci/scripts/windows_package.sh

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Copyright (c) The Bitcoin Core developers
4+
# Distributed under the MIT software license, see the accompanying
5+
# file COPYING or https://opensource.org/license/mit/.
6+
#
7+
# Package cross-compiled Windows binaries + runtime DLLs into a directory
8+
# suitable for uploading as a workflow artifact. Must be run inside the
9+
# nix shell produced by `ci/configs/windows.bash` so the mingw toolchain
10+
# (objdump, g++) is on PATH.
11+
12+
export LC_ALL=C.UTF-8
13+
14+
set -o errexit -o nounset -o pipefail -o xtrace
15+
16+
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
17+
readonly REPO_DIR="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
18+
19+
[ "${CI_CONFIG+x}" ] && source "$CI_CONFIG"
20+
: "${CI_DIR:=build-windows}"
21+
22+
readonly BUILD_DIR="${REPO_DIR}/${CI_DIR}"
23+
readonly ARTIFACT_DIR="${REPO_DIR}/windows-cross-artifact"
24+
25+
# Nix's cross stdenv exports these; fall back to the canonical names if not.
26+
: "${CXX:=x86_64-w64-mingw32-g++}"
27+
: "${OBJDUMP:=x86_64-w64-mingw32-objdump}"
28+
29+
copy_artifact_file() {
30+
install -D -m 0755 "$1" "${ARTIFACT_DIR}/$2"
31+
}
32+
33+
copy_runtime_dlls() {
34+
local exe="$1"
35+
local dll dll_path
36+
while read -r dll; do
37+
[[ -n "${dll}" ]] || continue
38+
# Skip DLLs that ship with Windows / Wine.
39+
case "${dll}" in
40+
ADVAPI32.dll|COMBASE.dll|COMCTL32.dll|GDI32.dll|KERNEL32.dll|OLE32.dll|OLEAUT32.dll|RPCRT4.dll|SHELL32.dll|UCRTBASE.dll|USER32.dll|WS2_32.dll)
41+
continue
42+
;;
43+
esac
44+
dll_path="$(${CXX} -print-file-name="${dll}")"
45+
if [[ "${dll_path}" == "${dll}" || ! -f "${dll_path}" ]]; then
46+
case "${dll}" in
47+
lib*.dll)
48+
echo "Could not locate runtime DLL ${dll}." >&2
49+
exit 1
50+
;;
51+
*)
52+
continue
53+
;;
54+
esac
55+
fi
56+
install -D -m 0755 "${dll_path}" "${ARTIFACT_DIR}/${dll}"
57+
done < <(${OBJDUMP} -p "${exe}" | awk '/DLL Name: / {print $3}' | sort -u)
58+
}
59+
60+
rm -rf "${ARTIFACT_DIR}"
61+
62+
readonly EXES=(
63+
test/mptest.exe
64+
example/mpexample.exe
65+
example/mpcalculator.exe
66+
example/mpprinter.exe
67+
)
68+
69+
for exe in "${EXES[@]}"; do
70+
copy_artifact_file "${BUILD_DIR}/${exe}" "${exe}"
71+
done
72+
73+
for exe in "${EXES[@]}"; do
74+
copy_runtime_dlls "${BUILD_DIR}/${exe}"
75+
done

shell.nix

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
, capnprotoSanitizers ? null # Optional sanitizers to build cap'n proto with
77
, cmakeVersion ? null
88
, libcxxSanitizers ? null # Optional LLVM_USE_SANITIZER value to use for libc++, see https://llvm.org/docs/CMake.html
9+
, enableWine ? false # Whether to add wine64 for running cross-compiled Windows binaries
910
}:
1011

1112
let
@@ -42,7 +43,24 @@ let
4243
} // (lib.optionalAttrs (lib.versionOlder capnprotoVersion "0.10") {
4344
env = { }; # Drop -std=c++20 flag forced by nixpkgs
4445
}));
45-
capnproto = (capnprotoBase.overrideAttrs (old: lib.optionalAttrs (capnprotoSanitizers != null) {
46+
# capnproto v1.1.0 puts cidr.c++ in libkj, but it calls inet_pton/inet_ntop
47+
# which require ws2_32 on Windows -- only kj-async links ws2_32. Apply the
48+
# upstream fix that moves cidr.c++ into kj-async (capnproto@a2deb05).
49+
# mingw with mcf thread model also requires _WIN32_WINNT to be defined
50+
# before any libstdc++ thread headers are included.
51+
capnprotoPatched = capnprotoBase.overrideAttrs (old: lib.optionalAttrs crossPkgs.stdenv.hostPlatform.isMinGW {
52+
patches = (old.patches or []) ++ [
53+
./ci/patches/capnproto-cidr-kj-async.patch
54+
./ci/patches/capnproto-wine-invalid-function.patch
55+
];
56+
env = (old.env or { }) // {
57+
NIX_CFLAGS_COMPILE = lib.concatStringsSep " " [
58+
(old.env.NIX_CFLAGS_COMPILE or "")
59+
"-D_WIN32_WINNT=0x0601"
60+
];
61+
};
62+
});
63+
capnproto = (capnprotoPatched.overrideAttrs (old: lib.optionalAttrs (capnprotoSanitizers != null) {
4664
env = (old.env or { }) // {
4765
CXXFLAGS =
4866
lib.concatStringsSep " " [
@@ -52,7 +70,14 @@ let
5270
"-g"
5371
];
5472
};
55-
})).override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; });
73+
})).override (
74+
if enableLibcxx then { clangStdenv = llvm.libcxxStdenv; }
75+
# nixpkgs forces capnproto to be built with clangStdenv, but the mingw
76+
# clang wrapper auto-adds `-lgcc_s` to the link line, which doesn't exist
77+
# in the mingw GCC runtime layout (see nixpkgs#177129). Fall back to the
78+
# GCC cross stdenv when cross-compiling to mingw.
79+
else if crossPkgs.stdenv.hostPlatform.isMinGW then { clangStdenv = crossPkgs.stdenv; }
80+
else { });
5681
clang = if enableLibcxx then llvm.libcxxClang else llvm.clang;
5782
clang-tools = llvm.clang-tools.override { inherit enableLibcxx; };
5883
cmakeHashes = {
@@ -66,7 +91,7 @@ let
6691
};
6792
patches = [];
6893
})).override { isMinimalBuild = true; };
69-
in crossPkgs.mkShell {
94+
in crossPkgs.mkShell ({
7095
buildInputs = [
7196
capnproto
7297
];
@@ -77,8 +102,29 @@ in crossPkgs.mkShell {
77102
] ++ lib.optionals (!minimal) [
78103
clang
79104
clang-tools
80-
];
105+
] ++ lib.optional enableWine pkgs.wineWowPackages.stable
106+
# When cross-compiling, also expose a native capnp/capnpc-c++ on PATH so
107+
# build-time code generators (capnp_generate_cpp) can run on the build host
108+
# instead of trying to execute target-arch binaries directly.
109+
++ lib.optional (crossPkgs.stdenv.hostPlatform != crossPkgs.stdenv.buildPlatform) pkgs.capnproto;
81110

82111
# Tell IWYU where its libc++ mapping lives
83112
IWYU_MAPPING_FILE = if enableLibcxx then "${llvm.libcxx.dev}/include/c++/v1/libcxx.imp" else null;
84-
}
113+
} // lib.optionalAttrs (enableWine && crossPkgs.stdenv.hostPlatform.isMinGW) {
114+
# Cross-compiled .exe files run under wine64 need the capnproto and mingw
115+
# thread runtime DLLs at startup. Wine searches the .exe directory and the
116+
# Windows system directory for PE imports, so symlink the required DLLs
117+
# into $WINEPREFIX/drive_c/windows/system32 when entering the shell.
118+
shellHook = ''
119+
if [ -n "''${WINEPREFIX-}" ]; then
120+
_mp_sys32="$WINEPREFIX/drive_c/windows/system32"
121+
mkdir -p "$_mp_sys32"
122+
for _d in ${capnproto}/bin ${crossPkgs.windows.mcfgthreads}/bin; do
123+
for _dll in "$_d"/*.dll; do
124+
[ -e "$_dll" ] && ln -sf "$_dll" "$_mp_sys32/"
125+
done
126+
done
127+
unset _mp_sys32 _d _dll
128+
fi
129+
'';
130+
})

0 commit comments

Comments
 (0)