xtask/
utils.rs

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 the path is already relative, just return it as is
205    if path.is_relative() {
206        return path.to_owned();
207    }
208
209    // Attempt to strip the prefix
210    if let Ok(stripped) = path.strip_prefix(dir) {
211        return stripped.to_owned();
212    }
213
214    // Fall back to manual computation like pathdiff does
215    let mut result = Utf8PathBuf::new();
216
217    let mut dir_iter = dir.components();
218    let mut path_iter = path.components();
219
220    // Skip common prefix components
221    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 remaining components in dir, add ".."
231    for _ in dir_iter {
232        result.push("..");
233    }
234
235    // Add remaining components from path
236    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}