blob: bf6f6a1b4e92dfdd626aa548b4f78aedfc2b7af0 [file] [log] [blame]
// 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(&copy_source, &copy_dest).map_err(|e| {
ProcessWrapperError(format!(
"failed to copy {} into {}: {}",
copy_source, copy_dest, e
))
})?;
}
}
exit(code)
}