// 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 license_checker; pub fn run_cmd_shell( dir: &path::Path, cmd: impl AsRef, ) -> anyhow::Result { run_cmd_shell_with_color::(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( dir: &path::Path, cmd: impl AsRef, ) -> anyhow::Result { run::( 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( dir: &path::Path, cmd: &P, args: A, ) -> anyhow::Result where P: AsRef + ?Sized, A: Clone + IntoIterator, S: AsRef, { run::( dir, process::Command::new(cmd.as_ref()) .current_dir(dir) .args(args), ) } /// Run the specified command. /// /// `cmd_with_args` is used fn run( dir: &path::Path, command: &mut process::Command, ) -> Result { // 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::( child.stdout.take().expect("stdout must be present"), io::stdout(), ); let stderr_thread = spawn_print_thread::( 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 { 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(input: R, mut output: W) -> thread::JoinHandle 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::()).expect("write to stdio failed"); } // TODO do something smarter for non-UTF8 output Err(e) => eprintln!("{}: {:?}", "Could not read line".red(), e), } } all_output }) }