| // Copyright 2020 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| mod flags; |
| mod options; |
| mod output; |
| mod rustc; |
| mod util; |
| |
| use std::fmt; |
| use std::fs::{copy, OpenOptions}; |
| use std::io; |
| use std::process::{exit, Command, ExitStatus, Stdio}; |
| |
| use crate::options::options; |
| use crate::output::{process_output, LineOutput}; |
| |
| #[cfg(windows)] |
| fn status_code(status: ExitStatus, was_killed: bool) -> i32 { |
| // On windows, there's no good way to know if the process was killed by a signal. |
| // If we killed the process, we override the code to signal success. |
| if was_killed { |
| 0 |
| } else { |
| status.code().unwrap_or(1) |
| } |
| } |
| |
| #[cfg(not(windows))] |
| fn status_code(status: ExitStatus, was_killed: bool) -> i32 { |
| // On unix, if code is None it means that the process was killed by a signal. |
| // https://doc.rust-lang.org/std/process/struct.ExitStatus.html#method.success |
| match status.code() { |
| Some(code) => code, |
| // If we killed the process, we expect None here |
| None if was_killed => 0, |
| // Otherwise it's some unexpected signal |
| None => 1, |
| } |
| } |
| |
| #[derive(Debug)] |
| struct ProcessWrapperError(String); |
| |
| impl fmt::Display for ProcessWrapperError { |
| fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| write!(f, "process wrapper error: {}", self.0) |
| } |
| } |
| |
| impl std::error::Error for ProcessWrapperError {} |
| |
| macro_rules! log { |
| ($($arg:tt)*) => { |
| if std::env::var_os("RULES_RUST_PROCESS_WRAPPER_DEBUG").is_some() { |
| eprintln!($($arg)*); |
| } |
| }; |
| } |
| |
| fn main() -> Result<(), ProcessWrapperError> { |
| let opts = options().map_err(|e| ProcessWrapperError(e.to_string()))?; |
| |
| let mut command = Command::new(opts.executable); |
| command |
| .args(opts.child_arguments) |
| .env_clear() |
| .envs(opts.child_environment) |
| .stdout(if let Some(stdout_file) = opts.stdout_file { |
| OpenOptions::new() |
| .create(true) |
| .truncate(true) |
| .write(true) |
| .open(stdout_file) |
| .map_err(|e| ProcessWrapperError(format!("unable to open stdout file: {}", e)))? |
| .into() |
| } else { |
| Stdio::inherit() |
| }) |
| .stderr(Stdio::piped()); |
| log!("{:#?}", command); |
| let mut child = command |
| .spawn() |
| .map_err(|e| ProcessWrapperError(format!("failed to spawn child process: {}", e)))?; |
| |
| let mut stderr: Box<dyn io::Write> = if let Some(stderr_file) = opts.stderr_file { |
| Box::new( |
| OpenOptions::new() |
| .create(true) |
| .truncate(true) |
| .write(true) |
| .open(stderr_file) |
| .map_err(|e| ProcessWrapperError(format!("unable to open stderr file: {}", e)))?, |
| ) |
| } else { |
| Box::new(io::stderr()) |
| }; |
| |
| let mut child_stderr = child.stderr.take().ok_or(ProcessWrapperError( |
| "unable to get child stderr".to_string(), |
| ))?; |
| |
| let mut output_file: Option<std::fs::File> = if let Some(output_file_name) = opts.output_file { |
| Some( |
| OpenOptions::new() |
| .create(true) |
| .truncate(true) |
| .write(true) |
| .open(output_file_name) |
| .map_err(|e| ProcessWrapperError(format!("Unable to open output_file: {}", e)))?, |
| ) |
| } else { |
| None |
| }; |
| |
| let mut was_killed = false; |
| let result = if let Some(format) = opts.rustc_output_format { |
| let quit_on_rmeta = opts.rustc_quit_on_rmeta; |
| // Process json rustc output and kill the subprocess when we get a signal |
| // that we emitted a metadata file. |
| let mut me = false; |
| let metadata_emitted = &mut me; |
| let result = process_output( |
| &mut child_stderr, |
| stderr.as_mut(), |
| output_file.as_mut(), |
| move |line| { |
| if quit_on_rmeta { |
| rustc::stop_on_rmeta_completion(line, format, metadata_emitted) |
| } else { |
| rustc::process_json(line, format) |
| } |
| }, |
| ); |
| if me { |
| // If recv returns Ok(), a signal was sent in this channel so we should terminate the child process. |
| // We can safely ignore the Result from kill() as we don't care if the process already terminated. |
| let _ = child.kill(); |
| was_killed = true; |
| } |
| result |
| } else { |
| // Process output normally by forwarding stderr |
| process_output( |
| &mut child_stderr, |
| stderr.as_mut(), |
| output_file.as_mut(), |
| move |line| Ok(LineOutput::Message(line)), |
| ) |
| }; |
| result.map_err(|e| ProcessWrapperError(format!("failed to process stderr: {}", e)))?; |
| |
| let status = child |
| .wait() |
| .map_err(|e| ProcessWrapperError(format!("failed to wait for child process: {}", e)))?; |
| // If the child process is rustc and is killed after metadata generation, that's also a success. |
| let code = status_code(status, was_killed); |
| let success = code == 0; |
| if success { |
| if let Some(tf) = opts.touch_file { |
| OpenOptions::new() |
| .create(true) |
| .truncate(true) |
| .write(true) |
| .open(tf) |
| .map_err(|e| ProcessWrapperError(format!("failed to create touch file: {}", e)))?; |
| } |
| if let Some((copy_source, copy_dest)) = opts.copy_output { |
| copy(©_source, ©_dest).map_err(|e| { |
| ProcessWrapperError(format!( |
| "failed to copy {} into {}: {}", |
| copy_source, copy_dest, e |
| )) |
| })?; |
| } |
| } |
| |
| exit(code) |
| } |