| // 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::collections::HashMap; |
| use std::fmt; |
| use std::fs::{copy, OpenOptions}; |
| use std::io; |
| use std::process::{exit, Command, ExitStatus, Stdio}; |
| |
| use tinyjson::JsonValue; |
| |
| use crate::options::options; |
| use crate::output::{process_output, LineOutput}; |
| use crate::rustc::ErrorFormat; |
| |
| #[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! debug_log { |
| ($($arg:tt)*) => { |
| if std::env::var_os("RULES_RUST_PROCESS_WRAPPER_DEBUG").is_some() { |
| eprintln!($($arg)*); |
| } |
| }; |
| } |
| |
| fn json_warning(line: &str) -> JsonValue { |
| JsonValue::Object(HashMap::from([ |
| ( |
| "$message_type".to_string(), |
| JsonValue::String("diagnostic".to_string()), |
| ), |
| ("message".to_string(), JsonValue::String(line.to_string())), |
| ("code".to_string(), JsonValue::Null), |
| ( |
| "level".to_string(), |
| JsonValue::String("warning".to_string()), |
| ), |
| ("spans".to_string(), JsonValue::Array(Vec::new())), |
| ("children".to_string(), JsonValue::Array(Vec::new())), |
| ("rendered".to_string(), JsonValue::String(line.to_string())), |
| ])) |
| } |
| |
| fn process_line( |
| mut line: String, |
| quit_on_rmeta: bool, |
| format: ErrorFormat, |
| metadata_emitted: &mut bool, |
| ) -> Result<LineOutput, String> { |
| // LLVM can emit lines that look like the following, and these will be interspersed |
| // with the regular JSON output. Arguably, rustc should be fixed not to emit lines |
| // like these (or to convert them to JSON), but for now we convert them to JSON |
| // ourselves. |
| if line.contains("is not a recognized feature for this target (ignoring feature)") { |
| if let Ok(json_str) = json_warning(&line).stringify() { |
| line = json_str; |
| } else { |
| return Ok(LineOutput::Skip); |
| } |
| } |
| if quit_on_rmeta { |
| rustc::stop_on_rmeta_completion(line, format, metadata_emitted) |
| } else { |
| rustc::process_json(line, format) |
| } |
| } |
| |
| 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()); |
| debug_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| process_line(line, quit_on_rmeta, format, metadata_emitted), |
| ); |
| 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) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| fn parse_json(json_str: &str) -> Result<JsonValue, String> { |
| json_str.parse::<JsonValue>().map_err(|e| e.to_string()) |
| } |
| |
| #[test] |
| fn test_process_line_diagnostic_json() -> Result<(), String> { |
| let mut metadata_emitted = false; |
| let LineOutput::Message(msg) = process_line( |
| r#" |
| { |
| "$message_type": "diagnostic", |
| "rendered": "Diagnostic message" |
| } |
| "# |
| .to_string(), |
| false, |
| ErrorFormat::Json, |
| &mut metadata_emitted, |
| )? |
| else { |
| return Err("Expected a LineOutput::Message".to_string()); |
| }; |
| assert_eq!( |
| parse_json(&msg)?, |
| parse_json( |
| r#" |
| { |
| "$message_type": "diagnostic", |
| "rendered": "Diagnostic message" |
| } |
| "# |
| )? |
| ); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_process_line_diagnostic_rendered() -> Result<(), String> { |
| let mut metadata_emitted = false; |
| let LineOutput::Message(msg) = process_line( |
| r#" |
| { |
| "$message_type": "diagnostic", |
| "rendered": "Diagnostic message" |
| } |
| "# |
| .to_string(), |
| /*quit_on_rmeta=*/ false, |
| ErrorFormat::Rendered, |
| &mut metadata_emitted, |
| )? |
| else { |
| return Err("Expected a LineOutput::Message".to_string()); |
| }; |
| assert_eq!(msg, "Diagnostic message"); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_process_line_llvm_feature_warning() -> Result<(), String> { |
| let mut metadata_emitted = false; |
| let LineOutput::Message(msg) = process_line( |
| "'+zaamo' is not a recognized feature for this target (ignoring feature)".to_string(), |
| /*quit_on_rmeta=*/ false, |
| ErrorFormat::Json, |
| &mut metadata_emitted, |
| )? |
| else { |
| return Err("Expected a LineOutput::Message".to_string()); |
| }; |
| assert_eq!( |
| parse_json(&msg)?, |
| parse_json( |
| r#" |
| { |
| "$message_type": "diagnostic", |
| "message": "'+zaamo' is not a recognized feature for this target (ignoring feature)", |
| "code": null, |
| "level": "warning", |
| "spans": [], |
| "children": [], |
| "rendered": "'+zaamo' is not a recognized feature for this target (ignoring feature)" |
| } |
| "# |
| )? |
| ); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_process_line_emit_link() -> Result<(), String> { |
| let mut metadata_emitted = false; |
| assert!(matches!( |
| process_line( |
| r#" |
| { |
| "$message_type": "artifact", |
| "emit": "link" |
| } |
| "# |
| .to_string(), |
| /*quit_on_rmeta=*/ true, |
| ErrorFormat::Rendered, |
| &mut metadata_emitted, |
| )?, |
| LineOutput::Skip |
| )); |
| assert!(!metadata_emitted); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_process_line_emit_metadata() -> Result<(), String> { |
| let mut metadata_emitted = false; |
| assert!(matches!( |
| process_line( |
| r#" |
| { |
| "$message_type": "artifact", |
| "emit": "metadata" |
| } |
| "# |
| .to_string(), |
| /*quit_on_rmeta=*/ true, |
| ErrorFormat::Rendered, |
| &mut metadata_emitted, |
| )?, |
| LineOutput::Terminate |
| )); |
| assert!(metadata_emitted); |
| Ok(()) |
| } |
| } |