Skip to content

Commit d97c586

Browse files
committed
ci: Add Windows cross-build and native test jobs
Adds two CI jobs and the supporting nix/shell plumbing to build libmultiprocess for Windows and run mptest: - windows-cross: nix-based mingw-w64 (UCRT) cross-build on Linux, followed by mptest.exe under wine-wow. - windows-native: native MSVC build + ctest on a Windows runner, driven by ci/scripts/windows_native_test.ps1. The cross job pins cap'n proto to v1.4.0 (v1.3.0+ includes the upstream fix moving cidr.c++ into kj-async, so the previously-required local patch is dropped) and uses a matching native capnpc helper (capnprotoNative) so build-time generated headers match the cross library version. A small wine-invalid-function patch is applied to capnp on the cross build only: capnp's DiskHandle::stat() calls GetFileInformationByHandleEx(FileCompressionInfo), which Wine answers with ERROR_INVALID_FUNCTION (its NTSTATUS->DOS mapping for the unsupported info class). capnp's existing fallback only tolerated ERROR_CALL_NOT_IMPLEMENTED -- a guess that has sat unverified in the filesystem-disk-win32 backend since 2017 and that no project appears to have actually exercised under Wine before -- so without the patch every mp::Connection setup that touches a temp file throws. See the patch header for details.
1 parent 7fd5ec4 commit d97c586

6 files changed

Lines changed: 328 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: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
# Pin capnproto v1.4.0: v1.3.0 includes the upstream fix that moves
20+
# cidr.c++ into kj-async (capnproto@a2deb05) so we no longer need a local
21+
# patch for that, and v1.4.0 is the current stable release.
22+
--arg capnprotoVersion '"1.4.0"'
23+
--arg crossPkgs 'import <nixpkgs> { crossSystem = { config = "x86_64-w64-mingw32"; libc = "ucrt"; }; }'
24+
# Wine stores its prefix under $HOME; preserve HOME so wineboot can initialize.
25+
--keep HOME
26+
--keep WINEPREFIX
27+
--keep WINEARCH
28+
--keep WINEDEBUG
29+
--keep XDG_RUNTIME_DIR
30+
)
31+
export CXXFLAGS="-Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wa,-mbig-obj"
32+
33+
# When sourced from inside the nix shell (during ci.sh), initialize wine and
34+
# pick up native capnp tools to use for build-time code generation. These
35+
# steps are no-ops when sourced from outside the shell (e.g. by run.sh).
36+
if command -v wineboot >/dev/null 2>&1; then
37+
wineboot --init >/dev/null 2>&1 || true
38+
fi
39+
CAPNP_NATIVE=$(command -v capnp 2>/dev/null || true)
40+
CAPNPC_CXX_NATIVE=$(command -v capnpc-c++ 2>/dev/null || true)
41+
42+
CMAKE_ARGS=(
43+
-G Ninja
44+
# Tell CMake we're targeting Windows so FindThreads picks Win32 threads
45+
# and other platform checks behave correctly.
46+
-DCMAKE_SYSTEM_NAME=Windows
47+
-DCMAKE_SYSTEM_PROCESSOR=x86_64
48+
# Run target-arch executables (mpgen, capnpc, mptest) through wine64.
49+
-DCMAKE_CROSSCOMPILING_EMULATOR=wine64
50+
# Avoid pulling in libgcc_s_seh-1.dll and libstdc++-6.dll at runtime.
51+
-DCMAKE_EXE_LINKER_FLAGS="-static-libgcc -static-libstdc++"
52+
# Use native capnp/capnpc-c++ for build-time code generation so cmake
53+
# doesn't try to exec target-arch .exe binaries directly.
54+
${CAPNP_NATIVE:+-DCAPNP_EXECUTABLE=$CAPNP_NATIVE}
55+
${CAPNPC_CXX_NATIVE:+-DCAPNPC_CXX_EXECUTABLE=$CAPNPC_CXX_NATIVE}
56+
)
57+
BUILD_ARGS=(-k 0)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
From: libmultiprocess CI <ci@invalid>
2+
Subject: [PATCH] kj: Treat ERROR_INVALID_FUNCTION from
3+
GetFileInformationByHandleEx as non-fatal
4+
5+
Wine's NtQueryInformationFile returns STATUS_NOT_IMPLEMENTED for any
6+
FILE_INFORMATION_CLASS it does not handle (see the default case in
7+
dlls/ntdll/unix/file.c:NtQueryInformationFile in the wine source).
8+
RtlNtStatusToDosError maps STATUS_NOT_IMPLEMENTED (0xC0000002) to
9+
ERROR_INVALID_FUNCTION (1), so GetFileInformationByHandleEx returns 1
10+
when called with FileCompressionInfo (and other classes Wine doesn't
11+
implement) instead of the ERROR_CALL_NOT_IMPLEMENTED (120) that the
12+
existing fallback expects. Without this, cap'n proto's KJ filesystem
13+
layer throws an uncaught exception when running cross-compiled capnp.exe
14+
under Wine. Treat ERROR_INVALID_FUNCTION the same way: skip the sparse
15+
file space-usage query and continue.
16+
17+
Forwarded: https://github.com/capnproto/capnproto/pull/2633
18+
Drop once that PR is merged and a release containing it is pinned.
19+
---
20+
c++/src/kj/filesystem-disk-win32.c++ | 5 +++++
21+
1 file changed, 5 insertions(+)
22+
23+
--- a/c++/src/kj/filesystem-disk-win32.c++
24+
+++ b/c++/src/kj/filesystem-disk-win32.c++
25+
@@ -380,6 +380,11 @@
26+
case ERROR_CALL_NOT_IMPLEMENTED:
27+
// Probably WINE.
28+
break;
29+
+ case ERROR_INVALID_FUNCTION:
30+
+ // Also WINE: NtQueryInformationFile returns STATUS_NOT_IMPLEMENTED for
31+
+ // unsupported FileInfoClass values, which RtlNtStatusToDosError maps to
32+
+ // ERROR_INVALID_FUNCTION (not ERROR_CALL_NOT_IMPLEMENTED).
33+
+ break;
34+
case ERROR_INVALID_PARAMETER:
35+
// Probably VeraCrypt. See https://github.com/capnproto/capnproto/issues/2176
36+
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

shell.nix

Lines changed: 64 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
@@ -29,6 +30,8 @@ let
2930
"1.0.2" = "sha256-LVdkqVBTeh8JZ1McdVNtRcnFVwEJRNjt0JV2l7RkuO8=";
3031
"1.1.0" = "sha256-gxkko7LFyJNlxpTS+CWOd/p9x/778/kNIXfpDGiKM2A=";
3132
"1.2.0" = "sha256-aDcn4bLZGq8915/NPPQsN5Jv8FRWd8cAspkG3078psc=";
33+
"1.3.0" = "sha256-fvZzNDBZr73U+xbj1LhVj1qWZyNmblKluh7lhacV+6I=";
34+
"1.4.0" = "sha256-CuhKOJwU+QG25lRR8F7ina+DV45ZlLzg/UJ2swf2tZ0=";
3235
};
3336
capnprotoBase = if capnprotoVersion == null then crossPkgs.capnproto else crossPkgs.capnproto.overrideAttrs (old: {
3437
version = capnprotoVersion;
@@ -42,7 +45,35 @@ let
4245
} // (lib.optionalAttrs (lib.versionOlder capnprotoVersion "0.10") {
4346
env = { }; # Drop -std=c++20 flag forced by nixpkgs
4447
}));
45-
capnproto = (capnprotoBase.overrideAttrs (old: lib.optionalAttrs (capnprotoSanitizers != null) {
48+
# Native build of the same capnproto version, used as a build-time helper
49+
# when cross-compiling so capnpc generates schemas matching the cross headers.
50+
capnprotoNative = if capnprotoVersion == null then pkgs.capnproto else pkgs.capnproto.overrideAttrs (old: {
51+
version = capnprotoVersion;
52+
src = pkgs.fetchFromGitHub {
53+
owner = "capnproto";
54+
repo = "capnproto";
55+
rev = "v${capnprotoVersion}";
56+
hash = lib.attrByPath [capnprotoVersion] "" capnprotoHashes;
57+
};
58+
patches = lib.optionals (lib.versionAtLeast capnprotoVersion "0.9.0" && lib.versionOlder capnprotoVersion "0.10.4") [ ./ci/patches/spaceship.patch ];
59+
} // (lib.optionalAttrs (lib.versionOlder capnprotoVersion "0.10") {
60+
env = { }; # Drop -std=c++20 flag forced by nixpkgs
61+
}));
62+
# mingw with mcf thread model requires _WIN32_WINNT to be defined before
63+
# any libstdc++ thread headers are included. See the patch header for
64+
# the rationale behind capnproto-wine-invalid-function.patch.
65+
capnprotoPatched = capnprotoBase.overrideAttrs (old: lib.optionalAttrs crossPkgs.stdenv.hostPlatform.isMinGW {
66+
patches = (old.patches or []) ++ [
67+
./ci/patches/capnproto-wine-invalid-function.patch
68+
];
69+
env = (old.env or { }) // {
70+
NIX_CFLAGS_COMPILE = lib.concatStringsSep " " [
71+
(old.env.NIX_CFLAGS_COMPILE or "")
72+
"-D_WIN32_WINNT=0x0601"
73+
];
74+
};
75+
});
76+
capnproto = (capnprotoPatched.overrideAttrs (old: lib.optionalAttrs (capnprotoSanitizers != null) {
4677
env = (old.env or { }) // {
4778
CXXFLAGS =
4879
lib.concatStringsSep " " [
@@ -52,7 +83,14 @@ let
5283
"-g"
5384
];
5485
};
55-
})).override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; });
86+
})).override (
87+
if enableLibcxx then { clangStdenv = llvm.libcxxStdenv; }
88+
# nixpkgs forces capnproto to be built with clangStdenv, but the mingw
89+
# clang wrapper auto-adds `-lgcc_s` to the link line, which doesn't exist
90+
# in the mingw GCC runtime layout (see nixpkgs#177129). Fall back to the
91+
# GCC cross stdenv when cross-compiling to mingw.
92+
else if crossPkgs.stdenv.hostPlatform.isMinGW then { clangStdenv = crossPkgs.stdenv; }
93+
else { });
5694
clang = if enableLibcxx then llvm.libcxxClang else llvm.clang;
5795
clang-tools = llvm.clang-tools.override { inherit enableLibcxx; };
5896
cmakeHashes = {
@@ -66,7 +104,7 @@ let
66104
};
67105
patches = [];
68106
})).override { isMinimalBuild = true; };
69-
in crossPkgs.mkShell {
107+
in crossPkgs.mkShell ({
70108
buildInputs = [
71109
capnproto
72110
];
@@ -77,8 +115,29 @@ in crossPkgs.mkShell {
77115
] ++ lib.optionals (!minimal) [
78116
clang
79117
clang-tools
80-
];
118+
] ++ lib.optional enableWine pkgs.wineWowPackages.stable
119+
# When cross-compiling, also expose a native capnp/capnpc-c++ on PATH so
120+
# build-time code generators (capnp_generate_cpp) can run on the build host
121+
# instead of trying to execute target-arch binaries directly.
122+
++ lib.optional (crossPkgs.stdenv.hostPlatform != crossPkgs.stdenv.buildPlatform) capnprotoNative;
81123

82124
# Tell IWYU where its libc++ mapping lives
83125
IWYU_MAPPING_FILE = if enableLibcxx then "${llvm.libcxx.dev}/include/c++/v1/libcxx.imp" else null;
84-
}
126+
} // lib.optionalAttrs (enableWine && crossPkgs.stdenv.hostPlatform.isMinGW) {
127+
# Cross-compiled .exe files run under wine64 need the capnproto and mingw
128+
# thread runtime DLLs at startup. Wine searches the .exe directory and the
129+
# Windows system directory for PE imports, so symlink the required DLLs
130+
# into $WINEPREFIX/drive_c/windows/system32 when entering the shell.
131+
shellHook = ''
132+
if [ -n "''${WINEPREFIX-}" ]; then
133+
_mp_sys32="$WINEPREFIX/drive_c/windows/system32"
134+
mkdir -p "$_mp_sys32"
135+
for _d in ${capnproto}/bin ${crossPkgs.windows.mcfgthreads}/bin; do
136+
for _dll in "$_d"/*.dll; do
137+
[ -e "$_dll" ] && ln -sf "$_dll" "$_mp_sys32/"
138+
done
139+
done
140+
unset _mp_sys32 _d _dll
141+
fi
142+
'';
143+
})

0 commit comments

Comments
 (0)