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