1 // Copyright 2023 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 use anyhow::{anyhow, Context as _};
16 use owo_colors::OwoColorize as _;
17 use std::{collections, env, ffi, io, io::BufRead, path, process, thread};
18 
19 pub mod cargo_workspace;
20 pub mod license_checker;
21 
run_cmd_shell( dir: &path::Path, cmd: impl AsRef<ffi::OsStr>, ) -> anyhow::Result<SuccessOutput>22 pub fn run_cmd_shell(
23     dir: &path::Path,
24     cmd: impl AsRef<ffi::OsStr>,
25 ) -> anyhow::Result<SuccessOutput> {
26     run_cmd_shell_with_color::<DefaultColors>(dir, cmd)
27 }
28 
29 /// Run a shell command using shell arg parsing.
30 ///
31 /// Removes all `*CARGO*` and `*RUSTUP*` env vars in case this was run with
32 /// `cargo run`. If they are left in, they confuse nested `cargo` invocations.
33 ///
34 /// Return Ok if the process completed normally.
run_cmd_shell_with_color<C: TermColors>( dir: &path::Path, cmd: impl AsRef<ffi::OsStr>, ) -> anyhow::Result<SuccessOutput>35 pub fn run_cmd_shell_with_color<C: TermColors>(
36     dir: &path::Path,
37     cmd: impl AsRef<ffi::OsStr>,
38 ) -> anyhow::Result<SuccessOutput> {
39     run::<C>(
40         dir,
41         process::Command::new("sh")
42             .current_dir(dir)
43             .args(["-c".as_ref(), cmd.as_ref()]),
44     )
45 }
46 
47 /// Run a cmd with explicit args directly without a shell.
48 ///
49 /// Removes all `*CARGO*` and `*RUSTUP*` env vars in case this was run with
50 /// `cargo run`.
51 ///
52 /// Return Ok if the process completed normally.
53 #[allow(dead_code)]
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>,54 pub fn run_cmd<C: TermColors, P, A, S>(
55     dir: &path::Path,
56     cmd: &P,
57     args: A,
58 ) -> anyhow::Result<SuccessOutput>
59 where
60     P: AsRef<path::Path> + ?Sized,
61     A: Clone + IntoIterator<Item = S>,
62     S: AsRef<ffi::OsStr>,
63 {
64     run::<C>(
65         dir,
66         process::Command::new(cmd.as_ref())
67             .current_dir(dir)
68             .args(args),
69     )
70 }
71 
72 /// Run the specified command.
73 ///
74 /// `cmd_with_args` is used
run<C: TermColors>( dir: &path::Path, command: &mut process::Command, ) -> Result<SuccessOutput, anyhow::Error>75 fn run<C: TermColors>(
76     dir: &path::Path,
77     command: &mut process::Command,
78 ) -> Result<SuccessOutput, anyhow::Error> {
79     // approximately human readable version of the invocation for logging
80     let cmd_with_args = command.get_args().fold(
81         command.get_program().to_os_string(),
82         |mut acc: ffi::OsString, s| {
83             acc.push(" ");
84             acc.push(shell_escape::escape(s.to_string_lossy()).as_ref());
85             acc
86         },
87     );
88 
89     let context = format!(
90         "{} [{}]",
91         cmd_with_args.to_string_lossy(),
92         dir.to_string_lossy(),
93     );
94     println!(
95         "[{}] [{}]",
96         cmd_with_args.to_string_lossy().green(),
97         dir.to_string_lossy().blue()
98     );
99 
100     let mut child = command
101         .env_clear()
102         .envs(modified_cmd_env())
103         .stdin(process::Stdio::null())
104         .stdout(process::Stdio::piped())
105         .stderr(process::Stdio::piped())
106         .spawn()
107         .context(context.clone())?;
108 
109     // If thread creation overhead becomes a problem, we could always use a shared context
110     // that holds on to some channels.
111     let stdout_thread = spawn_print_thread::<C::StdoutColor, _, _>(
112         child.stdout.take().expect("stdout must be present"),
113         io::stdout(),
114     );
115     let stderr_thread = spawn_print_thread::<C::StderrColor, _, _>(
116         child.stderr.take().expect("stderr must be present"),
117         io::stderr(),
118     );
119 
120     let status = child.wait()?;
121 
122     let stdout = stdout_thread.join().expect("stdout thread panicked");
123     stderr_thread.join().expect("stderr thread panicked");
124 
125     match status.code() {
126         None => {
127             eprintln!("Process terminated by signal");
128             Err(anyhow!("Process terminated by signal"))
129         }
130         Some(0) => Ok(SuccessOutput { stdout }),
131         Some(n) => {
132             eprintln!("Exit code: {n}");
133             Err(anyhow!("Exit code: {n}"))
134         }
135     }
136     .context(context)
137 }
138 
139 pub struct SuccessOutput {
140     stdout: String,
141 }
142 
143 impl SuccessOutput {
stdout(&self) -> &str144     pub fn stdout(&self) -> &str {
145         &self.stdout
146     }
147 }
148 
149 /// Returns modified env vars that are suitable for use in child invocations.
modified_cmd_env() -> collections::HashMap<String, String>150 fn modified_cmd_env() -> collections::HashMap<String, String> {
151     env::vars()
152         // Filter out `*CARGO*` or `*RUSTUP*` vars as those will confuse child invocations of `cargo`.
153         .filter(|(k, _)| !(k.contains("CARGO") || k.contains("RUSTUP")))
154         // We want colors in our cargo invocations
155         .chain([(String::from("CARGO_TERM_COLOR"), String::from("always"))])
156         .collect()
157 }
158 
159 /// Trait for specifying the terminal text colors of the command output.
160 pub trait TermColors {
161     /// Color for stdout. Use `owo_colors::colors::Default` to keep color codes from the command.
162     type StdoutColor: owo_colors::Color;
163     /// Color for stderr. Use `owo_colors::colors::Default` to keep color codes from the command.
164     type StderrColor: owo_colors::Color;
165 }
166 
167 /// Override only the stderr color to yellow.
168 #[non_exhaustive]
169 pub struct YellowStderr;
170 
171 impl TermColors for YellowStderr {
172     type StdoutColor = owo_colors::colors::Default;
173     type StderrColor = owo_colors::colors::Yellow;
174 }
175 
176 /// Keep the default colors from the command output. Typically used with `--color=always` or
177 /// equivalent env vars like `CARGO_TERM_COLOR` to keep the colors even though the output is not a
178 /// tty.
179 #[non_exhaustive]
180 pub struct DefaultColors;
181 impl TermColors for DefaultColors {
182     type StdoutColor = owo_colors::colors::Default;
183     type StderrColor = owo_colors::colors::Default;
184 }
185 
186 /// Spawn a thread that will print any lines read from the input using the specified color on
187 /// the provided writer (intended to be `stdin`/`stdout`.
188 ///
189 /// The thread accumulates all output lines and returns it
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,190 fn spawn_print_thread<C, R, W>(input: R, mut output: W) -> thread::JoinHandle<String>
191 where
192     C: owo_colors::Color,
193     R: io::Read + Send + 'static,
194     W: io::Write + Send + 'static,
195 {
196     thread::spawn(move || {
197         let mut line = String::new();
198         let mut all_output = String::new();
199         let mut buf_read = io::BufReader::new(input);
200 
201         loop {
202             line.clear();
203             match buf_read.read_line(&mut line) {
204                 Ok(0) => break,
205                 Ok(_) => {
206                     all_output.push_str(&line);
207                     write!(output, "{}", line.fg::<C>()).expect("write to stdio failed");
208                 }
209                 // TODO do something smarter for non-UTF8 output
210                 Err(e) => eprintln!("{}: {:?}", "Could not read line".red(), e),
211             }
212         }
213 
214         all_output
215     })
216 }
217