From 4e39c1cd2053deaa2514ca1cfea375e5fb454162 Mon Sep 17 00:00:00 2001 From: Adam Perry Date: Sat, 13 Jun 2026 03:40:17 +0000 Subject: [PATCH] bazel: materialize PowerShell in Wine prefixes --- bazel/rules/testing/wine/src/lib.rs | 136 +++++++++++++++++++--- bazel/rules/testing/wine/src/lib_tests.rs | 27 +++++ 2 files changed, 149 insertions(+), 14 deletions(-) diff --git a/bazel/rules/testing/wine/src/lib.rs b/bazel/rules/testing/wine/src/lib.rs index 8f5eef1bf8..f4fb4bd2a7 100644 --- a/bazel/rules/testing/wine/src/lib.rs +++ b/bazel/rules/testing/wine/src/lib.rs @@ -2,6 +2,7 @@ compile_error!("wine_test_support can only run on Linux"); use std::ffi::OsString; +use std::fs; use std::future::Future; use std::io::Write; use std::path::Path; @@ -16,6 +17,10 @@ use tempfile::TempDir; use tokio::process::Child; use tokio::process::ChildStdout; use tokio::process::Command as TokioCommand; +use tokio::time::timeout; + +const ASYNC_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10); +const BLOCKING_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); /// Builds a command that runs a Windows executable in an isolated Wine prefix. pub struct WineTestCommand { @@ -42,6 +47,7 @@ struct WineProcesses { struct WineRuntimePaths { dll_path: PathBuf, + powershell_runtime: Option, wine: PathBuf, wineserver: PathBuf, } @@ -74,6 +80,9 @@ impl WineTestCommand { pub fn spawn(self) -> Result { let runtime = WineRuntimePaths::from_runfiles()?; let prefix = TempDir::new().context("create isolated Wine prefix")?; + if let Some(powershell_runtime) = runtime.powershell_runtime.as_deref() { + install_powershell_runtime(prefix.path(), powershell_runtime)?; + } let mut command = StdCommand::new(&runtime.wine); configure_wine_environment(&mut command, &runtime, prefix.path()); command @@ -164,8 +173,20 @@ impl WineRuntimePaths { .context("locate Wine runtime directory")? .to_path_buf(); let wineserver = codex_utils_cargo_bin::cargo_bin("wineserver")?; + let powershell_runtime = if std::env::var_os("CARGO_BIN_EXE_pwsh-runtime-marker").is_some() { + let marker = codex_utils_cargo_bin::cargo_bin("pwsh-runtime-marker")?; + Some( + marker + .parent() + .context("locate PowerShell runtime directory")? + .to_path_buf(), + ) + } else { + None + }; Ok(Self { dll_path, + powershell_runtime, wine, wineserver, }) @@ -185,10 +206,8 @@ impl WineProcesses { Err(error) => (Err(error).context("check Windows process status"), false), }; let wait_result = self - .child - .wait() + .wait_for_child() .await - .context("wait for Windows process running under Wine") .and_then(|status| { anyhow::ensure!( !check_exit_status || status.success(), @@ -196,20 +215,30 @@ impl WineProcesses { ); Ok(()) }); - let wineserver_result = async { + let wineserver_result = timeout(ASYNC_SHUTDOWN_TIMEOUT, 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; + }) + .await + .context("stop isolated wineserver timed out") + .and_then(std::convert::identity); - // 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 + let result = kill_result.and(wait_result).and(wineserver_result); + if result.is_ok() { + self.cleanup_complete = true; + } else { + self.shutdown_blocking(); + } + result + } + + async fn wait_for_child(&mut self) -> Result { + timeout(ASYNC_SHUTDOWN_TIMEOUT, self.child.wait()) + .await + .context("wait for Windows process running under Wine timed out")? + .context("wait for Windows process running under Wine") } fn stop_wineserver_command(&self) -> StdCommand { @@ -234,6 +263,7 @@ impl WineProcesses { } log_panic_cleanup(format_args!("Wine panic cleanup waiting for its child")); + let deadline = std::time::Instant::now() + BLOCKING_SHUTDOWN_TIMEOUT; loop { match self.child.try_wait() { Ok(Some(status)) => { @@ -242,7 +272,15 @@ impl WineProcesses { )); break; } - Ok(None) => std::thread::sleep(Duration::from_millis(10)), + Ok(None) if std::time::Instant::now() < deadline => { + std::thread::sleep(Duration::from_millis(10)); + } + Ok(None) => { + log_panic_cleanup(format_args!( + "Wine panic cleanup timed out waiting for its child" + )); + break; + } Err(error) => { log_panic_cleanup(format_args!( "Wine panic cleanup could not wait for its child: {error}" @@ -253,7 +291,7 @@ impl WineProcesses { } log_panic_cleanup(format_args!("Wine panic cleanup stopping its wineserver")); - match self.stop_wineserver_command().status() { + match self.kill_wineserver_command().status() { Ok(status) => log_panic_cleanup(format_args!( "Wine panic cleanup wineserver exited with {status}" )), @@ -264,6 +302,16 @@ impl WineProcesses { self.cleanup_complete = true; log_panic_cleanup(format_args!("Wine panic cleanup complete")); } + + fn kill_wineserver_command(&self) -> StdCommand { + let mut command = StdCommand::new(&self.runtime.wineserver); + configure_wine_environment(&mut command, &self.runtime, self.prefix.path()); + command + .arg("-k") + .stdout(Stdio::null()) + .stderr(Stdio::null()); + command + } } impl Drop for WineProcesses { @@ -298,6 +346,66 @@ fn configure_wine_environment(command: &mut StdCommand, runtime: &WineRuntimePat .env("TMP", r"C:\windows\temp"); } +/// Installs the opt-in pinned PowerShell runtime into a Wine prefix. +/// +/// The calling `wine_rust_test` target must set `include_powershell = True`; +/// otherwise the runtime marker is absent. +pub fn install_pinned_powershell_runtime(prefix: &Path) -> Result<()> { + let marker = codex_utils_cargo_bin::cargo_bin("pwsh-runtime-marker")?; + let runtime = marker + .parent() + .context("locate PowerShell runtime directory")?; + install_powershell_runtime(prefix, runtime) +} + +fn install_powershell_runtime(prefix: &Path, runtime: &Path) -> Result<()> { + let powershell_parent = prefix + .join("drive_c") + .join("Program Files") + .join("PowerShell"); + fs::create_dir_all(&powershell_parent).context("create PowerShell installation parent")?; + let destination = powershell_parent.join("7"); + materialize_runtime_directory(runtime, &destination) +} + +fn materialize_runtime_directory(source: &Path, destination: &Path) -> Result<()> { + fs::create_dir_all(destination).with_context(|| { + format!( + "create PowerShell runtime directory {}", + destination.display() + ) + })?; + for entry in fs::read_dir(source) + .with_context(|| format!("read PowerShell runtime directory {}", source.display()))? + { + let entry = entry.context("read PowerShell runtime entry")?; + let source_path = entry.path(); + let destination_path = destination.join(entry.file_name()); + let file_type = entry + .file_type() + .with_context(|| format!("inspect PowerShell runtime entry {}", source_path.display()))?; + if file_type.is_dir() { + materialize_runtime_directory(&source_path, &destination_path)?; + } else if file_type.is_file() { + if fs::hard_link(&source_path, &destination_path).is_err() { + fs::copy(&source_path, &destination_path).with_context(|| { + format!( + "copy PowerShell runtime file {} to {}", + source_path.display(), + destination_path.display() + ) + })?; + } + } else { + anyhow::bail!( + "unsupported PowerShell runtime entry type at {}", + source_path.display() + ); + } + } + Ok(()) +} + #[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 index 3b11de730a..2568ca6da2 100644 --- a/bazel/rules/testing/wine/src/lib_tests.rs +++ b/bazel/rules/testing/wine/src/lib_tests.rs @@ -1,5 +1,6 @@ use std::any::Any; use std::future::Future; +use std::fs; use std::panic::AssertUnwindSafe; use std::path::Path; use std::path::PathBuf; @@ -9,12 +10,38 @@ use anyhow::Result; use anyhow::anyhow; use futures::FutureExt; use pretty_assertions::assert_eq; +use tempfile::TempDir; use tokio::io::AsyncBufReadExt; use tokio::io::BufReader; use tokio::process::Command as TokioCommand; use super::WineTestCommand; use super::WineTestProcess; +use super::install_powershell_runtime; + +#[test] +fn powershell_runtime_is_materialized_at_the_windows_fallback_path() -> Result<()> { + let prefix = TempDir::new()?; + let runtime = TempDir::new()?; + fs::create_dir(runtime.path().join("Modules"))?; + fs::write(runtime.path().join("pwsh.exe"), b"pwsh")?; + fs::write(runtime.path().join("Modules").join("marker.txt"), b"module")?; + + install_powershell_runtime(prefix.path(), runtime.path())?; + + let installed = prefix + .path() + .join("drive_c") + .join("Program Files") + .join("PowerShell") + .join("7"); + assert_eq!(fs::read(installed.join("pwsh.exe"))?, b"pwsh"); + assert_eq!( + fs::read(installed.join("Modules").join("marker.txt"))?, + b"module" + ); + Ok(()) +} async fn waiting_smoke_process() -> Result { let executable = codex_utils_cargo_bin::cargo_bin("wine-smoke")?;