Skip to content

Commit 58be7b8

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

7 files changed

Lines changed: 346 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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
# Runtime DLLs for cross-compiled dependencies live in the sibling bin/
34+
# directory of each -L path the cross stdenv adds to NIX_LDFLAGS (e.g.
35+
# capnproto, mcfgthreads). Collect those bin/ dirs once for dll lookup.
36+
EXTRA_DLL_DIRS=()
37+
for flag in ${NIX_LDFLAGS:-}; do
38+
case "${flag}" in
39+
-L*)
40+
candidate="${flag#-L}/../bin"
41+
[[ -d "${candidate}" ]] && EXTRA_DLL_DIRS+=("${candidate}")
42+
;;
43+
esac
44+
done
45+
46+
copy_runtime_dlls() {
47+
local exe="$1"
48+
local dll dll_path dir
49+
while read -r dll; do
50+
[[ -n "${dll}" ]] || continue
51+
# Skip DLLs that ship with Windows / Wine.
52+
case "${dll}" in
53+
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)
54+
continue
55+
;;
56+
esac
57+
dll_path="$(${CXX} -print-file-name="${dll}")"
58+
if [[ "${dll_path}" == "${dll}" || ! -f "${dll_path}" ]]; then
59+
dll_path=""
60+
for dir in "${EXTRA_DLL_DIRS[@]+"${EXTRA_DLL_DIRS[@]}"}"; do
61+
if [[ -f "${dir}/${dll}" ]]; then
62+
dll_path="${dir}/${dll}"
63+
break
64+
fi
65+
done
66+
fi
67+
if [[ -z "${dll_path}" || ! -f "${dll_path}" ]]; then
68+
case "${dll}" in
69+
lib*.dll)
70+
echo "Could not locate runtime DLL ${dll}." >&2
71+
exit 1
72+
;;
73+
*)
74+
continue
75+
;;
76+
esac
77+
fi
78+
install -D -m 0755 "${dll_path}" "${ARTIFACT_DIR}/${dll}"
79+
done < <(${OBJDUMP} -p "${exe}" | awk '/DLL Name: / {print $3}' | sort -u)
80+
}
81+
82+
rm -rf "${ARTIFACT_DIR}"
83+
84+
readonly EXES=(
85+
test/mptest.exe
86+
example/mpexample.exe
87+
example/mpcalculator.exe
88+
example/mpprinter.exe
89+
)
90+
91+
for exe in "${EXES[@]}"; do
92+
copy_artifact_file "${BUILD_DIR}/${exe}" "${exe}"
93+
done
94+
95+
for exe in "${EXES[@]}"; do
96+
copy_runtime_dlls "${BUILD_DIR}/${exe}"
97+
done

0 commit comments

Comments
 (0)