From 6fc733512fee76554134a1eba322d1340bd00eeb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 18 Aug 2026 19:07:01 +0200 Subject: [PATCH 1/7] fix: add basic Wine support and test pipeline Make the Windows build compatible with Wine cross-compilation by using portable header casing, limiting MSVC-only abort setup, and accepting missing UBR registry metadata. Cross-compile Windows test binaries with Clang and MinGW-w64 on Ubuntu, then run the test suite through Wine. This exercises actual Windows code paths from Linux CI. Crashpad is skipped because its vendored Windows build does not currently cross-compile with this toolchain; other skips cover host-only harnesses or specific Wine limitations. --- .github/workflows/ci.yml | 10 +++++++ examples/example.c | 2 +- .../sentry_modulefinder_windows.c | 4 +-- src/sentry_os.c | 9 +++---- tests/__init__.py | 20 +++++++++++--- tests/assertions.py | 13 ++++----- tests/build_config.py | 21 ++++++++++++++- tests/cmake.py | 5 +--- tests/conditions.py | 1 + tests/conftest.py | 7 ++++- tests/test_build_static.py | 15 ++++++++--- tests/test_dotnet_signals.py | 14 +++++++++- tests/test_integration_crashpad.py | 13 +++++---- tests/test_integration_http.py | 8 +++++- tests/test_integration_native.py | 27 ++++++++++++------- tests/test_integration_proxy.py | 5 ++-- tests/test_integration_stdout.py | 19 ++++++------- tests/test_stress_inproc.py | 6 ++++- tests/test_unit.py | 5 +++- tests/unit/test_value.c | 6 ++++- 20 files changed, 150 insertions(+), 60 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cd8466e73..9fbdd2a7ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,6 +207,11 @@ jobs: MINGW_PKG_PREFIX: x86_64-w64-mingw32 MINGW_ASM_MASM_COMPILER: llvm-ml MINGW_ASM_MASM_FLAGS: -m64 + - name: Wine (Clang 21 + MinGW-w64) + os: ubuntu-26.04 + CC: clang-21 + CXX: clang++-21 + TEST_WINE: 1 - name: Android (API 21, NDK 23) os: ubuntu-latest ANDROID_API: 21 @@ -237,6 +242,7 @@ jobs: CXX: ${{ matrix.CXX }} TEST_X86: ${{ matrix.TEST_X86 }} TEST_MINGW: ${{ matrix.TEST_MINGW }} + TEST_WINE: ${{ matrix.TEST_WINE }} TEST_ARM32: ${{ matrix.TEST_ARM32 }} TEST_QEMU: ${{ matrix.TEST_QEMU }} USE_SCCACHE: ${{ matrix.USE_SCCACHE }} @@ -295,6 +301,10 @@ jobs: exit 1 fi + - name: Installing Wine Dependencies + if: ${{ env['TEST_WINE'] }} + run: sudo apt install wine mingw-w64 lld + - name: Installing kcov dependencies if: ${{ contains(env['RUN_ANALYZER'], 'kcov') }} run: | diff --git a/examples/example.c b/examples/example.c index 005a8f36dd..b5f99ce72f 100644 --- a/examples/example.c +++ b/examples/example.c @@ -1316,7 +1316,7 @@ main(int argc, char **argv) assert(0); } if (has_arg(argc, argv, "abort")) { -#ifdef _WIN32 +#if defined(_WIN32) && defined(_MSC_VER) // Suppress the Windows abort dialog that would block CI _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); #endif diff --git a/src/modulefinder/sentry_modulefinder_windows.c b/src/modulefinder/sentry_modulefinder_windows.c index ea3d82008c..88f5e4ace7 100644 --- a/src/modulefinder/sentry_modulefinder_windows.c +++ b/src/modulefinder/sentry_modulefinder_windows.c @@ -8,8 +8,8 @@ # include #endif #define PSAPI_VERSION 2 -#include -#include +#include +#include static bool g_initialized = false; static sentry_mutex_t g_mutex = SENTRY__MUTEX_INIT; diff --git a/src/sentry_os.c b/src/sentry_os.c index e1bb18cb94..cd8ed25532 100644 --- a/src/sentry_os.c +++ b/src/sentry_os.c @@ -167,12 +167,11 @@ sentry__get_windows_version(windows_version_t *win_ver) } win_ver->build = strtoul(buf, NULL, 10); + // UBR (Update Build Revision) is optional (not present on Wine) + reg_version = 0; buf_size = sizeof(uint32_t); - if (RegGetValueA(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "UBR", - RRF_RT_REG_DWORD, NULL, ®_version, &buf_size) - != ERROR_SUCCESS) { - return 0; - } + RegGetValueA(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "UBR", RRF_RT_REG_DWORD, + NULL, ®_version, &buf_size); win_ver->ubr = reg_version; return 1; diff --git a/tests/__init__.py b/tests/__init__.py index df299728d7..5aa9af156f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -20,6 +20,19 @@ def adb(*args, **kwargs): ) +def exe_name(name): + if sys.platform == "win32" or os.environ.get("TEST_WINE"): + return f"{name}.exe" + return name + + +def run_command(name): + name = exe_name(name) + if os.environ.get("TEST_WINE"): + return ["wine", name] + return [name] + + # https://docs.pytest.org/en/latest/assert.html#assert-details pytest.register_assert_rewrite("tests.assertions") @@ -191,9 +204,10 @@ def run( ) return child - cmd = [ - "./{}".format(exe) if sys.platform != "win32" else "{}\\{}.exe".format(cwd, exe) - ] + executable = ( + "./{}".format(exe) if sys.platform != "win32" else "{}\\{}".format(cwd, exe) + ) + cmd = run_command(executable) if "asan" in os.environ.get("RUN_ANALYZER", ""): asan_options = env.get("ASAN_OPTIONS", "") if "detect_leaks" not in asan_options: diff --git a/tests/assertions.py b/tests/assertions.py index b5cce57cbc..44ce4f5640 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -13,7 +13,7 @@ import msgpack from . import REPLAY_ID, SENTRY_VERSION -from .conditions import is_android, is_asan, is_tsan +from .conditions import is_android, is_asan, is_tsan, is_wine VERSION_RE = re.compile(r"(\d+\.\d+\.\d+)[-.]?(.*)") @@ -131,11 +131,12 @@ def assert_event_meta( if is_android: expected_sdk["name"] = "sentry.native.android" else: - if sys.platform == "win32": - assert_matches( - event["contexts"]["os"], - {"name": "Windows", "version": platform.version()}, - ) + if sys.platform == "win32" or is_wine: + expected_os_context = {"name": "Windows"} + if not is_wine: + expected_os_context["version"] = platform.version() + assert_matches(event["contexts"]["os"], expected_os_context) + assert event["contexts"]["os"]["version"] is not None assert event["contexts"]["os"]["build"] is not None elif sys.platform == "linux": version = platform.release() diff --git a/tests/build_config.py b/tests/build_config.py index 850a348d9e..1c9670dde6 100644 --- a/tests/build_config.py +++ b/tests/build_config.py @@ -42,12 +42,31 @@ def get_platform_cmake_args(): This handles: - 32-bit builds (TEST_X86) + - Wine cross-builds (TEST_WINE) - ASAN/TSAN sanitizers (RUN_ANALYZER) - Additional CMAKE_DEFINES from environment """ args = [] - if sys.platform == "win32" and os.environ.get("TEST_X86"): + if os.environ.get("TEST_WINE"): + cc = os.environ.get("CC") or "clang" + cxx = os.environ.get("CXX") or "clang++" + linker_flags = "-fuse-ld=lld -static-libgcc -static-libstdc++" + args.extend( + [ + "-DCMAKE_SYSTEM_NAME=Windows", + "-DCMAKE_SYSTEM_VERSION=10.0", + f"-DCMAKE_C_COMPILER={cc}", + "-DCMAKE_C_COMPILER_TARGET=x86_64-w64-windows-gnu", + f"-DCMAKE_CXX_COMPILER={cxx}", + "-DCMAKE_CXX_COMPILER_TARGET=x86_64-w64-windows-gnu", + "-DCMAKE_RC_COMPILER=x86_64-w64-mingw32-windres", + f"-DCMAKE_EXE_LINKER_FLAGS={linker_flags}", + f"-DCMAKE_SHARED_LINKER_FLAGS={linker_flags}", + f"-DCMAKE_MODULE_LINKER_FLAGS={linker_flags}", + ] + ) + elif sys.platform == "win32" and os.environ.get("TEST_X86"): args.append("-AWin32") elif sys.platform == "linux" and os.environ.get("TEST_X86"): args.append("-DSENTRY_BUILD_FORCE32=ON") diff --git a/tests/cmake.py b/tests/cmake.py index 305d784b5c..6885cf4de1 100644 --- a/tests/cmake.py +++ b/tests/cmake.py @@ -8,7 +8,7 @@ import pytest -from . import adb +from . import adb, exe_name from .conditions import has_sccache from .build_config import ( get_android_config, @@ -73,9 +73,6 @@ def destroy(self): if "llvm-cov" in os.environ.get("RUN_ANALYZER", ""): - def exe_name(name): - return name + ".exe" if sys.platform == "win32" else name - def lib_name(name): if sys.platform == "win32": return name + ".dll" diff --git a/tests/conditions.py b/tests/conditions.py index 4e10e4c350..fd7c87955c 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -4,6 +4,7 @@ is_aix = sys.platform == "aix" or sys.platform == "os400" is_android = os.environ.get("ANDROID_API") +is_wine = bool(os.environ.get("TEST_WINE")) is_x86 = os.environ.get("TEST_X86") is_arm32 = bool(os.environ.get("TEST_ARM32")) is_qemu = bool(os.environ.get("TEST_QEMU")) diff --git a/tests/conftest.py b/tests/conftest.py index a30a0d232e..a43d6140b2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ from datetime import datetime, timedelta, UTC from . import adb, run from .cmake import CMake -from .conditions import has_sccache, is_android +from .conditions import has_sccache, is_android, is_wine import tests LABEL = "label" @@ -211,6 +211,11 @@ def pytest_sessionfinish(session, exitstatus): def pytest_sessionstart(session): + if is_wine: + # Keep Wine diagnostics from obscuring test output. + os.environ.setdefault("WINEDEBUG", "-all") + # Prevent intentional crashes from opening WineDbg and blocking pytest. + os.environ.setdefault("WINEDLLOVERRIDES", "winedbg.exe=d") if has_sccache: subprocess.run(["sccache", "--zero-stats"], capture_output=True) diff --git a/tests/test_build_static.py b/tests/test_build_static.py index bda4a2227c..950ed334f6 100644 --- a/tests/test_build_static.py +++ b/tests/test_build_static.py @@ -2,7 +2,14 @@ import sys import os import pytest -from .conditions import has_breakpad, has_crashpad, has_native, is_android, is_qemu +from .conditions import ( + has_breakpad, + has_crashpad, + has_native, + is_android, + is_qemu, + is_wine, +) def test_static_lib(cmake): @@ -16,12 +23,12 @@ def test_static_lib(cmake): ) # on linux we can use `ldd` to check that we don’t link to `libsentry.so` - if sys.platform == "linux" and not is_android and not is_qemu: + if sys.platform == "linux" and not is_android and not is_qemu and not is_wine: output = subprocess.check_output("ldd sentry_example", cwd=tmp_path, shell=True) assert b"libsentry.so" not in output # on windows, we read the file header to check that the exe is compiled correctly - if sys.platform == "win32": + if sys.platform == "win32" or is_wine: with open(os.path.join(tmp_path, "sentry_example.exe"), "rb") as binary: binary.seek(0x3C, 0) offset = int.from_bytes(binary.read(4), byteorder="little") @@ -36,7 +43,7 @@ def test_static_lib(cmake): expected = b"PE\x00\x00\x64\x86" # IMAGE_FILE_MACHINE_AMD64 assert magic == expected # similarly, we use `file` on linux - if sys.platform == "linux": + if sys.platform == "linux" and not is_wine: output = subprocess.check_output( "file sentry_example", cwd=tmp_path, shell=True ) diff --git a/tests/test_dotnet_signals.py b/tests/test_dotnet_signals.py index ef3def7e94..8be0938ab3 100644 --- a/tests/test_dotnet_signals.py +++ b/tests/test_dotnet_signals.py @@ -9,10 +9,22 @@ from tests import adb from tests.assertions import wait_for -from tests.conditions import has_native, is_android, is_arm32, is_tsan, is_x86, is_asan +from tests.conditions import ( + has_native, + is_android, + is_arm32, + is_tsan, + is_wine, + is_x86, + is_asan, +) project_fixture_path = pathlib.Path("tests/fixtures/dotnet_signal") +pytestmark = pytest.mark.skipif( + is_wine, reason=".NET signal tests do not support Windows targets under Wine" +) + def assert_empty_run_dir(database_path): run_dirs = [d for d in database_path.glob("*.run") if d.is_dir()] diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index 0773639f84..ad91e9afe6 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -19,7 +19,7 @@ is_replay_envelope, REPLAY_ID, ) -from .conditions import has_crashpad, has_oom +from .conditions import has_crashpad, has_oom, is_wine from .proxy import ( setup_proxy_env_vars, cleanup_proxy_env_vars, @@ -40,10 +40,13 @@ wait_for_file, ) -pytestmark = pytest.mark.skipif( - not has_crashpad, - reason="Tests need a crashpad backend and a valid environment for it", -) +pytestmark = [ + pytest.mark.skipif( + not has_crashpad, + reason="Tests need a crashpad backend and a valid environment for it", + ), + pytest.mark.skipif(is_wine, reason="Crashpad does not support Wine cross-builds"), +] # Windows and Linux are currently able to flush all the state on crash flushes_state = sys.platform != "darwin" diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 18c2129b6b..7dff8db964 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -47,6 +47,7 @@ has_files, is_asan, is_qemu, + is_wine, ) @@ -360,7 +361,12 @@ def test_external_crash_reporter_http(cmake, httpserver, build_args): crash = crash_request.get_data() envelope = Envelope.deserialize(crash) - assert envelope.headers["cache_dir"] == str(cache_dir) + actual_cache_dir = envelope.headers["cache_dir"] + if is_wine: + actual_cache_dir = subprocess.check_output( + ["winepath", "-u", actual_cache_dir], text=True + ).strip() + assert actual_cache_dir == str(cache_dir) assert_meta(envelope, integration=build_args.get("SENTRY_BACKEND", "")) envelope = Envelope.deserialize(feedback) diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 4cae17405e..bb9edbaed6 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -21,6 +21,7 @@ run, run_crash, stage_replay, + run_command, Envelope, split_log_request_cond, REPLAY_ID, @@ -37,7 +38,7 @@ wait_for_file, assert_user_feedback, ) -from .conditions import has_native, has_oom, is_asan, is_tsan, is_qemu +from .conditions import has_native, has_oom, is_asan, is_tsan, is_qemu, is_wine pytestmark = pytest.mark.skipif( not has_native or is_qemu, @@ -148,6 +149,7 @@ def test_native_oom(cmake, httpserver): ), ], ) +@pytest.mark.skipif(is_wine, reason="Wine does not terminate after stack overflow") def test_native_stack_overflow(cmake, httpserver, stack_size): """Test stack overflow crash capture with native backend""" env = dict(os.environ) @@ -318,11 +320,9 @@ def test_native_attachment_manifest_is_current(cmake, httpserver): """ tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) - exe = tmp_path / ( - "sentry_example.exe" if sys.platform == "win32" else "sentry_example" - ) + cmd = run_command(str(tmp_path / "sentry_example")) child = subprocess.Popen( - [str(exe), "log", "attach-custom-filename", "sleep"], + [*cmd, "log", "attach-custom-filename", "sleep"], cwd=tmp_path, env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), ) @@ -855,6 +855,10 @@ def _codesign_for_task_for_pid(*paths): ) +@pytest.mark.skipif( + is_wine, + reason="Wine does not capture indirectly referenced minidump memory", +) def test_native_smart_mode_captures_indirect_heap_memory(cmake, httpserver): """ Verify SMART minidump mode captures memory regions referenced by the @@ -964,11 +968,9 @@ def in_any(addr, ranges): def test_native_uses_existing_run(cmake): """The daemon adopts the existing run instead of creating root artifacts.""" tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) - exe = tmp_path / ( - "sentry_example.exe" if sys.platform == "win32" else "sentry_example" - ) + cmd = run_command(str(tmp_path / "sentry_example")) child = subprocess.Popen( - [str(exe), "log", "sleep"], + [*cmd, "log", "sleep"], cwd=tmp_path, env=dict(os.environ, SENTRY_DSN="https://foo@sentry.invalid/42"), ) @@ -1068,7 +1070,12 @@ def test_native_external_crash_reporter(cmake, httpserver): # Verify it's a minidump crash report and user feedback envelope = Envelope.deserialize(crash) - assert envelope.headers["cache_dir"] == str(cache_dir) + actual_cache_dir = envelope.headers["cache_dir"] + if is_wine: + actual_cache_dir = subprocess.check_output( + ["winepath", "-u", actual_cache_dir], text=True + ).strip() + assert actual_cache_dir == str(cache_dir) assert_meta(envelope, integration="native") assert_breadcrumb(envelope) diff --git a/tests/test_integration_proxy.py b/tests/test_integration_proxy.py index 2eced91a64..900d19d35a 100644 --- a/tests/test_integration_proxy.py +++ b/tests/test_integration_proxy.py @@ -10,7 +10,7 @@ from .assertions import ( assert_failed_proxy_auth_request, ) -from .conditions import has_http +from .conditions import has_http, is_wine from .proxy import ( closed_port, start_proxy, @@ -131,6 +131,7 @@ def test_proxy_auth_incorrect(cmake, httpserver): ) +@pytest.mark.skipif(is_wine, reason="Wine WinHTTP does not support IPv6 proxies") def test_proxy_ipv6(cmake, httpserver): proxy_process = None # store the proxy process to terminate it later try: @@ -197,7 +198,7 @@ def test_proxy_https_not_http(cmake, httpserver): pytest.param( ["socks5-proxy"], marks=pytest.mark.skipif( - sys.platform not in ["darwin", "linux"], + sys.platform not in ["darwin", "linux"] or is_wine, reason="SOCKS5 proxy tests are only supported on macOS and Linux", ), ), diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 54485adc82..54ea52fafe 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -6,7 +6,7 @@ import pytest -from . import check_output, run, Envelope +from . import check_output, run, run_command, Envelope from .assertions import ( assert_attachment, assert_meta, @@ -22,7 +22,7 @@ assert_exception, wait_for, ) -from .conditions import has_breakpad, has_files, is_qemu +from .conditions import has_breakpad, has_files, is_qemu, is_wine @pytest.mark.skipif(is_qemu, reason="unreliable under qemu-user") @@ -123,15 +123,10 @@ def test_multi_process(cmake): cwd = tmp_path.joinpath("unicode ❤️ Юля") cwd.mkdir() - exe = "sentry_example" - cmd = ( - "../{}".format(exe) - if sys.platform != "win32" - else "{}\\{}.exe".format(tmp_path, exe) - ) + cmd = run_command(str(tmp_path / "sentry_example")) - child1 = subprocess.Popen([cmd, "sleep"], cwd=cwd) - child2 = subprocess.Popen([cmd, "sleep"], cwd=cwd) + child1 = subprocess.Popen([*cmd, "sleep"], cwd=cwd) + child2 = subprocess.Popen([*cmd, "sleep"], cwd=cwd) def list_db(suffix): try: @@ -153,7 +148,7 @@ def list_db(suffix): child2.wait() # and start another process that cleans up the old runs - subprocess.run([cmd], cwd=cwd) + subprocess.run(cmd, cwd=cwd) assert len(list_db(".run")) == 0 assert len(list_db(".lock")) == 0 @@ -278,6 +273,7 @@ def test_inproc_crash_stdout_before_send_and_on_crash(cmake): ), ], ) +@pytest.mark.skipif(is_wine, reason="Wine does not terminate after stack overflow") def test_inproc_stack_overflow_stdout(cmake, stack_size): env = dict(os.environ) if stack_size: @@ -373,6 +369,7 @@ def test_breakpad_crash_stdout_before_send_and_on_crash(cmake): ], ) @pytest.mark.skipif(not has_breakpad or is_qemu, reason="test needs breakpad backend") +@pytest.mark.skipif(is_wine, reason="Wine does not terminate after stack overflow") def test_breakpad_stack_overflow_stdout(cmake, stack_size): env = dict(os.environ) if stack_size: diff --git a/tests/test_stress_inproc.py b/tests/test_stress_inproc.py index a88529f0cb..41d64bdf23 100644 --- a/tests/test_stress_inproc.py +++ b/tests/test_stress_inproc.py @@ -11,10 +11,14 @@ from . import adb, Envelope from .assertions import assert_inproc_crash from .build_config import get_test_executable_cmake_args, get_test_executable_env -from .conditions import is_android, is_tsan +from .conditions import is_android, is_tsan, is_wine fixture_path = pathlib.Path("tests/fixtures/inproc_stress") +pytestmark = pytest.mark.skipif( + is_wine, reason="inproc stress test harness does not support Wine" +) + ANDROID_TMP = "/data/local/tmp" diff --git a/tests/test_unit.py b/tests/test_unit.py index a170e33116..24a4c014d0 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -1,10 +1,13 @@ import os import pytest from . import run -from .conditions import has_http +from .conditions import has_http, is_wine def _skip_if_unsupported(unittest): + if is_wine and unittest == "path_remove_all_symlink": + pytest.skip("Wine does not implement CreateSymbolicLinkW") + # app_hang_end_to_end drives the real cross-thread RT-signal sampler and # unwinds a signal frame inside the handler. qemu-user does not emulate # thread-targeted signal delivery/unwinding faithfully, so the sample never diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index f461e2f9df..61bb84221b 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -1089,7 +1089,11 @@ SENTRY_TEST(value_stringify) STRINGIFY_AND_CHECK(rv, "3.14"); rv = sentry_value_new_double(1000000000000000); - STRINGIFY_AND_CHECK(rv, "1e+15"); + char *stringified = sentry__value_stringify(rv); + TEST_CHECK(strcmp(stringified, "1e+15") == 0 // msvcrt, libc, stbsp + || strcmp(stringified, "1e+015") == 0); // wine + sentry_free(stringified); + sentry_value_decref(rv); rv = sentry_value_new_double(INFINITY); STRINGIFY_AND_CHECK_CONTAINS(rv, "inf"); From d154eb161bc0ea7c66695751d40f41be90821707 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 19 Aug 2026 09:40:43 +0200 Subject: [PATCH 2/7] Wine 11 (has CreateSymbolicLink) --- .github/workflows/ci.yml | 9 ++++++++- tests/conftest.py | 5 +++-- tests/test_unit.py | 3 --- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fbdd2a7ea..69240e8c7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -303,7 +303,14 @@ jobs: - name: Installing Wine Dependencies if: ${{ env['TEST_WINE'] }} - run: sudo apt install wine mingw-w64 lld + run: | + sudo install -d -m 0755 /etc/apt/keyrings + sudo curl -fsSL -o /etc/apt/keyrings/winehq-archive.asc https://dl.winehq.org/wine-builds/winehq.key + sudo curl -fsSL -o /etc/apt/sources.list.d/winehq-resolute.sources https://dl.winehq.org/wine-builds/ubuntu/dists/resolute/winehq-resolute.sources + sudo sed -i 's|winehq-archive\.key|winehq-archive.asc|' /etc/apt/sources.list.d/winehq-resolute.sources + sudo apt-get update + sudo apt-get install --install-recommends winehq-stable mingw-w64 lld + wine --version - name: Installing kcov dependencies if: ${{ contains(env['RUN_ANALYZER'], 'kcov') }} diff --git a/tests/conftest.py b/tests/conftest.py index a43d6140b2..a743ff266f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -214,8 +214,9 @@ def pytest_sessionstart(session): if is_wine: # Keep Wine diagnostics from obscuring test output. os.environ.setdefault("WINEDEBUG", "-all") - # Prevent intentional crashes from opening WineDbg and blocking pytest. - os.environ.setdefault("WINEDLLOVERRIDES", "winedbg.exe=d") + # Prevent intentional crashes from opening WineDbg and fresh prefixes + # from prompting to install Wine Mono. + os.environ.setdefault("WINEDLLOVERRIDES", "winedbg.exe=d;mscoree=d") if has_sccache: subprocess.run(["sccache", "--zero-stats"], capture_output=True) diff --git a/tests/test_unit.py b/tests/test_unit.py index 24a4c014d0..4f418fb9fd 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -5,9 +5,6 @@ def _skip_if_unsupported(unittest): - if is_wine and unittest == "path_remove_all_symlink": - pytest.skip("Wine does not implement CreateSymbolicLinkW") - # app_hang_end_to_end drives the real cross-thread RT-signal sampler and # unwinds a signal frame inside the handler. qemu-user does not emulate # thread-targeted signal delivery/unwinding faithfully, so the sample never From c14dbd62fcaf63555a112da6d465147dde5cbb22 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 19 Aug 2026 09:43:42 +0200 Subject: [PATCH 3/7] disable has_crashpad for is_wine --- tests/conditions.py | 3 ++- tests/test_integration_crashpad.py | 13 +++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/conditions.py b/tests/conditions.py index fd7c87955c..094810abcf 100644 --- a/tests/conditions.py +++ b/tests/conditions.py @@ -30,7 +30,7 @@ # breakpad accesses thread state registers directly, which doesn't work on arm64e and not (is_arm64e and sys.platform == "darwin") ) -# crashpad requires http, needs porting to AIX, and doesn’t work with kcov/valgrind/tsan either +# crashpad requires http, needs porting to AIX, and doesn’t work with kcov/valgrind/tsan or Wine cross-builds has_crashpad = ( has_http and not is_valgrind @@ -38,6 +38,7 @@ and not is_android and not is_aix and not is_tsan + and not is_wine ) # android has no local filesystem has_files = not is_android diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index ad91e9afe6..0773639f84 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -19,7 +19,7 @@ is_replay_envelope, REPLAY_ID, ) -from .conditions import has_crashpad, has_oom, is_wine +from .conditions import has_crashpad, has_oom from .proxy import ( setup_proxy_env_vars, cleanup_proxy_env_vars, @@ -40,13 +40,10 @@ wait_for_file, ) -pytestmark = [ - pytest.mark.skipif( - not has_crashpad, - reason="Tests need a crashpad backend and a valid environment for it", - ), - pytest.mark.skipif(is_wine, reason="Crashpad does not support Wine cross-builds"), -] +pytestmark = pytest.mark.skipif( + not has_crashpad, + reason="Tests need a crashpad backend and a valid environment for it", +) # Windows and Linux are currently able to flush all the state on crash flushes_state = sys.platform != "darwin" From 977f8c027aee774d7e3a09329790dbc1c1d9134b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 19 Aug 2026 09:49:24 +0200 Subject: [PATCH 4/7] build config --- tests/build_config.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/build_config.py b/tests/build_config.py index 1c9670dde6..fd11279a16 100644 --- a/tests/build_config.py +++ b/tests/build_config.py @@ -48,7 +48,21 @@ def get_platform_cmake_args(): """ args = [] - if os.environ.get("TEST_WINE"): + if sys.platform == "win32" and os.environ.get("TEST_X86"): + args.append("-AWin32") + elif sys.platform == "linux" and os.environ.get("TEST_X86"): + args.append("-DSENTRY_BUILD_FORCE32=ON") + elif sys.platform == "linux" and os.environ.get("TEST_ARM32"): + args.extend( + [ + "-DCMAKE_SYSTEM_NAME=Linux", + "-DCMAKE_SYSTEM_PROCESSOR=arm", + "-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc", + "-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++", + "-DCMAKE_ASM_COMPILER=arm-linux-gnueabihf-gcc", + ] + ) + elif sys.platform == "linux" and os.environ.get("TEST_WINE"): cc = os.environ.get("CC") or "clang" cxx = os.environ.get("CXX") or "clang++" linker_flags = "-fuse-ld=lld -static-libgcc -static-libstdc++" @@ -66,20 +80,6 @@ def get_platform_cmake_args(): f"-DCMAKE_MODULE_LINKER_FLAGS={linker_flags}", ] ) - elif sys.platform == "win32" and os.environ.get("TEST_X86"): - args.append("-AWin32") - elif sys.platform == "linux" and os.environ.get("TEST_X86"): - args.append("-DSENTRY_BUILD_FORCE32=ON") - elif sys.platform == "linux" and os.environ.get("TEST_ARM32"): - args.extend( - [ - "-DCMAKE_SYSTEM_NAME=Linux", - "-DCMAKE_SYSTEM_PROCESSOR=arm", - "-DCMAKE_C_COMPILER=arm-linux-gnueabihf-gcc", - "-DCMAKE_CXX_COMPILER=arm-linux-gnueabihf-g++", - "-DCMAKE_ASM_COMPILER=arm-linux-gnueabihf-gcc", - ] - ) if "asan" in os.environ.get("RUN_ANALYZER", ""): args.append("-DWITH_ASAN_OPTION=ON") From 578b730dec2bb49428845c7a895f04495f8ddb89 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 19 Aug 2026 09:55:45 +0200 Subject: [PATCH 5/7] ucrt _set_abort_behavior --- examples/example.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/example.c b/examples/example.c index b5f99ce72f..1c4cc3da42 100644 --- a/examples/example.c +++ b/examples/example.c @@ -1316,7 +1316,7 @@ main(int argc, char **argv) assert(0); } if (has_arg(argc, argv, "abort")) { -#if defined(_WIN32) && defined(_MSC_VER) +#if defined(_WIN32) && (defined(_MSC_VER) || defined(_UCRT)) // Suppress the Windows abort dialog that would block CI _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); #endif From 13f57c8b5173e8cc8fb68de36c8b18cc10b473e3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 19 Aug 2026 10:01:16 +0200 Subject: [PATCH 6/7] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98eacde709..d7def99d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Native: store daemon logs, minidumps, crash envelopes, and scratch files in `.run` directories so they are cleaned up with the run instead of accumulating in the database root. Minidumps can still be retained with `cache_keep`, which stores `.dmp` sidecars alongside cached envelopes. ([#1976](https://github.com/getsentry/sentry-native/pull/1976)) - Linux/ARM32: prevent recursive crashes when libunwind receives an unmapped initial instruction pointer during crash handling. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) +- Wine: fix OS version detection and cross-compiling Windows builds from Linux. ([#2001](https://github.com/getsentry/sentry-native/pull/2001)) ## 0.16.3 From a7facb35a38e74a73af74895d35282fe39d6409a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 19 Aug 2026 10:36:56 +0200 Subject: [PATCH 7/7] test: Avoid resetting rejected proxy requests Consume the request body before returning a proxy authentication challenge so closing the connection does not produce a TCP reset. This lets Wine WinHTTP receive the challenge and retry with credentials. --- tests/proxy_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/proxy_server.py b/tests/proxy_server.py index 2a3d52c0a0..78628372a1 100644 --- a/tests/proxy_server.py +++ b/tests/proxy_server.py @@ -169,6 +169,7 @@ def _forward_http(client, proxy_auth): lines = header.decode("iso-8859-1").split("\r\n") method, target, version = lines[0].split(" ", 2) headers = _parse_headers(lines[1:]) + body = _read_http_body(client, headers, initial_body) if not _auth_matches(headers, proxy_auth): _send_proxy_auth_required(client, method) @@ -188,7 +189,6 @@ def _forward_http(client, proxy_auth): if url.query: path += "?" + url.query - body = _read_http_body(client, headers, initial_body) outbound_headers = [ (name, value) for name, value in headers