From 806aed4b2ac3c21371cc21b54b13c40e2936681f Mon Sep 17 00:00:00 2001 From: Aditya Kamath Date: Wed, 20 May 2026 03:52:49 -0500 Subject: [PATCH 09/10] Make AIX using blocking IO for UV. **Purpose**: Fix infinite hanging when building packages on AIX **Why needed**: Tokio async I/O does not detect EOF on AIX pipes **Solution**: Use blocking I/O with threads instead of async I/O on AIX **Impact**: CRITICAL - Without this, uv install hangs on any package build --- crates/uv-build-frontend/src/lib.rs | 210 +++++++++++++++++++++------- 1 file changed, 156 insertions(+), 54 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index 5ad3497ed..7e2074d45 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -24,7 +24,6 @@ use serde::de::{self, IntoDeserializer, SeqAccess, Visitor, value}; use serde::{Deserialize, Deserializer}; use tempfile::TempDir; use tokio::io::AsyncBufReadExt; -use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, warn}; use uv_auth::CredentialsCache; @@ -1200,62 +1199,165 @@ impl PythonRunner { let _permit = self.concurrent_build_slots.acquire().await.unwrap(); - let mut child = Command::new(venv.python_executable()) - .args(["-c", script]) - .current_dir(source_tree.simplified()) - .envs(environment_variables) - .env(EnvVars::PATH, modified_path) - .env(EnvVars::VIRTUAL_ENV, venv.root()) - // NOTE: it would be nice to get colored output from build backends, - // but setting CLICOLOR_FORCE=1 changes the output of underlying - // tools, which might mess with wrappers trying to parse their - // output. - .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace") - // Remove potentially-sensitive environment variables. - .env_remove(EnvVars::PYX_API_KEY) - .env_remove(EnvVars::UV_API_KEY) - .env_remove(EnvVars::PYX_AUTH_TOKEN) - .env_remove(EnvVars::UV_AUTH_TOKEN) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .spawn() - .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?; - - // Create buffers to capture `stdout` and `stderr`. - let mut stdout_buf = Vec::with_capacity(1024); - let mut stderr_buf = Vec::with_capacity(1024); - - // Create separate readers for `stdout` and `stderr`. - let stdout_reader = tokio::io::BufReader::new(child.stdout.take().unwrap()).split(b'\n'); - let stderr_reader = tokio::io::BufReader::new(child.stderr.take().unwrap()).split(b'\n'); - - // Asynchronously read from the in-memory pipes. - let printer = Printer::from(self.level); - let result = tokio::join!( - read_from(stdout_reader, printer, &mut stdout_buf), - read_from(stderr_reader, printer, &mut stderr_buf), - ); - match result { - (Ok(()), Ok(())) => {} - (Err(err), _) | (_, Err(err)) => { - return Err(Error::CommandFailed( - venv.python_executable().to_path_buf(), - err, - )); - } + // On AIX, use blocking I/O instead of async to avoid pipe EOF issues + #[cfg(target_os = "aix")] + { + use std::io::{BufRead, BufReader}; + use std::process::{Command as StdCommand, Stdio}; + + let python_exe = venv.python_executable().to_path_buf(); + let source_tree_path = source_tree.simplified().to_path_buf(); + let venv_root = venv.root().to_path_buf(); + let script_owned = script.to_string(); + let env_vars: Vec<(OsString, OsString)> = environment_variables.iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + let modified_path_owned = modified_path.clone(); + let printer_level = self.level; + + // Run the command in a blocking task to avoid blocking the async runtime + let (stdout_buf, stderr_buf, status) = tokio::task::spawn_blocking(move || { + let mut command = StdCommand::new(&python_exe); + command + .args(["-c", &script_owned]) + .current_dir(&source_tree_path) + .envs(env_vars) + .env(EnvVars::PATH, &modified_path_owned) + .env(EnvVars::VIRTUAL_ENV, &venv_root) + .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace") + .env_remove(EnvVars::PYX_API_KEY) + .env_remove(EnvVars::UV_API_KEY) + .env_remove(EnvVars::PYX_AUTH_TOKEN) + .env_remove(EnvVars::UV_AUTH_TOKEN) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = command.spawn() + .map_err(|err| Error::CommandFailed(python_exe.clone(), err))?; + + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + let mut stdout_buf = Vec::with_capacity(1024); + let mut stderr_buf = Vec::with_capacity(1024); + + // Use blocking I/O to read from pipes + let stdout_handle = std::thread::spawn(move || { + let mut lines = Vec::new(); + let reader = BufReader::new(stdout); + let mut printer = Printer::from(printer_level); + for line in reader.lines() { + match line { + Ok(line) => { + let _ = writeln!(printer, "{}", line); + lines.push(line); + } + Err(_) => break, + } + } + lines + }); + + let stderr_handle = std::thread::spawn(move || { + let mut lines = Vec::new(); + let reader = BufReader::new(stderr); + let mut printer = Printer::from(printer_level); + for line in reader.lines() { + match line { + Ok(line) => { + let _ = writeln!(printer, "{}", line); + lines.push(line); + } + Err(_) => break, + } + } + lines + }); + + // Wait for both threads to finish reading + stdout_buf = stdout_handle.join().unwrap(); + stderr_buf = stderr_handle.join().unwrap(); + + // Wait for child process + let status = child.wait() + .map_err(|err| Error::CommandFailed(python_exe, err))?; + + Ok::<_, Error>((stdout_buf, stderr_buf, status)) + }).await + .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), + io::Error::new(io::ErrorKind::Other, err.to_string())))??; + + return Ok(PythonRunnerOutput { + stdout: stdout_buf, + stderr: stderr_buf, + status, + }); } - // Wait for the child process to finish. - let status = child - .wait() - .await - .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?; + #[cfg(not(target_os = "aix"))] + { + use tokio::process::Command; + + let mut command = Command::new(venv.python_executable()); + command + .args(["-c", script]) + .current_dir(source_tree.simplified()) + .envs(environment_variables) + .env(EnvVars::PATH, modified_path) + .env(EnvVars::VIRTUAL_ENV, venv.root()) + // NOTE: it would be nice to get colored output from build backends, + // but setting CLICOLOR_FORCE=1 changes the output of underlying + // tools, which might mess with wrappers trying to parse their + // output. + .env(EnvVars::PYTHONIOENCODING, "utf-8:backslashreplace") + // Remove potentially-sensitive environment variables. + .env_remove(EnvVars::PYX_API_KEY) + .env_remove(EnvVars::UV_API_KEY) + .env_remove(EnvVars::PYX_AUTH_TOKEN) + .env_remove(EnvVars::UV_AUTH_TOKEN) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + + let mut child = command + .spawn() + .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?; + + // Create buffers to capture `stdout` and `stderr`. + let mut stdout_buf = Vec::with_capacity(1024); + let mut stderr_buf = Vec::with_capacity(1024); + + // Create separate readers for `stdout` and `stderr`. + let stdout_reader = tokio::io::BufReader::new(child.stdout.take().unwrap()).split(b'\n'); + let stderr_reader = tokio::io::BufReader::new(child.stderr.take().unwrap()).split(b'\n'); + + // Asynchronously read from the in-memory pipes. + let printer = Printer::from(self.level); + let result = tokio::join!( + read_from(stdout_reader, printer, &mut stdout_buf), + read_from(stderr_reader, printer, &mut stderr_buf), + ); + + let status = child + .wait() + .await + .map_err(|err| Error::CommandFailed(venv.python_executable().to_path_buf(), err))?; + + match result { + (Ok(()), Ok(())) => {} + (Err(err), _) | (_, Err(err)) => { + return Err(Error::CommandFailed( + venv.python_executable().to_path_buf(), + err, + )); + } + } - Ok(PythonRunnerOutput { - stdout: stdout_buf, - stderr: stderr_buf, - status, - }) + return Ok(PythonRunnerOutput { + stdout: stdout_buf, + stderr: stderr_buf, + status, + }); + } } } -- 2.51.2