diff --git a/MODULE.bazel b/MODULE.bazel index 559516bd1..0a2521d75 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -326,6 +326,8 @@ http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "ht http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file") new_local_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository") +include("//bazel/modules:wine.MODULE.bazel") + new_local_repository( name = "v8_targets", build_file = "//third_party/v8:BUILD.bazel", diff --git a/bazel/modules/BUILD.bazel b/bazel/modules/BUILD.bazel new file mode 100644 index 000000000..bcc3dcf52 --- /dev/null +++ b/bazel/modules/BUILD.bazel @@ -0,0 +1 @@ +exports_files(["wine.MODULE.bazel"]) diff --git a/bazel/modules/wine.MODULE.bazel b/bazel/modules/wine.MODULE.bazel new file mode 100644 index 000000000..d50e8a360 --- /dev/null +++ b/bazel/modules/wine.MODULE.bazel @@ -0,0 +1,13 @@ +http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# Pin a new-WoW64 build so tests need neither system Wine nor 32-bit host +# libraries. +http_archive( + name = "wine_linux_x86_64", + build_file = "//third_party/wine:BUILD.bazel", + sha256 = "39574efa1132c3ca0d5c77dd2eddbe4a49cca0d6cc2c290ff4924493a1c40314", + strip_prefix = "wine-11.0-amd64-wow64", + urls = [ + "https://github.com/Kron4ek/Wine-Builds/releases/download/11.0/wine-11.0-amd64-wow64.tar.xz", + ], +) diff --git a/bazel/rules/testing/BUILD.bazel b/bazel/rules/testing/BUILD.bazel new file mode 100644 index 000000000..821c3a7aa --- /dev/null +++ b/bazel/rules/testing/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "foreign_platform_binary.bzl", + "wine.bzl", +]) diff --git a/bazel/rules/testing/foreign_platform_binary.bzl b/bazel/rules/testing/foreign_platform_binary.bzl new file mode 100644 index 000000000..cb709d81c --- /dev/null +++ b/bazel/rules/testing/foreign_platform_binary.bzl @@ -0,0 +1,53 @@ +"""Makes a binary built for a foreign platform available as test data.""" + +_EXTRA_RUSTC_FLAGS = "@rules_rust//rust/settings:extra_rustc_flags" + +def _foreign_platform_transition_impl(settings, attr): + # A transition cannot rewrite a dependency's rule attributes. Use the + # rules_rust build setting when every Rust target in the foreign + # configuration needs additional compiler or linker flags. + return { + "//command_line_option:platforms": [attr.platform], + _EXTRA_RUSTC_FLAGS: settings[_EXTRA_RUSTC_FLAGS] + attr.extra_rustc_flags, + } + +_foreign_platform_transition = transition( + implementation = _foreign_platform_transition_impl, + inputs = [_EXTRA_RUSTC_FLAGS], + outputs = [ + "//command_line_option:platforms", + _EXTRA_RUSTC_FLAGS, + ], +) + +def _foreign_platform_binary_impl(ctx): + if len(ctx.attr.binary) != 1: + fail("expected exactly one transitioned binary") + binary = ctx.attr.binary[0][DefaultInfo] + runfiles = ctx.runfiles(transitive_files = binary.files) + runfiles = runfiles.merge(binary.default_runfiles) + return [ + DefaultInfo( + files = binary.files, + runfiles = runfiles, + ), + ] + +foreign_platform_binary = rule( + implementation = _foreign_platform_binary_impl, + attrs = { + "binary": attr.label( + cfg = _foreign_platform_transition, + executable = True, + mandatory = True, + ), + "extra_rustc_flags": attr.string_list( + doc = "Additional flags applied to every Rust target in the foreign configuration.", + ), + "platform": attr.string(mandatory = True), + "_allowlist_function_transition": attr.label( + default = "@bazel_tools//tools/allowlists/function_transition_allowlist", + ), + }, + doc = "Builds `binary` for `platform` and exposes its files and runfiles.", +) diff --git a/bazel/rules/testing/wine.bzl b/bazel/rules/testing/wine.bzl new file mode 100644 index 000000000..5dc535a5e --- /dev/null +++ b/bazel/rules/testing/wine.bzl @@ -0,0 +1,76 @@ +"""Macros for cross-building Windows Rust binaries and testing them with Wine.""" + +load("@rules_rust//rust:defs.bzl", "rust_test") +load("//:defs.bzl", "WINDOWS_GNULLVM_RUSTC_LINK_FLAGS") +load(":foreign_platform_binary.bzl", "foreign_platform_binary") + +_WINE_RUNTIME_BINARIES = { + "wine": "@wine_linux_x86_64//:wine", + "wine-runtime-marker": "@wine_linux_x86_64//:runtime_marker", + "wineserver": "@wine_linux_x86_64//:wineserver", +} + +def wine_rust_test( + name, + windows_binaries, + data = [], + target_compatible_with = [], + **kwargs): + """Defines an x86-64 Linux Rust test with a pinned Wine runtime. + + Each `windows_binaries` executable is transitioned to GNU/LLVM Windows; + every Rust dependency receives the repository's Windows linker flags while + the test stays on x86-64 Linux. Its environment-variable contract is: + + * Each entry contributes `CARGO_BIN_EXE_` for its executable. + * `CARGO_BIN_EXE_wine` and `CARGO_BIN_EXE_wineserver` identify Wine tools. + * `CARGO_BIN_EXE_wine-runtime-marker` identifies a file whose parent is the + Wine DLL directory to use as `WINEDLLPATH`. + + These are Bazel runfile locations. Resolve binaries with + `codex_utils_cargo_bin::cargo_bin`; `:wine_test_support` resolves the fixed + runtime names and starts each process in an isolated prefix. + + Args: + name: Name of the generated Linux `rust_test`. + windows_binaries: Map from `CARGO_BIN_EXE_*` suffixes to Windows targets. + data: Additional runtime data for the Linux test. + target_compatible_with: Additional compatibility constraints. + **kwargs: Remaining attributes forwarded to `rust_test`. + """ + binaries = dict(_WINE_RUNTIME_BINARIES) + for index, binary_name in enumerate(sorted(windows_binaries.keys())): + if binary_name in binaries: + fail("Windows test binary name collides with Wine runtime: {}".format(binary_name)) + transitioned_binary = name + "-windows-binary-" + str(index) + foreign_platform_binary( + name = transitioned_binary, + binary = windows_binaries[binary_name], + extra_rustc_flags = WINDOWS_GNULLVM_RUSTC_LINK_FLAGS, + platform = "//:windows_x86_64_gnullvm", + tags = ["manual"], + target_compatible_with = [ + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + testonly = True, + visibility = ["//visibility:private"], + ) + binaries[binary_name] = ":" + transitioned_binary + + rust_test( + name = name, + data = data + [ + "@wine_linux_x86_64//:runtime", + ] + [binary for binary in binaries.values()], + env = { + "CARGO_BIN_EXE_{}".format(binary_name): "$(rlocationpath {})".format(binary) + for binary_name, binary in binaries.items() + }, + target_compatible_with = target_compatible_with + [ + "@llvm//constraints/libc:gnu.2.28", + "@platforms//cpu:x86_64", + "@platforms//os:linux", + ], + **kwargs + ) diff --git a/bazel/rules/testing/wine/BUILD.bazel b/bazel/rules/testing/wine/BUILD.bazel new file mode 100644 index 000000000..7e5cb1bb1 --- /dev/null +++ b/bazel/rules/testing/wine/BUILD.bazel @@ -0,0 +1,48 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") +load("//bazel/rules/testing:wine.bzl", "wine_rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "wine_test_support", + testonly = True, + srcs = [ + "src/lib.rs", + "src/lib_tests.rs", + ], + crate_name = "wine_test_support", + crate_root = "src/lib.rs", + edition = "2024", + target_compatible_with = ["@platforms//os:linux"], + deps = [ + "//codex-rs/utils/cargo-bin", + "@crates//:anyhow", + "@crates//:tempfile", + "@crates//:tokio", + ], +) + +rust_binary( + name = "windows-smoke", + testonly = True, + srcs = ["fixtures/windows_smoke.rs"], + crate_name = "wine_smoke", + crate_root = "fixtures/windows_smoke.rs", + edition = "2024", + tags = ["manual"], + target_compatible_with = ["@platforms//os:windows"], + visibility = ["//visibility:private"], +) + +wine_rust_test( + name = "wine-test-support-unit-tests", + timeout = "short", + crate = ":wine_test_support", + windows_binaries = { + "wine-smoke": ":windows-smoke", + }, + deps = [ + "@crates//:futures", + "@crates//:pretty_assertions", + ], +) diff --git a/bazel/rules/testing/wine/fixtures/windows_smoke.rs b/bazel/rules/testing/wine/fixtures/windows_smoke.rs new file mode 100644 index 000000000..ffd98a244 --- /dev/null +++ b/bazel/rules/testing/wine/fixtures/windows_smoke.rs @@ -0,0 +1,15 @@ +use std::io::Write; + +fn main() { + println!("WINE_TEST_READY"); + std::io::stdout().flush().expect("flush readiness marker"); + + if std::env::args().any(|arg| arg == "--fail") { + std::process::exit(9); + } + if std::env::args().any(|arg| arg == "--wait") { + loop { + std::thread::park(); + } + } +} diff --git a/bazel/rules/testing/wine/src/lib.rs b/bazel/rules/testing/wine/src/lib.rs new file mode 100644 index 000000000..8f5eef1bf --- /dev/null +++ b/bazel/rules/testing/wine/src/lib.rs @@ -0,0 +1,303 @@ +#[cfg(not(target_os = "linux"))] +compile_error!("wine_test_support can only run on Linux"); + +use std::ffi::OsString; +use std::future::Future; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command as StdCommand; +use std::process::Stdio; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use tempfile::TempDir; +use tokio::process::Child; +use tokio::process::ChildStdout; +use tokio::process::Command as TokioCommand; + +/// Builds a command that runs a Windows executable in an isolated Wine prefix. +pub struct WineTestCommand { + executable: PathBuf, + args: Vec, + env: Vec<(OsString, OsString)>, +} + +/// Owns a Wine process and its isolated wineserver. +/// +/// Call [`Self::scope`] or [`Self::shutdown`] on every successful path. A +/// normal unguarded drop panics, while a drop during unwinding performs +/// blocking cleanup without introducing a second panic. +pub struct WineTestProcess { + processes: Option, +} + +struct WineProcesses { + child: Child, + cleanup_complete: bool, + prefix: TempDir, + runtime: WineRuntimePaths, +} + +struct WineRuntimePaths { + dll_path: PathBuf, + wine: PathBuf, + wineserver: PathBuf, +} + +impl WineTestCommand { + /// Creates a Wine command for `executable`. + pub fn new(executable: impl Into) -> Self { + Self { + executable: executable.into(), + args: Vec::new(), + env: Vec::new(), + } + } + + /// Adds an argument passed to the Windows executable. + #[must_use] + pub fn arg(mut self, arg: impl Into) -> Self { + self.args.push(arg.into()); + self + } + + /// Adds or overrides an environment variable for the Wine process. + #[must_use] + pub fn env(mut self, key: impl Into, value: impl Into) -> Self { + self.env.push((key.into(), value.into())); + self + } + + /// Starts the Windows executable with a fresh `WINEPREFIX`. + pub fn spawn(self) -> Result { + let runtime = WineRuntimePaths::from_runfiles()?; + let prefix = TempDir::new().context("create isolated Wine prefix")?; + let mut command = StdCommand::new(&runtime.wine); + configure_wine_environment(&mut command, &runtime, prefix.path()); + command + .arg(self.executable) + .args(self.args) + .envs(self.env) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + let mut command = TokioCommand::from(command); + command.kill_on_drop(true); + let child = command + .spawn() + .context("start Windows process under Wine")?; + + Ok(WineTestProcess { + processes: Some(WineProcesses { + child, + cleanup_complete: false, + prefix, + runtime, + }), + }) + } +} + +impl WineTestProcess { + /// Takes the piped standard output of the Wine process. + /// + /// This may only be called once for a process created by + /// [`WineTestCommand::spawn`]. + pub fn take_stdout(&mut self) -> ChildStdout { + let Some(processes) = self.processes.as_mut() else { + panic!("Wine process guard is missing"); + }; + let Some(stdout) = processes.child.stdout.take() else { + panic!("Wine process stdout has already been taken"); + }; + stdout + } + + /// Runs `future`, then asynchronously tears down Wine before returning. + /// + /// If both the scoped operation and teardown fail, the operation error is + /// returned with the teardown error attached as context. A panic in the + /// scoped operation triggers the blocking unwind-time fallback instead. + pub async fn scope(self, future: impl Future>) -> Result { + let scope_result = future.await; + let shutdown_result = self.shutdown().await; + match (scope_result, shutdown_result) { + (Ok(value), Ok(())) => Ok(value), + (Err(error), Ok(())) => Err(error), + (Ok(_), Err(error)) => Err(error), + (Err(error), Err(shutdown_error)) => { + Err(error.context(format!("Wine teardown also failed: {shutdown_error:#}"))) + } + } + } + + /// Kills the Windows process, waits for it, and stops its wineserver. + pub async fn shutdown(mut self) -> Result<()> { + let Some(processes) = self.processes.as_mut() else { + anyhow::bail!("Wine process guard is missing"); + }; + let result = processes.shutdown().await; + self.processes.take(); + result + } +} + +impl Drop for WineTestProcess { + fn drop(&mut self) { + // Panicking here starts unwinding, after which WineProcesses performs + // the blocking fallback while its field is dropped. + if self.processes.is_some() && !std::thread::panicking() { + panic!("WineTestProcess dropped without async teardown"); + } + } +} + +impl WineRuntimePaths { + fn from_runfiles() -> Result { + let wine = codex_utils_cargo_bin::cargo_bin("wine")?; + let runtime_marker = codex_utils_cargo_bin::cargo_bin("wine-runtime-marker")?; + let dll_path = runtime_marker + .parent() + .context("locate Wine runtime directory")? + .to_path_buf(); + let wineserver = codex_utils_cargo_bin::cargo_bin("wineserver")?; + Ok(Self { + dll_path, + wine, + wineserver, + }) + } +} + +impl WineProcesses { + async fn shutdown(&mut self) -> Result<()> { + let (kill_result, check_exit_status) = match self.child.try_wait() { + Ok(Some(_)) => (Ok(()), true), + Ok(None) => ( + self.child + .start_kill() + .context("kill Windows process running under Wine"), + false, + ), + Err(error) => (Err(error).context("check Windows process status"), false), + }; + let wait_result = self + .child + .wait() + .await + .context("wait for Windows process running under Wine") + .and_then(|status| { + anyhow::ensure!( + !check_exit_status || status.success(), + "Windows process exited with {status}" + ); + Ok(()) + }); + let wineserver_result = async { + let mut command = TokioCommand::from(self.stop_wineserver_command()); + let status = command.status().await.context("stop isolated wineserver")?; + anyhow::ensure!(status.success(), "wineserver exited with {status}"); + Ok(()) + } + .await; + + // Every cleanup action has been attempted, so an individual error + // should not cause the blocking fallback to repeat them. + self.cleanup_complete = true; + kill_result?; + wait_result?; + wineserver_result + } + + fn stop_wineserver_command(&self) -> StdCommand { + let mut command = StdCommand::new(&self.runtime.wineserver); + configure_wine_environment(&mut command, &self.runtime, self.prefix.path()); + command + .args(["-k", "-w"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + command + } + + fn shutdown_blocking(&mut self) { + log_panic_cleanup(format_args!( + "Wine panic cleanup starting for prefix {}", + self.prefix.path().display() + )); + if let Err(error) = self.child.start_kill() { + log_panic_cleanup(format_args!( + "Wine panic cleanup could not kill its child: {error}" + )); + } + + log_panic_cleanup(format_args!("Wine panic cleanup waiting for its child")); + loop { + match self.child.try_wait() { + Ok(Some(status)) => { + log_panic_cleanup(format_args!( + "Wine panic cleanup child exited with {status}" + )); + break; + } + Ok(None) => std::thread::sleep(Duration::from_millis(10)), + Err(error) => { + log_panic_cleanup(format_args!( + "Wine panic cleanup could not wait for its child: {error}" + )); + break; + } + } + } + + log_panic_cleanup(format_args!("Wine panic cleanup stopping its wineserver")); + match self.stop_wineserver_command().status() { + Ok(status) => log_panic_cleanup(format_args!( + "Wine panic cleanup wineserver exited with {status}" + )), + Err(error) => log_panic_cleanup(format_args!( + "Wine panic cleanup could not stop its wineserver: {error}" + )), + } + self.cleanup_complete = true; + log_panic_cleanup(format_args!("Wine panic cleanup complete")); + } +} + +impl Drop for WineProcesses { + fn drop(&mut self) { + // Never introduce a second panic while unwinding. Blocking here is + // intentional because test failures must not leak Wine children. + if !self.cleanup_complete && std::thread::panicking() { + self.shutdown_blocking(); + } + } +} + +fn log_panic_cleanup(args: std::fmt::Arguments<'_>) { + let _ = writeln!(std::io::stderr().lock(), "{args}"); +} + +fn configure_wine_environment(command: &mut StdCommand, runtime: &WineRuntimePaths, prefix: &Path) { + command + .env_remove("DISPLAY") + .env("HOME", prefix) + .env("XDG_RUNTIME_DIR", prefix) + .env("WINEARCH", "win64") + .env("WINEPREFIX", prefix) + .env("WINEDLLPATH", &runtime.dll_path) + .env("WINESERVER", &runtime.wineserver) + .env("WINEDEBUG", "-all") + .env("WINEDLLOVERRIDES", "mscoree,mshtml,winegstreamer=") + .env("LANG", "C.UTF-8") + .env("LC_ALL", "C.UTF-8") + .env("LC_CTYPE", "C.UTF-8") + .env("TEMP", r"C:\windows\temp") + .env("TMP", r"C:\windows\temp"); +} + +#[cfg(test)] +#[path = "lib_tests.rs"] +mod tests; diff --git a/bazel/rules/testing/wine/src/lib_tests.rs b/bazel/rules/testing/wine/src/lib_tests.rs new file mode 100644 index 000000000..3b11de730 --- /dev/null +++ b/bazel/rules/testing/wine/src/lib_tests.rs @@ -0,0 +1,232 @@ +use std::any::Any; +use std::future::Future; +use std::panic::AssertUnwindSafe; +use std::path::Path; +use std::path::PathBuf; + +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use futures::FutureExt; +use pretty_assertions::assert_eq; +use tokio::io::AsyncBufReadExt; +use tokio::io::BufReader; +use tokio::process::Command as TokioCommand; + +use super::WineTestCommand; +use super::WineTestProcess; + +async fn waiting_smoke_process() -> Result { + let executable = codex_utils_cargo_bin::cargo_bin("wine-smoke")?; + let mut process = WineTestCommand::new(executable).arg("--wait").spawn()?; + let mut lines = BufReader::new(process.take_stdout()).lines(); + let ready_line = lines + .next_line() + .await? + .context("Windows smoke process exited before becoming ready")?; + assert_eq!(ready_line, "WINE_TEST_READY"); + Ok(process) +} + +fn prefix_path(process: &WineTestProcess) -> PathBuf { + process + .processes + .as_ref() + .expect("Wine process guard") + .prefix + .path() + .to_path_buf() +} + +fn assert_prefix_removed(prefix: &Path) { + assert!( + !prefix.exists(), + "Wine prefix remains: {}", + prefix.display() + ); +} + +fn assert_panic_message(panic: Box, expected: &str) { + assert_eq!(panic.downcast_ref::<&str>(), Some(&expected)); +} + +async fn assert_future_panics(future: impl Future, expected: &str) { + let panic = match AssertUnwindSafe(future).catch_unwind().await { + Ok(_) => panic!("future should panic"), + Err(panic) => panic, + }; + assert_panic_message(panic, expected); +} + +async fn process_with_failing_wineserver_stop() -> Result { + let mut process = waiting_smoke_process().await?; + let processes = process.processes.as_mut().expect("Wine process guard"); + + let mut command = TokioCommand::from(processes.stop_wineserver_command()); + let status = command + .status() + .await + .context("pre-stop isolated wineserver")?; + assert!(status.success(), "wineserver exited with {status}"); + + processes.runtime.wineserver = processes.prefix.path().join("missing-wineserver"); + Ok(process) +} + +#[tokio::test] +async fn dropping_without_teardown_panics() -> Result<()> { + let process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + assert_future_panics( + async move { drop(process) }, + "WineTestProcess dropped without async teardown", + ) + .await; + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn dropping_while_panicking_does_not_panic_again() -> Result<()> { + let process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + assert_future_panics( + async move { + let _process = process; + panic!("sentinel panic"); + }, + "sentinel panic", + ) + .await; + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn async_teardown_disarms_drop_bomb() -> Result<()> { + let process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + process.shutdown().await?; + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn take_stdout_panics_when_called_twice() -> Result<()> { + let mut process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + assert_future_panics( + async { + process.take_stdout(); + }, + "Wine process stdout has already been taken", + ) + .await; + process.shutdown().await?; + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn scope_returns_value_and_tears_down() -> Result<()> { + let process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + let value = process + .scope(async { Ok::<_, anyhow::Error>("scope value") }) + .await?; + assert_eq!(value, "scope value"); + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn scope_returns_body_error_and_tears_down() -> Result<()> { + let process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + + let error = process + .scope(async { Err::<(), _>(anyhow!("scope body failed")) }) + .await + .expect_err("scope body should fail"); + + assert_eq!(error.to_string(), "scope body failed"); + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn scope_panic_preserves_panic_and_tears_down() -> Result<()> { + let process = waiting_smoke_process().await?; + let prefix = prefix_path(&process); + + assert_future_panics( + process.scope::<()>(async { panic!("scope panic") }), + "scope panic", + ) + .await; + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn shutdown_reports_nonzero_process_exit() -> Result<()> { + let executable = codex_utils_cargo_bin::cargo_bin("wine-smoke")?; + let mut process = WineTestCommand::new(executable).arg("--fail").spawn()?; + let prefix = prefix_path(&process); + let status = process + .processes + .as_mut() + .expect("Wine process guard") + .child + .wait() + .await?; + assert!( + !status.success(), + "Windows smoke process unexpectedly passed" + ); + + let error = process.shutdown().await.expect_err("shutdown should fail"); + + assert!(error.to_string().starts_with("Windows process exited with")); + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn scope_preserves_body_error_when_teardown_also_fails() -> Result<()> { + let process = process_with_failing_wineserver_stop().await?; + let prefix = prefix_path(&process); + + let error = process + .scope(async { Err::<(), _>(anyhow!("scope body failed")) }) + .await + .expect_err("scope body and teardown should fail"); + + assert!( + error + .to_string() + .starts_with("Wine teardown also failed: stop isolated wineserver"), + "unexpected error: {error:#}" + ); + assert_eq!( + error.chain().last().map(ToString::to_string), + Some("scope body failed".to_string()) + ); + assert_prefix_removed(&prefix); + Ok(()) +} + +#[tokio::test] +async fn shutdown_returns_teardown_error() -> Result<()> { + let process = process_with_failing_wineserver_stop().await?; + let prefix = prefix_path(&process); + + let error = process + .shutdown() + .await + .expect_err("shutdown should report a wineserver failure"); + + assert_eq!(error.to_string(), "stop isolated wineserver"); + assert_prefix_removed(&prefix); + Ok(()) +} diff --git a/defs.bzl b/defs.bzl index b255160b0..9685b2f0f 100644 --- a/defs.bzl +++ b/defs.bzl @@ -5,11 +5,13 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_proc_mac # Match Cargo's Windows linker behavior so Bazel-built binaries and tests use # the same stack reserve on both Windows ABIs and resolve UCRT imports on MSVC. +WINDOWS_GNULLVM_RUSTC_LINK_FLAGS = [ + "-C", + "link-arg=-Wl,--stack,8388608", # 8 MiB +] + WINDOWS_RUSTC_LINK_FLAGS = select({ - "@rules_rs//rs/experimental/platforms/constraints:windows_gnullvm": [ - "-C", - "link-arg=-Wl,--stack,8388608", # 8 MiB - ], + "@rules_rs//rs/experimental/platforms/constraints:windows_gnullvm": WINDOWS_GNULLVM_RUSTC_LINK_FLAGS, "@rules_rs//rs/experimental/platforms/constraints:windows_msvc": [ "-C", "link-arg=/STACK:8388608", # 8 MiB diff --git a/third_party/wine/BUILD.bazel b/third_party/wine/BUILD.bazel new file mode 100644 index 000000000..b0073861d --- /dev/null +++ b/third_party/wine/BUILD.bazel @@ -0,0 +1,40 @@ +package(default_visibility = ["//visibility:public"]) + +exports_files([ + "bin/wine", + "bin/wineserver", +]) + +filegroup( + name = "wine", + srcs = ["bin/wine"], + tags = ["manual"], +) + +filegroup( + name = "wineserver", + srcs = ["bin/wineserver"], + tags = ["manual"], +) + +filegroup( + name = "runtime_marker", + srcs = ["lib/wine/x86_64-unix/ntdll.so"], + tags = ["manual"], +) + +# Static import libraries are build-time-only, so omit them from test runfiles. +filegroup( + name = "runtime", + srcs = glob( + [ + "lib/wine/**", + "share/wine/**", + ], + # This BUILD file is also loaded from the source tree, where the + # archive-only paths are intentionally absent. + allow_empty = True, + exclude = ["**/*.a"], + ), + tags = ["manual"], +)