| 1 | //! Miscellaneous helpers for running commands |
| 2 | |
| 3 | use std::{ |
| 4 | collections::hash_map, |
| 5 | ffi::OsString, |
| 6 | fmt::Display, |
| 7 | fs, |
| 8 | hash::Hasher, |
| 9 | io::{self, Read, Write}, |
| 10 | path::Path, |
| 11 | process::{Child, ChildStderr, Command, Stdio}, |
| 12 | sync::{ |
| 13 | atomic::{AtomicBool, Ordering}, |
| 14 | Arc, |
| 15 | }, |
| 16 | }; |
| 17 | |
| 18 | use crate::{Error, ErrorKind, Object}; |
| 19 | |
| 20 | #[derive (Clone, Debug)] |
| 21 | pub(crate) struct CargoOutput { |
| 22 | pub(crate) metadata: bool, |
| 23 | pub(crate) warnings: bool, |
| 24 | pub(crate) debug: bool, |
| 25 | pub(crate) output: OutputKind, |
| 26 | checked_dbg_var: Arc<AtomicBool>, |
| 27 | } |
| 28 | |
| 29 | /// Different strategies for handling compiler output (to stdout) |
| 30 | #[derive (Clone, Debug)] |
| 31 | pub(crate) enum OutputKind { |
| 32 | /// Forward the output to this process' stdout ([`Stdio::inherit()`]) |
| 33 | Forward, |
| 34 | /// Discard the output ([`Stdio::null()`]) |
| 35 | Discard, |
| 36 | /// Capture the result (`[Stdio::piped()`]) |
| 37 | Capture, |
| 38 | } |
| 39 | |
| 40 | impl CargoOutput { |
| 41 | pub(crate) fn new() -> Self { |
| 42 | #[allow (clippy::disallowed_methods)] |
| 43 | Self { |
| 44 | metadata: true, |
| 45 | warnings: true, |
| 46 | output: OutputKind::Forward, |
| 47 | debug: match std::env::var_os("CC_ENABLE_DEBUG_OUTPUT" ) { |
| 48 | Some(v) => v != "0" && v != "false" && v != "" , |
| 49 | None => false, |
| 50 | }, |
| 51 | checked_dbg_var: Arc::new(AtomicBool::new(false)), |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | pub(crate) fn print_metadata(&self, s: &dyn Display) { |
| 56 | if self.metadata { |
| 57 | println!(" {}" , s); |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | pub(crate) fn print_warning(&self, arg: &dyn Display) { |
| 62 | if self.warnings { |
| 63 | println!("cargo:warning= {}" , arg); |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | pub(crate) fn print_debug(&self, arg: &dyn Display) { |
| 68 | if self.metadata && !self.checked_dbg_var.load(Ordering::Relaxed) { |
| 69 | self.checked_dbg_var.store(true, Ordering::Relaxed); |
| 70 | println!("cargo:rerun-if-env-changed=CC_ENABLE_DEBUG_OUTPUT" ); |
| 71 | } |
| 72 | if self.debug { |
| 73 | println!(" {}" , arg); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | fn stdio_for_warnings(&self) -> Stdio { |
| 78 | if self.warnings { |
| 79 | Stdio::piped() |
| 80 | } else { |
| 81 | Stdio::null() |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | fn stdio_for_output(&self) -> Stdio { |
| 86 | match self.output { |
| 87 | OutputKind::Capture => Stdio::piped(), |
| 88 | OutputKind::Forward => Stdio::inherit(), |
| 89 | OutputKind::Discard => Stdio::null(), |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | pub(crate) struct StderrForwarder { |
| 95 | inner: Option<(ChildStderr, Vec<u8>)>, |
| 96 | #[cfg (feature = "parallel" )] |
| 97 | is_non_blocking: bool, |
| 98 | #[cfg (feature = "parallel" )] |
| 99 | bytes_available_failed: bool, |
| 100 | /// number of bytes buffered in inner |
| 101 | bytes_buffered: usize, |
| 102 | } |
| 103 | |
| 104 | const MIN_BUFFER_CAPACITY: usize = 100; |
| 105 | |
| 106 | impl StderrForwarder { |
| 107 | pub(crate) fn new(child: &mut Child) -> Self { |
| 108 | Self { |
| 109 | inner: child |
| 110 | .stderr |
| 111 | .take() |
| 112 | .map(|stderr| (stderr, Vec::with_capacity(MIN_BUFFER_CAPACITY))), |
| 113 | bytes_buffered: 0, |
| 114 | #[cfg (feature = "parallel" )] |
| 115 | is_non_blocking: false, |
| 116 | #[cfg (feature = "parallel" )] |
| 117 | bytes_available_failed: false, |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | fn forward_available(&mut self) -> bool { |
| 122 | if let Some((stderr, buffer)) = self.inner.as_mut() { |
| 123 | loop { |
| 124 | // For non-blocking we check to see if there is data available, so we should try to |
| 125 | // read at least that much. For blocking, always read at least the minimum amount. |
| 126 | #[cfg (not(feature = "parallel" ))] |
| 127 | let to_reserve = MIN_BUFFER_CAPACITY; |
| 128 | #[cfg (feature = "parallel" )] |
| 129 | let to_reserve = if self.is_non_blocking && !self.bytes_available_failed { |
| 130 | match crate::parallel::stderr::bytes_available(stderr) { |
| 131 | #[cfg (windows)] |
| 132 | Ok(0) => break false, |
| 133 | #[cfg (unix)] |
| 134 | Ok(0) => { |
| 135 | // On Unix, depending on the implementation, we may sometimes get 0 in a |
| 136 | // loop (either there is data available or the pipe is broken), so |
| 137 | // continue with the non-blocking read anyway. |
| 138 | MIN_BUFFER_CAPACITY |
| 139 | } |
| 140 | #[cfg (windows)] |
| 141 | Err(_) => { |
| 142 | // On Windows, if we get an error then the pipe is broken, so flush |
| 143 | // the buffer and bail. |
| 144 | if !buffer.is_empty() { |
| 145 | write_warning(&buffer[..]); |
| 146 | } |
| 147 | self.inner = None; |
| 148 | break true; |
| 149 | } |
| 150 | #[cfg (unix)] |
| 151 | Err(_) => { |
| 152 | // On Unix, depending on the implementation, we may get spurious |
| 153 | // errors so make a note not to use bytes_available again and try |
| 154 | // the non-blocking read anyway. |
| 155 | self.bytes_available_failed = true; |
| 156 | MIN_BUFFER_CAPACITY |
| 157 | } |
| 158 | #[cfg (target_family = "wasm" )] |
| 159 | Err(_) => panic!("bytes_available should always succeed on wasm" ), |
| 160 | Ok(bytes_available) => MIN_BUFFER_CAPACITY.max(bytes_available), |
| 161 | } |
| 162 | } else { |
| 163 | MIN_BUFFER_CAPACITY |
| 164 | }; |
| 165 | if self.bytes_buffered + to_reserve > buffer.len() { |
| 166 | buffer.resize(self.bytes_buffered + to_reserve, 0); |
| 167 | } |
| 168 | |
| 169 | match stderr.read(&mut buffer[self.bytes_buffered..]) { |
| 170 | Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { |
| 171 | // No data currently, yield back. |
| 172 | break false; |
| 173 | } |
| 174 | Err(err) if err.kind() == std::io::ErrorKind::Interrupted => { |
| 175 | // Interrupted, try again. |
| 176 | continue; |
| 177 | } |
| 178 | Ok(bytes_read) if bytes_read != 0 => { |
| 179 | self.bytes_buffered += bytes_read; |
| 180 | let mut consumed = 0; |
| 181 | for line in buffer[..self.bytes_buffered].split_inclusive(|&b| b == b' \n' ) { |
| 182 | // Only forward complete lines, leave the rest in the buffer. |
| 183 | if let Some((b' \n' , line)) = line.split_last() { |
| 184 | consumed += line.len() + 1; |
| 185 | write_warning(line); |
| 186 | } |
| 187 | } |
| 188 | if consumed > 0 && consumed < self.bytes_buffered { |
| 189 | // Remove the consumed bytes from buffer |
| 190 | buffer.copy_within(consumed.., 0); |
| 191 | } |
| 192 | self.bytes_buffered -= consumed; |
| 193 | } |
| 194 | res => { |
| 195 | // End of stream: flush remaining data and bail. |
| 196 | if self.bytes_buffered > 0 { |
| 197 | write_warning(&buffer[..self.bytes_buffered]); |
| 198 | } |
| 199 | if let Err(err) = res { |
| 200 | write_warning( |
| 201 | format!("Failed to read from child stderr: {err}" ).as_bytes(), |
| 202 | ); |
| 203 | } |
| 204 | self.inner.take(); |
| 205 | break true; |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | } else { |
| 210 | true |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | #[cfg (feature = "parallel" )] |
| 215 | pub(crate) fn set_non_blocking(&mut self) -> Result<(), Error> { |
| 216 | assert!(!self.is_non_blocking); |
| 217 | |
| 218 | #[cfg (unix)] |
| 219 | if let Some((stderr, _)) = self.inner.as_ref() { |
| 220 | crate::parallel::stderr::set_non_blocking(stderr)?; |
| 221 | } |
| 222 | |
| 223 | self.is_non_blocking = true; |
| 224 | Ok(()) |
| 225 | } |
| 226 | |
| 227 | #[cfg (feature = "parallel" )] |
| 228 | fn forward_all(&mut self) { |
| 229 | while !self.forward_available() {} |
| 230 | } |
| 231 | |
| 232 | #[cfg (not(feature = "parallel" ))] |
| 233 | fn forward_all(&mut self) { |
| 234 | let forward_result = self.forward_available(); |
| 235 | assert!(forward_result, "Should have consumed all data" ); |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | fn write_warning(line: &[u8]) { |
| 240 | let stdout: Stdout = io::stdout(); |
| 241 | let mut stdout: StdoutLock<'static> = stdout.lock(); |
| 242 | stdout.write_all(buf:b"cargo:warning=" ).unwrap(); |
| 243 | stdout.write_all(buf:line).unwrap(); |
| 244 | stdout.write_all(buf:b" \n" ).unwrap(); |
| 245 | } |
| 246 | |
| 247 | fn wait_on_child( |
| 248 | cmd: &Command, |
| 249 | program: &Path, |
| 250 | child: &mut Child, |
| 251 | cargo_output: &CargoOutput, |
| 252 | ) -> Result<(), Error> { |
| 253 | StderrForwarder::new(child).forward_all(); |
| 254 | |
| 255 | let status = match child.wait() { |
| 256 | Ok(s) => s, |
| 257 | Err(e) => { |
| 258 | return Err(Error::new( |
| 259 | ErrorKind::ToolExecError, |
| 260 | format!( |
| 261 | "Failed to wait on spawned child process, command {:?} with args {}: {}." , |
| 262 | cmd, |
| 263 | program.display(), |
| 264 | e |
| 265 | ), |
| 266 | )); |
| 267 | } |
| 268 | }; |
| 269 | |
| 270 | cargo_output.print_debug(&status); |
| 271 | |
| 272 | if status.success() { |
| 273 | Ok(()) |
| 274 | } else { |
| 275 | Err(Error::new( |
| 276 | ErrorKind::ToolExecError, |
| 277 | format!( |
| 278 | "Command {:?} with args {} did not execute successfully (status code {})." , |
| 279 | cmd, |
| 280 | program.display(), |
| 281 | status |
| 282 | ), |
| 283 | )) |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | /// Find the destination object path for each file in the input source files, |
| 288 | /// and store them in the output Object. |
| 289 | pub(crate) fn objects_from_files(files: &[Arc<Path>], dst: &Path) -> Result<Vec<Object>, Error> { |
| 290 | let mut objects = Vec::with_capacity(files.len()); |
| 291 | for file in files { |
| 292 | let basename = file |
| 293 | .file_name() |
| 294 | .ok_or_else(|| { |
| 295 | Error::new( |
| 296 | ErrorKind::InvalidArgument, |
| 297 | "No file_name for object file path!" , |
| 298 | ) |
| 299 | })? |
| 300 | .to_string_lossy(); |
| 301 | let dirname = file |
| 302 | .parent() |
| 303 | .ok_or_else(|| { |
| 304 | Error::new( |
| 305 | ErrorKind::InvalidArgument, |
| 306 | "No parent for object file path!" , |
| 307 | ) |
| 308 | })? |
| 309 | .to_string_lossy(); |
| 310 | |
| 311 | // Hash the dirname. This should prevent conflicts if we have multiple |
| 312 | // object files with the same filename in different subfolders. |
| 313 | let mut hasher = hash_map::DefaultHasher::new(); |
| 314 | hasher.write(dirname.to_string().as_bytes()); |
| 315 | let obj = dst |
| 316 | .join(format!(" {:016x}- {}" , hasher.finish(), basename)) |
| 317 | .with_extension("o" ); |
| 318 | |
| 319 | match obj.parent() { |
| 320 | Some(s) => fs::create_dir_all(s)?, |
| 321 | None => { |
| 322 | return Err(Error::new( |
| 323 | ErrorKind::InvalidArgument, |
| 324 | "dst is an invalid path with no parent" , |
| 325 | )); |
| 326 | } |
| 327 | }; |
| 328 | |
| 329 | objects.push(Object::new(file.to_path_buf(), obj)); |
| 330 | } |
| 331 | |
| 332 | Ok(objects) |
| 333 | } |
| 334 | |
| 335 | pub(crate) fn run( |
| 336 | cmd: &mut Command, |
| 337 | program: impl AsRef<Path>, |
| 338 | cargo_output: &CargoOutput, |
| 339 | ) -> Result<(), Error> { |
| 340 | let program: &Path = program.as_ref(); |
| 341 | |
| 342 | let mut child: Child = spawn(cmd, program, cargo_output)?; |
| 343 | wait_on_child(cmd, program, &mut child, cargo_output) |
| 344 | } |
| 345 | |
| 346 | pub(crate) fn run_output( |
| 347 | cmd: &mut Command, |
| 348 | program: impl AsRef<Path>, |
| 349 | cargo_output: &CargoOutput, |
| 350 | ) -> Result<Vec<u8>, Error> { |
| 351 | let program: &Path = program.as_ref(); |
| 352 | |
| 353 | // We specifically need the output to be captured, so override default |
| 354 | let mut captured_cargo_output: CargoOutput = cargo_output.clone(); |
| 355 | captured_cargo_output.output = OutputKind::Capture; |
| 356 | let mut child: Child = spawn(cmd, program, &captured_cargo_output)?; |
| 357 | |
| 358 | let mut stdout: Vec = vec![]; |
| 359 | childResult |
| 360 | .stdout |
| 361 | .take() |
| 362 | .unwrap() |
| 363 | .read_to_end(&mut stdout) |
| 364 | .unwrap(); |
| 365 | |
| 366 | // Don't care about this output, use the normal settings |
| 367 | wait_on_child(cmd, program, &mut child, cargo_output)?; |
| 368 | |
| 369 | Ok(stdout) |
| 370 | } |
| 371 | |
| 372 | pub(crate) fn spawn( |
| 373 | cmd: &mut Command, |
| 374 | program: &Path, |
| 375 | cargo_output: &CargoOutput, |
| 376 | ) -> Result<Child, Error> { |
| 377 | struct ResetStderr<'cmd>(&'cmd mut Command); |
| 378 | |
| 379 | impl Drop for ResetStderr<'_> { |
| 380 | fn drop(&mut self) { |
| 381 | // Reset stderr to default to release pipe_writer so that print thread will |
| 382 | // not block forever. |
| 383 | self.0.stderr(Stdio::inherit()); |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | cargo_output.print_debug(&format_args!("running: {:?}" , cmd)); |
| 388 | |
| 389 | let cmd = ResetStderr(cmd); |
| 390 | let child = cmd |
| 391 | .0 |
| 392 | .stderr(cargo_output.stdio_for_warnings()) |
| 393 | .stdout(cargo_output.stdio_for_output()) |
| 394 | .spawn(); |
| 395 | match child { |
| 396 | Ok(child) => Ok(child), |
| 397 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => { |
| 398 | let extra = if cfg!(windows) { |
| 399 | " (see https://docs.rs/cc/latest/cc/#compile-time-requirements \ |
| 400 | for help)" |
| 401 | } else { |
| 402 | "" |
| 403 | }; |
| 404 | Err(Error::new( |
| 405 | ErrorKind::ToolNotFound, |
| 406 | format!( |
| 407 | "Failed to find tool. Is ` {}` installed? {}" , |
| 408 | program.display(), |
| 409 | extra |
| 410 | ), |
| 411 | )) |
| 412 | } |
| 413 | Err(e) => Err(Error::new( |
| 414 | ErrorKind::ToolExecError, |
| 415 | format!( |
| 416 | "Command {:?} with args {} failed to start: {:?}" , |
| 417 | cmd.0, |
| 418 | program.display(), |
| 419 | e |
| 420 | ), |
| 421 | )), |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | pub(crate) struct CmdAddOutputFileArgs { |
| 426 | pub(crate) cuda: bool, |
| 427 | pub(crate) is_assembler_msvc: bool, |
| 428 | pub(crate) msvc: bool, |
| 429 | pub(crate) clang: bool, |
| 430 | pub(crate) gnu: bool, |
| 431 | pub(crate) is_asm: bool, |
| 432 | pub(crate) is_arm: bool, |
| 433 | } |
| 434 | |
| 435 | pub(crate) fn command_add_output_file(cmd: &mut Command, dst: &Path, args: CmdAddOutputFileArgs) { |
| 436 | if args.is_assembler_msvc |
| 437 | || !(!args.msvc || args.clang || args.gnu || args.cuda || (args.is_asm && args.is_arm)) |
| 438 | { |
| 439 | let mut s: OsString = OsString::from("-Fo" ); |
| 440 | s.push(dst); |
| 441 | cmd.arg(s); |
| 442 | } else { |
| 443 | cmd.arg("-o" ).arg(dst); |
| 444 | } |
| 445 | } |
| 446 | |
| 447 | #[cfg (feature = "parallel" )] |
| 448 | pub(crate) fn try_wait_on_child( |
| 449 | cmd: &Command, |
| 450 | program: &Path, |
| 451 | child: &mut Child, |
| 452 | stdout: &mut dyn io::Write, |
| 453 | stderr_forwarder: &mut StderrForwarder, |
| 454 | ) -> Result<Option<()>, Error> { |
| 455 | stderr_forwarder.forward_available(); |
| 456 | |
| 457 | match child.try_wait() { |
| 458 | Ok(Some(status)) => { |
| 459 | stderr_forwarder.forward_all(); |
| 460 | |
| 461 | let _ = writeln!(stdout, " {}" , status); |
| 462 | |
| 463 | if status.success() { |
| 464 | Ok(Some(())) |
| 465 | } else { |
| 466 | Err(Error::new( |
| 467 | ErrorKind::ToolExecError, |
| 468 | format!( |
| 469 | "Command {:?} with args {} did not execute successfully (status code {})." , |
| 470 | cmd, |
| 471 | program.display(), |
| 472 | status |
| 473 | ), |
| 474 | )) |
| 475 | } |
| 476 | } |
| 477 | Ok(None) => Ok(None), |
| 478 | Err(e) => { |
| 479 | stderr_forwarder.forward_all(); |
| 480 | Err(Error::new( |
| 481 | ErrorKind::ToolExecError, |
| 482 | format!( |
| 483 | "Failed to wait on spawned child process, command {:?} with args {}: {}." , |
| 484 | cmd, |
| 485 | program.display(), |
| 486 | e |
| 487 | ), |
| 488 | )) |
| 489 | } |
| 490 | } |
| 491 | } |
| 492 | |