1use std::ffi::OsStr;
2use std::fmt::Write;
3use std::process::Stdio;
4use std::sync::{Condvar, Mutex};
5
6use anyhow::Context;
7use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
8
9pub fn metadata() -> anyhow::Result<cargo_metadata::Metadata> {
10 metadata_for_manifest(None)
11}
12
13pub fn metadata_for_manifest(manifest: Option<&Utf8Path>) -> anyhow::Result<cargo_metadata::Metadata> {
14 let mut cmd = cargo_metadata::MetadataCommand::new();
15 if let Some(manifest) = manifest {
16 cmd.manifest_path(manifest);
17 }
18 let output = Command::from_command(cmd.cargo_command()).output().context("exec")?;
19 if !output.status.success() {
20 anyhow::bail!("cargo metadata: {}", String::from_utf8(output.stderr)?)
21 }
22 let stdout = std::str::from_utf8(&output.stdout)?
23 .lines()
24 .find(|line| line.starts_with('{'))
25 .context("metadata has no json")?;
26
27 cargo_metadata::MetadataCommand::parse(stdout).context("parse")
28}
29
30pub fn cargo_cmd() -> Command {
31 Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()))
32}
33
34pub fn comma_delimited(features: impl IntoIterator<Item = impl AsRef<str>>) -> String {
35 let mut string = String::new();
36 for feature in features {
37 if !string.is_empty() {
38 string.push(',');
39 }
40 string.push_str(feature.as_ref());
41 }
42 string
43}
44
45pub struct Command {
46 command: std::process::Command,
47}
48
49impl Command {
50 pub fn new(arg: impl AsRef<OsStr>) -> Self {
51 Self {
52 command: std::process::Command::new(arg),
53 }
54 }
55
56 pub fn from_command(command: std::process::Command) -> Self {
57 Self { command }
58 }
59
60 pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
61 self.command.arg(arg);
62 self
63 }
64
65 pub fn args(&mut self, arg: impl IntoIterator<Item = impl AsRef<OsStr>>) -> &mut Self {
66 self.command.args(arg);
67 self
68 }
69
70 pub fn env(&mut self, key: impl AsRef<OsStr>, val: impl AsRef<OsStr>) -> &mut Self {
71 self.command.env(key, val);
72 self
73 }
74
75 pub fn stdout(&mut self, stdin: impl Into<std::process::Stdio>) -> &mut Self {
76 self.command.stdout(stdin);
77 self
78 }
79
80 pub fn stderr(&mut self, stdin: impl Into<std::process::Stdio>) -> &mut Self {
81 self.command.stderr(stdin);
82 self
83 }
84
85 pub fn spawn(&mut self) -> std::io::Result<std::process::Child> {
86 tracing::debug!("executing: {self}");
87 self.command.spawn()
88 }
89
90 pub fn status(&mut self) -> std::io::Result<std::process::ExitStatus> {
91 tracing::debug!("executing: {self}");
92 self.command.status()
93 }
94
95 pub fn output(&mut self) -> std::io::Result<std::process::Output> {
96 tracing::debug!("executing: {self}");
97 self.command.output()
98 }
99}
100
101impl std::fmt::Display for Command {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 let args = std::iter::once(self.command.get_program()).chain(self.command.get_args());
104 for (idx, arg) in args.enumerate() {
105 if idx > 0 {
106 f.write_str(" ")?;
107 }
108
109 let arg = arg.to_string_lossy();
110 let has_spaces = arg.split_whitespace().nth(1).is_some();
111 if has_spaces {
112 f.write_char('\'')?;
113 }
114 f.write_str(&arg)?;
115 if has_spaces {
116 f.write_char('\'')?;
117 }
118 }
119 Ok(())
120 }
121}
122
123pub fn git_workdir_clean() -> anyhow::Result<()> {
124 const ERROR_MESSAGE: &str = "git working directory is dirty, please commit your changes or run with --allow-dirty";
125 anyhow::ensure!(
126 Command::new("git")
127 .arg("diff")
128 .arg("--exit-code")
129 .stderr(Stdio::null())
130 .stdout(Stdio::null())
131 .output()
132 .context("git diff")?
133 .status
134 .success(),
135 ERROR_MESSAGE,
136 );
137
138 anyhow::ensure!(
139 Command::new("git")
140 .arg("diff")
141 .arg("--staged")
142 .arg("--exit-code")
143 .stderr(Stdio::null())
144 .stdout(Stdio::null())
145 .output()
146 .context("git diff")?
147 .status
148 .success(),
149 ERROR_MESSAGE,
150 );
151
152 Ok(())
153}
154
155struct Semaphore {
156 count: Mutex<usize>,
157 cvar: Condvar,
158}
159
160impl Semaphore {
161 fn new(initial: usize) -> Self {
162 Self {
163 count: Mutex::new(if initial == 0 { usize::MAX } else { initial }),
164 cvar: Condvar::new(),
165 }
166 }
167
168 fn acquire(&self) {
169 let count = self.count.lock().unwrap();
170 let mut count = self.cvar.wait_while(count, |count| *count == 0).unwrap();
171 *count -= 1;
172 }
173
174 fn release(&self) {
175 let mut count = self.count.lock().unwrap();
176 *count += 1;
177 self.cvar.notify_one();
178 }
179}
180
181pub fn concurrently<U: Send, T: Send, C: FromIterator<T>>(
182 concurrency: usize,
183 items: impl IntoIterator<Item = U>,
184 func: impl Fn(U) -> T + Send + Sync,
185) -> C {
186 let sem = Semaphore::new(concurrency);
187 std::thread::scope(|s| {
188 let items = items
189 .into_iter()
190 .map(|item| {
191 s.spawn(|| {
192 sem.acquire();
193 let r = func(item);
194 sem.release();
195 r
196 })
197 })
198 .collect::<Vec<_>>();
199 C::from_iter(items.into_iter().map(|item| item.join().unwrap()))
200 })
201}
202
203pub fn relative_to(path: &Utf8Path, dir: &Utf8Path) -> Utf8PathBuf {
204 if path.is_relative() {
206 return path.to_owned();
207 }
208
209 if let Ok(stripped) = path.strip_prefix(dir) {
211 return stripped.to_owned();
212 }
213
214 let mut result = Utf8PathBuf::new();
216
217 let mut dir_iter = dir.components();
218 let mut path_iter = path.components();
219
220 while let (Some(d), Some(p)) = (dir_iter.clone().next(), path_iter.clone().next()) {
222 if d == p {
223 dir_iter.next();
224 path_iter.next();
225 } else {
226 break;
227 }
228 }
229
230 for _ in dir_iter {
232 result.push("..");
233 }
234
235 for p in path_iter {
237 result.push(p);
238 }
239
240 result
241}
242
243pub struct DropRunner<F: FnOnce()> {
244 func: Option<F>,
245}
246
247impl<F: FnOnce()> DropRunner<F> {
248 pub fn new(func: F) -> Self {
249 Self { func: Some(func) }
250 }
251}
252
253impl<F: FnOnce()> Drop for DropRunner<F> {
254 fn drop(&mut self) {
255 if let Some(func) = self.func.take() {
256 func()
257 }
258 }
259}