| // Copyright 2023 Google LLC | 
 | // | 
 | // 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. | 
 |  | 
 | use anyhow::{anyhow, Context as _}; | 
 | use owo_colors::OwoColorize as _; | 
 | use std::{collections, env, ffi, io, io::BufRead, path, process, thread}; | 
 |  | 
 | pub mod cargo_workspace; | 
 | pub mod fuzzers; | 
 | pub mod license_checker; | 
 |  | 
 | pub fn run_cmd_shell( | 
 |     dir: &path::Path, | 
 |     cmd: impl AsRef<ffi::OsStr>, | 
 | ) -> anyhow::Result<SuccessOutput> { | 
 |     run_cmd_shell_with_color::<DefaultColors>(dir, cmd) | 
 | } | 
 |  | 
 | /// Run a shell command using shell arg parsing. | 
 | /// | 
 | /// Removes all `*CARGO*` and `*RUSTUP*` env vars in case this was run with | 
 | /// `cargo run`. If they are left in, they confuse nested `cargo` invocations. | 
 | /// | 
 | /// Return Ok if the process completed normally. | 
 | pub fn run_cmd_shell_with_color<C: TermColors>( | 
 |     dir: &path::Path, | 
 |     cmd: impl AsRef<ffi::OsStr>, | 
 | ) -> anyhow::Result<SuccessOutput> { | 
 |     run::<C>( | 
 |         dir, | 
 |         process::Command::new("sh") | 
 |             .current_dir(dir) | 
 |             .args(["-c".as_ref(), cmd.as_ref()]), | 
 |     ) | 
 | } | 
 |  | 
 | /// Run a cmd with explicit args directly without a shell. | 
 | /// | 
 | /// Removes all `*CARGO*` and `*RUSTUP*` env vars in case this was run with | 
 | /// `cargo run`. | 
 | /// | 
 | /// Return Ok if the process completed normally. | 
 | #[allow(dead_code)] | 
 | pub fn run_cmd<C: TermColors, P, A, S>( | 
 |     dir: &path::Path, | 
 |     cmd: &P, | 
 |     args: A, | 
 | ) -> anyhow::Result<SuccessOutput> | 
 | where | 
 |     P: AsRef<path::Path> + ?Sized, | 
 |     A: Clone + IntoIterator<Item = S>, | 
 |     S: AsRef<ffi::OsStr>, | 
 | { | 
 |     run::<C>( | 
 |         dir, | 
 |         process::Command::new(cmd.as_ref()) | 
 |             .current_dir(dir) | 
 |             .args(args), | 
 |     ) | 
 | } | 
 |  | 
 | /// Run the specified command. | 
 | /// | 
 | /// `cmd_with_args` is used | 
 | fn run<C: TermColors>( | 
 |     dir: &path::Path, | 
 |     command: &mut process::Command, | 
 | ) -> Result<SuccessOutput, anyhow::Error> { | 
 |     // approximately human readable version of the invocation for logging | 
 |     let cmd_with_args = command.get_args().fold( | 
 |         command.get_program().to_os_string(), | 
 |         |mut acc: ffi::OsString, s| { | 
 |             acc.push(" "); | 
 |             acc.push(shell_escape::escape(s.to_string_lossy()).as_ref()); | 
 |             acc | 
 |         }, | 
 |     ); | 
 |  | 
 |     let context = format!( | 
 |         "{} [{}]", | 
 |         cmd_with_args.to_string_lossy(), | 
 |         dir.to_string_lossy(), | 
 |     ); | 
 |     println!( | 
 |         "[{}] [{}]", | 
 |         cmd_with_args.to_string_lossy().green(), | 
 |         dir.to_string_lossy().blue() | 
 |     ); | 
 |  | 
 |     let mut child = command | 
 |         .env_clear() | 
 |         .envs(modified_cmd_env()) | 
 |         .stdin(process::Stdio::null()) | 
 |         .stdout(process::Stdio::piped()) | 
 |         .stderr(process::Stdio::piped()) | 
 |         .spawn() | 
 |         .context(context.clone())?; | 
 |  | 
 |     // If thread creation overhead becomes a problem, we could always use a shared context | 
 |     // that holds on to some channels. | 
 |     let stdout_thread = spawn_print_thread::<C::StdoutColor, _, _>( | 
 |         child.stdout.take().expect("stdout must be present"), | 
 |         io::stdout(), | 
 |     ); | 
 |     let stderr_thread = spawn_print_thread::<C::StderrColor, _, _>( | 
 |         child.stderr.take().expect("stderr must be present"), | 
 |         io::stderr(), | 
 |     ); | 
 |  | 
 |     let status = child.wait()?; | 
 |  | 
 |     let stdout = stdout_thread.join().expect("stdout thread panicked"); | 
 |     stderr_thread.join().expect("stderr thread panicked"); | 
 |  | 
 |     match status.code() { | 
 |         None => { | 
 |             eprintln!("Process terminated by signal"); | 
 |             Err(anyhow!("Process terminated by signal")) | 
 |         } | 
 |         Some(0) => Ok(SuccessOutput { stdout }), | 
 |         Some(n) => { | 
 |             eprintln!("Exit code: {n}"); | 
 |             Err(anyhow!("Exit code: {n}")) | 
 |         } | 
 |     } | 
 |     .context(context) | 
 | } | 
 |  | 
 | pub struct SuccessOutput { | 
 |     stdout: String, | 
 | } | 
 |  | 
 | impl SuccessOutput { | 
 |     pub fn stdout(&self) -> &str { | 
 |         &self.stdout | 
 |     } | 
 | } | 
 |  | 
 | /// Returns modified env vars that are suitable for use in child invocations. | 
 | fn modified_cmd_env() -> collections::HashMap<String, String> { | 
 |     env::vars() | 
 |         // Filter out `*CARGO*` or `*RUSTUP*` vars as those will confuse child invocations of `cargo`. | 
 |         .filter(|(k, _)| !(k.contains("CARGO") || k.contains("RUSTUP"))) | 
 |         // We want colors in our cargo invocations | 
 |         .chain([(String::from("CARGO_TERM_COLOR"), String::from("always"))]) | 
 |         .collect() | 
 | } | 
 |  | 
 | /// Trait for specifying the terminal text colors of the command output. | 
 | pub trait TermColors { | 
 |     /// Color for stdout. Use `owo_colors::colors::Default` to keep color codes from the command. | 
 |     type StdoutColor: owo_colors::Color; | 
 |     /// Color for stderr. Use `owo_colors::colors::Default` to keep color codes from the command. | 
 |     type StderrColor: owo_colors::Color; | 
 | } | 
 |  | 
 | /// Override only the stderr color to yellow. | 
 | #[non_exhaustive] | 
 | pub struct YellowStderr; | 
 |  | 
 | impl TermColors for YellowStderr { | 
 |     type StdoutColor = owo_colors::colors::Default; | 
 |     type StderrColor = owo_colors::colors::Yellow; | 
 | } | 
 |  | 
 | /// Keep the default colors from the command output. Typically used with `--color=always` or | 
 | /// equivalent env vars like `CARGO_TERM_COLOR` to keep the colors even though the output is not a | 
 | /// tty. | 
 | #[non_exhaustive] | 
 | pub struct DefaultColors; | 
 | impl TermColors for DefaultColors { | 
 |     type StdoutColor = owo_colors::colors::Default; | 
 |     type StderrColor = owo_colors::colors::Default; | 
 | } | 
 |  | 
 | /// Spawn a thread that will print any lines read from the input using the specified color on | 
 | /// the provided writer (intended to be `stdin`/`stdout`. | 
 | /// | 
 | /// The thread accumulates all output lines and returns it | 
 | fn spawn_print_thread<C, R, W>(input: R, mut output: W) -> thread::JoinHandle<String> | 
 | where | 
 |     C: owo_colors::Color, | 
 |     R: io::Read + Send + 'static, | 
 |     W: io::Write + Send + 'static, | 
 | { | 
 |     thread::spawn(move || { | 
 |         let mut line = String::new(); | 
 |         let mut all_output = String::new(); | 
 |         let mut buf_read = io::BufReader::new(input); | 
 |  | 
 |         loop { | 
 |             line.clear(); | 
 |             match buf_read.read_line(&mut line) { | 
 |                 Ok(0) => break, | 
 |                 Ok(_) => { | 
 |                     all_output.push_str(&line); | 
 |                     write!(output, "{}", line.fg::<C>()).expect("write to stdio failed"); | 
 |                 } | 
 |                 // TODO do something smarter for non-UTF8 output | 
 |                 Err(e) => eprintln!("{}: {:?}", "Could not read line".red(), e), | 
 |             } | 
 |         } | 
 |  | 
 |         all_output | 
 |     }) | 
 | } |