Skip to content
Draft
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
72 changes: 66 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,29 @@ jobs:
export PATH="$(brew --prefix llvm)/bin:$PATH"
CI_CONFIG="ci/configs/macos.bash" bash ci/scripts/ci.sh

test-windows-cross:
runs-on: windows-latest
name: test • windows runner
needs: build-windows-cross
timeout-minutes: 30

steps:
- uses: actions/checkout@v6

- name: Download Windows artifacts
uses: actions/download-artifact@v8
with:
name: windows-cross-build
path: windows-cross-artifact

- name: Run Windows tests
shell: pwsh
run: ci/scripts/windows_native_test.ps1 -ArtifactRoot windows-cross-artifact

build:
runs-on: ubuntu-latest

env:
env: &nix-build-env
CI_NIXPKGS_CHANNEL: nixos-26.05
CI_NIX_CONFIG: |
keep-env-derivations = true
Expand All @@ -153,15 +172,18 @@ jobs:
name: build • ${{ matrix.config }}

steps:
- uses: actions/checkout@v5
- &nix-checkout
uses: actions/checkout@v5

- name: Determine CI configuration
- &nix-config
name: Determine CI configuration
id: config
env:
CI_CONFIG: ci/configs/${{ matrix.config }}.bash
run: ci/scripts/config.sh

- name: Install Nix
- &nix-install
name: Install Nix
uses: cachix/install-nix-action@v31 # 2025-05-27, from https://github.com/cachix/install-nix-action/tags
with:
# Act executes inside an unprivileged container (Docker or Podman),
Expand All @@ -171,7 +193,8 @@ jobs:
${{ env.CI_NIX_CONFIG }}
${{ github.actor == 'nektos/act' && env.CI_NIX_CONFIG_ACT || '' }}

- name: Cache Nix store
- &nix-cache
name: Cache Nix store
if: steps.config.outputs.cache_nix_store == 'true'
uses: nix-community/cache-nix-action@v7
with:
Expand All @@ -182,7 +205,44 @@ jobs:
nix-${{ runner.os }}-
gc-max-store-size-linux: 10G

- name: Run CI script
- &nix-run
name: Run CI script
env:
CI_CONFIG: ci/configs/${{ matrix.config }}.bash
run: ci/scripts/run.sh

build-windows-cross:
runs-on: ubuntu-latest
env: *nix-build-env

# Keep the shared steps and cache keys using the same configuration context.
strategy:
fail-fast: false
matrix:
config: [windows]

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

steps:
- *nix-checkout
- *nix-config
- *nix-install
- *nix-cache
- *nix-run

- name: Package Windows artifacts
env:
CI_CONFIG: ci/configs/${{ matrix.config }}.bash
run: |
source "$CI_CONFIG"
nix_args=("${NIX_ARGS[@]+"${NIX_ARGS[@]}"}")
if [[ -n "${CI_NIXPKGS_CHANNEL:-}" ]]; then
nix_args+=(-I "nixpkgs=channel:${CI_NIXPKGS_CHANNEL}")
fi
nix develop --ignore-environment --keep CI_CONFIG "${nix_args[@]}" -f shell.nix --command bash ci/scripts/windows_package.sh

- name: Upload Windows artifacts
uses: actions/upload-artifact@v7
with:
name: windows-cross-build
path: windows-cross-artifact
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ target_link_libraries(multiprocess PUBLIC CapnProto::capnp)
target_link_libraries(multiprocess PUBLIC CapnProto::capnp-rpc)
target_link_libraries(multiprocess PUBLIC CapnProto::kj)
target_link_libraries(multiprocess PUBLIC CapnProto::kj-async)
target_link_libraries(multiprocess PUBLIC Threads::Threads)
set_target_properties(multiprocess PROPERTIES
PUBLIC_HEADER "${MP_PUBLIC_HEADERS}")
install(TARGETS multiprocess EXPORT LibTargets
Expand Down
1 change: 1 addition & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ CI_CONFIG=ci/configs/gnu32.bash ci/scripts/run.sh
CI_CONFIG=ci/configs/sanitize.bash ci/scripts/run.sh
CI_CONFIG=ci/configs/olddeps.bash ci/scripts/run.sh
CI_CONFIG=ci/configs/newdeps.bash ci/scripts/run.sh
CI_CONFIG=ci/configs/windows.bash ci/scripts/run.sh
```

By default CI jobs will reuse their build directories. `CI_CLEAN=1` can be specified to delete them before running instead.
Expand Down
21 changes: 21 additions & 0 deletions ci/configs/windows.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CI_DESC="CI job cross-compiling to Windows with MinGW, tested with Wine"
CI_DIR=build-windows
CI_CACHE_NIX_STORE=true
NIX_ARGS=(
--arg windows true
--arg minimal true
)
CMAKE_ARGS=(
-G Ninja
-DCMAKE_SYSTEM_NAME=Windows
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER
-DCMAKE_CROSSCOMPILING_EMULATOR=wine
# -Wa,-mbig-obj: template-heavy C++ generates >32K COFF sections per .obj;
# BigCOFF format raises the limit. Must be passed to the assembler via -Wa.
"-DCMAKE_CXX_FLAGS=-Wa,-mbig-obj"
)
# CXX is set by the nix cross shell to the mingw g++ wrapper; cmake picks
# it up from the environment, so no CMAKE_CXX_COMPILER override needed.
BUILD_ARGS=(-k 0)
# Build native mpgen first (as a code generator for cross-compiled test/example targets).
MPGEN_PRE_BUILD=1
52 changes: 52 additions & 0 deletions ci/scripts/ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,49 @@ if [ -n "${CAPNP_CHECKOUT-}" ]; then
fi

src_dir=$PWD

# If cross-compiling, build native mpgen first so it can be used as a code
# generator when cmake invokes it from add_custom_command (which does not go
# through CMAKE_CROSSCOMPILING_EMULATOR, unlike add_test executables).
if [ -n "${MPGEN_PRE_BUILD-}" ]; then
native_dir="${src_dir}/${CI_DIR}-native"
[ -n "${CI_CLEAN-}" ] && rm -rf "$native_dir"
mkdir -p "$native_dir"
# Unset cross-compilation env vars so cmake uses the native compiler and
# does a native (not cross) build. Key vars to clear:
# CXX/CC/AR/RANLIB/LD - set to cross-compiler by the cross shell
# cmakeFlags - nix cross shell injects -DCMAKE_SYSTEM_NAME=Windows etc.
# Pass NATIVE_CAPNPROTO_PREFIX as CMAKE_PREFIX_PATH so cmake finds the
# native Cap'n Proto rather than the cross-compiled one.
native_cmake_args=()
if [ -n "${NATIVE_CAPNPROTO_PREFIX-}" ]; then
# Build a cmake prefix path with native capnproto and its dependencies
# (openssl, zlib) so find_package and find_dependency succeed.
native_prefix="${NATIVE_CAPNPROTO_PREFIX}"
[ -n "${NATIVE_OPENSSL_DEV-}" ] && native_prefix="${native_prefix};${NATIVE_OPENSSL_DEV}"
[ -n "${NATIVE_OPENSSL_LIB-}" ] && native_prefix="${native_prefix};${NATIVE_OPENSSL_LIB}"
[ -n "${NATIVE_ZLIB_DEV-}" ] && native_prefix="${native_prefix};${NATIVE_ZLIB_DEV}"
[ -n "${NATIVE_ZLIB_LIB-}" ] && native_prefix="${native_prefix};${NATIVE_ZLIB_LIB}"
native_cmake_args+=(
"-DCMAKE_PREFIX_PATH=${native_prefix}"
"-DCapnProto_DIR=${NATIVE_CAPNPROTO_PREFIX}/lib/cmake/CapnProto"
)
fi
# -static-libstdc++ / -static-libgcc: the native mpgen must run inside the
# cross nix shell where the native libstdc++.so may not be in LD_LIBRARY_PATH.
native_cmake_args+=("-DCMAKE_EXE_LINKER_FLAGS=-static-libstdc++ -static-libgcc")
(cd "$native_dir" && env -u CXX -u CC -u AR -u RANLIB -u LD -u cmakeFlags cmake "$src_dir" "${native_cmake_args[@]+${native_cmake_args[@]}}" && cmake --build . -t mpgen)
CMAKE_ARGS+=("-DMPGEN_EXECUTABLE=${native_dir}/mpgen")

# Override capnp tool executables: the cross capnproto cmake config sets
# CAPNP_EXECUTABLE to capnp.exe (Windows binary), which can't run on Linux.
# Use the native capnp/capnpc-c++ binaries from pkgs.capnproto in nativeBuildInputs.
_capnp=$(command -v capnp 2>/dev/null || true)
_capnpc=$(command -v capnpc-c++ 2>/dev/null || true)
[ -n "$_capnp" ] && CMAKE_ARGS+=("-DCAPNP_EXECUTABLE=$_capnp")
[ -n "$_capnpc" ] && CMAKE_ARGS+=("-DCAPNPC_CXX_EXECUTABLE=$_capnpc")
fi

mkdir -p "$CI_DIR"
cd "$CI_DIR"
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
Expand Down Expand Up @@ -67,4 +110,13 @@ else
cmake --build . --target "$t" -- "${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}"
done
fi
# When cross-compiling for Windows, copy GCC and MCF thread runtime DLLs
# alongside the test executables so wine can find them (wine DLL loading
# checks the executable's directory first, before any search-path logic).
if [ -n "${WIN_RUNTIME_DLLS-}" ]; then
IFS=: read -ra _dll_dirs <<< "$WIN_RUNTIME_DLLS"
for _dir in "${_dll_dirs[@]}"; do
find "$_dir" -maxdepth 1 -name "*.dll" -exec cp -n {} test/ \;
done
fi
ctest --output-on-failure
39 changes: 39 additions & 0 deletions ci/scripts/windows_native_test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
param(
[Parameter(Mandatory = $true)]
[string]$ArtifactRoot
)

# Copyright (c) The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/license/mit/.

$ErrorActionPreference = "Stop"
# Prevent PowerShell 7 from turning native-command stderr into a terminating
# error before we get a chance to see it.
$PSNativeCommandUseErrorActionPreference = $false

$artifactPath = (Resolve-Path $ArtifactRoot).Path
$env:PATH = "$artifactPath;$env:PATH"

$mptest = Join-Path $artifactPath "test\mptest.exe"
& $mptest 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
Write-Host ("mptest exit code: {0} (0x{0:X8})" -f $code)
if ($code -ne 0) {
exit $code
}

$exampleDir = Join-Path $artifactPath "example"
Push-Location $exampleDir
try {
$output = "2+2`nexit`n" | & ".\mpexample.exe" 2>&1 | Out-String
Write-Host $output
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if ($output -notmatch "mpprinter:" -or $output -notmatch "Bye!") {
throw "Unexpected mpexample output."
}
} finally {
Pop-Location
}
104 changes: 104 additions & 0 deletions ci/scripts/windows_package.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
#
# Copyright (c) The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/license/mit/.
#
# Package cross-compiled Windows binaries + runtime DLLs into a directory
# suitable for uploading as a workflow artifact. Must be run inside the
# nix shell produced by `ci/configs/windows.bash` so the mingw toolchain
# (objdump, g++) is on PATH.

export LC_ALL=C.UTF-8

set -o errexit -o nounset -o pipefail -o xtrace

readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly REPO_DIR="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"

[ "${CI_CONFIG+x}" ] && source "$CI_CONFIG"
: "${CI_DIR:=build-windows}"

readonly BUILD_DIR="${REPO_DIR}/${CI_DIR}"
readonly ARTIFACT_DIR="${REPO_DIR}/windows-cross-artifact"

# Nix's cross stdenv exports these; fall back to the canonical names if not.
: "${CXX:=x86_64-w64-mingw32-g++}"
: "${OBJDUMP:=x86_64-w64-mingw32-objdump}"

copy_artifact_file() {
install -D -m 0755 "$1" "${ARTIFACT_DIR}/$2"
}

# Runtime DLLs for cross-compiled dependencies live in the sibling bin/
# directory of each -L path the cross stdenv adds to NIX_LDFLAGS (e.g.
# capnproto, mcfgthreads). Collect those bin/ dirs once for dll lookup.
EXTRA_DLL_DIRS=("${BUILD_DIR}/test")
if [[ -n "${WIN_RUNTIME_DLLS:-}" ]]; then
IFS=: read -r -a runtime_dirs <<< "$WIN_RUNTIME_DLLS"
EXTRA_DLL_DIRS+=("${runtime_dirs[@]}")
fi
for flag in ${NIX_LDFLAGS:-} ${NIX_LDFLAGS_x86_64_w64_mingw32:-}; do
case "${flag}" in
-L*)
candidate="${flag#-L}/../bin"
[[ -d "${candidate}" ]] && EXTRA_DLL_DIRS+=("${candidate}")
;;
esac
done

copy_runtime_dlls() {
local exe="$1"
local dll dll_path dir
while read -r dll; do
[[ -n "${dll}" ]] || continue
# Skip DLLs that ship with Windows / Wine.
case "${dll}" in
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)
continue
;;
esac
# Already copied dependencies also break cycles between DLLs.
[[ -f "${ARTIFACT_DIR}/${dll}" ]] && continue
dll_path="$(${CXX} -print-file-name="${dll}")"
if [[ "${dll_path}" == "${dll}" || ! -f "${dll_path}" ]]; then
dll_path=""
for dir in "${EXTRA_DLL_DIRS[@]+"${EXTRA_DLL_DIRS[@]}"}"; do
if [[ -f "${dir}/${dll}" ]]; then
dll_path="${dir}/${dll}"
break
fi
done
fi
if [[ -z "${dll_path}" || ! -f "${dll_path}" ]]; then
case "${dll}" in
lib*.dll)
echo "Could not locate runtime DLL ${dll}." >&2
exit 1
;;
*)
continue
;;
esac
fi
install -D -m 0755 "${dll_path}" "${ARTIFACT_DIR}/${dll}"
copy_runtime_dlls "${dll_path}"
done < <(${OBJDUMP} -p "${exe}" | awk '/DLL Name: / {print $3}' | sort -u)
}

rm -rf "${ARTIFACT_DIR}"

readonly EXES=(
test/mptest.exe
example/mpexample.exe
example/mpcalculator.exe
example/mpprinter.exe
)

for exe in "${EXES[@]}"; do
copy_artifact_file "${BUILD_DIR}/${exe}" "${exe}"
done

for exe in "${EXES[@]}"; do
copy_runtime_dlls "${BUILD_DIR}/${exe}"
done
5 changes: 3 additions & 2 deletions example/example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cstring> // IWYU pragma: keep
#include <filesystem>
#include <fstream>
#include <functional>
#include <future>
#include <iostream>
#include <kj/common.h>
Expand All @@ -26,11 +27,11 @@ namespace fs = std::filesystem;

static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
{
const auto [pid, socket] = mp::SpawnProcess([&](mp::SpawnConnectInfo info) -> std::vector<std::string> {
const auto [pid, socket] = mp::SpawnProcess([&](std::string connect_info) -> std::vector<std::string> {
fs::path path = process_argv0;
path.remove_filename();
path.append(new_exe_name);
return {path.string(), std::move(info)};
return {path.string(), std::move(connect_info)};
});
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, mp::MakeStream(loop, socket)), pid);
}
Expand Down
Loading
Loading