1use std::{
7 ffi::OsStr,
8 ops::{Deref, DerefMut},
9 path::Path,
10};
11
12use anyhow::Context;
13use anyhow::bail;
14use colored::Colorize;
15
16pub struct Command {
21 inner: std::process::Command,
22 value_replace: Box<dyn Fn(&OsStr) -> String>,
23}
24
25impl Deref for Command {
26 type Target = std::process::Command;
27
28 fn deref(&self) -> &Self::Target {
29 &self.inner
30 }
31}
32
33impl DerefMut for Command {
34 fn deref_mut(&mut self) -> &mut Self::Target {
35 &mut self.inner
36 }
37}
38
39impl Command {
40 pub fn new<S>(
48 program: S,
49 workdir: &Path,
50 value_replace: impl Fn(&OsStr) -> String + 'static,
51 ) -> Command
52 where
53 S: AsRef<OsStr>,
54 {
55 let mut cmd = std::process::Command::new(program);
56 cmd.current_dir(workdir);
57 cmd.env("WORKSPACE_FOLDER", workdir.display().to_string());
58
59 Self {
60 inner: cmd,
61 value_replace: Box::new(value_replace),
62 }
63 }
64
65 pub fn print_cmd(&self) {
67 let program = self.get_program().to_string_lossy();
68 let mut cmd_str = program.into_owned();
69 for arg in self.get_args() {
70 cmd_str.push(' ');
71 cmd_str.push_str(arg.to_string_lossy().as_ref());
72 }
73
74 println!("{}", cmd_str.purple().bold());
75 }
76
77 pub fn into_std(self) -> std::process::Command {
78 self.inner
79 }
80
81 pub fn run(&mut self) -> anyhow::Result<()> {
87 self.print_cmd();
88 let status = self.status()?;
89 if !status.success() {
90 bail!("failed with status: {status}");
91 }
92 Ok(())
93 }
94
95 pub fn arg<S>(&mut self, arg: S) -> &mut Command
97 where
98 S: AsRef<OsStr>,
99 {
100 let value = (self.value_replace)(arg.as_ref());
101 self.inner.arg(value);
102 self
103 }
104
105 pub fn args<I, S>(&mut self, args: I) -> &mut Command
107 where
108 I: IntoIterator<Item = S>,
109 S: AsRef<OsStr>,
110 {
111 for arg in args {
112 self.arg(arg.as_ref());
113 }
114 self
115 }
116
117 pub fn env<K, V>(&mut self, key: K, val: V) -> &mut Command
119 where
120 K: AsRef<OsStr>,
121 V: AsRef<OsStr>,
122 {
123 let value = (self.value_replace)(val.as_ref());
124 self.inner.env(key, value);
125 self
126 }
127}
128
129pub trait PathResultExt<T> {
131 fn with_path<P>(self, action: &'static str, path: P) -> anyhow::Result<T>
133 where
134 P: AsRef<Path>;
135}
136
137impl<T, E> PathResultExt<T> for Result<T, E>
138where
139 E: std::error::Error + Send + Sync + 'static,
140{
141 fn with_path<P>(self, action: &'static str, path: P) -> anyhow::Result<T>
142 where
143 P: AsRef<Path>,
144 {
145 let path = path.as_ref().to_path_buf();
146 self.with_context(move || format!("{action}: {}", path.display()))
147 }
148}
149
150pub fn replace_env_placeholders(input: &str) -> anyhow::Result<String> {
166 replace_placeholders(input, |placeholder| {
167 if let Some(env_var_name) = placeholder.strip_prefix("env:") {
168 return Ok(Some(std::env::var(env_var_name).unwrap_or_default()));
169 }
170
171 Ok(None)
172 })
173}
174
175pub fn replace_placeholders<F>(input: &str, mut resolver: F) -> anyhow::Result<String>
181where
182 F: FnMut(&str) -> anyhow::Result<Option<String>>,
183{
184 let mut result = String::new();
185 let mut chars = input.chars().peekable();
186
187 while let Some(ch) = chars.next() {
188 if ch == '$' && chars.peek() == Some(&'{') {
189 chars.next(); let mut placeholder = String::new();
192 let mut brace_count = 1;
193 let mut found_closing_brace = false;
194
195 for ch in chars.by_ref() {
197 if ch == '{' {
198 brace_count += 1;
199 placeholder.push(ch);
200 } else if ch == '}' {
201 brace_count -= 1;
202 if brace_count == 0 {
203 found_closing_brace = true;
204 break;
205 } else {
206 placeholder.push(ch);
207 }
208 } else {
209 placeholder.push(ch);
210 }
211 }
212
213 if found_closing_brace {
214 if let Some(value) = resolver(&placeholder)? {
215 result.push_str(&value);
216 } else {
217 result.push_str("${");
218 result.push_str(&placeholder);
219 result.push('}');
220 }
221 } else {
222 result.push_str("${");
223 result.push_str(&placeholder);
224 }
225 } else {
226 result.push(ch);
227 }
228 }
229
230 Ok(result)
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use std::env;
237
238 #[test]
239 fn test_replace_placeholders_supports_custom_variables() {
240 unsafe {
241 env::set_var("OSTOOL_TEST_CUSTOM_ENV", "env-value");
242 }
243
244 let result = replace_placeholders(
245 "workspace=${workspace}, package=${package}, env=${env:OSTOOL_TEST_CUSTOM_ENV}",
246 |placeholder| {
247 Ok(match placeholder {
248 "workspace" => Some("/tmp/workspace".into()),
249 "package" => Some("/tmp/workspace/kernel".into()),
250 p if p.starts_with("env:") => Some(env::var(&p[4..]).unwrap_or_default()),
251 _ => None,
252 })
253 },
254 )
255 .unwrap();
256
257 assert_eq!(
258 result,
259 "workspace=/tmp/workspace, package=/tmp/workspace/kernel, env=env-value"
260 );
261 }
262
263 #[test]
264 fn test_replace_env_placeholders() {
265 unsafe {
267 env::set_var("OSTOOL_TEST_HOME_REPLACE", "/home/test");
268 env::set_var("OSTOOL_TEST_PATH_REPLACE", "/usr/local/bin");
269 }
270
271 assert_eq!(
273 replace_env_placeholders("${env:OSTOOL_TEST_HOME_REPLACE}").unwrap(),
274 "/home/test"
275 );
276
277 assert_eq!(
279 replace_env_placeholders(
280 "${env:OSTOOL_TEST_HOME_REPLACE}:${env:OSTOOL_TEST_PATH_REPLACE}"
281 )
282 .unwrap(),
283 "/home/test:/usr/local/bin"
284 );
285
286 let result = replace_env_placeholders("${env:NON_EXISTENT}");
288 assert!(result.is_ok());
289 assert_eq!(result.unwrap(), "");
290
291 assert_eq!(
293 replace_env_placeholders("Path: ${env:OSTOOL_TEST_HOME_REPLACE}/bin").unwrap(),
294 "Path: /home/test/bin"
295 );
296
297 assert_eq!(
299 replace_env_placeholders("${not_env:placeholder}").unwrap(),
300 "${not_env:placeholder}"
301 );
302
303 assert_eq!(
305 replace_env_placeholders("Just a normal string").unwrap(),
306 "Just a normal string"
307 );
308
309 assert_eq!(replace_env_placeholders("").unwrap(), "");
311 }
312
313 #[test]
314 fn test_nested_braces() {
315 unsafe {
316 env::set_var("OSTOOL_TEST_VAR_NESTED", "value");
317 }
318
319 assert_eq!(
321 replace_env_placeholders("${env:OSTOOL_TEST_VAR_NESTED} and ${other:placeholder}")
322 .unwrap(),
323 "value and ${other:placeholder}"
324 );
325 }
326
327 #[test]
328 fn test_replace_placeholders_keeps_unknown_and_legacy_placeholders() {
329 let result = replace_placeholders(
330 "${workspaceFolder}:${unknown}:${workspace}",
331 |placeholder| {
332 Ok(match placeholder {
333 "workspaceFolder" => Some("/legacy".into()),
334 "workspace" => Some("/modern".into()),
335 _ => None,
336 })
337 },
338 )
339 .unwrap();
340
341 assert_eq!(result, "/legacy:${unknown}:/modern");
342 }
343
344 #[test]
345 fn test_real_env_vars() {
346 if let Ok(home) = env::var("HOME") {
348 assert_eq!(replace_env_placeholders("${env:HOME}").unwrap(), home);
349 }
350 }
351
352 #[test]
353 fn test_edge_cases() {
354 assert_eq!(replace_env_placeholders("${").unwrap(), "${");
356 assert_eq!(replace_env_placeholders("${env").unwrap(), "${env");
357 assert_eq!(replace_env_placeholders("${env:").unwrap(), "${env:");
358 assert_eq!(replace_env_placeholders("${env:VAR").unwrap(), "${env:VAR");
359
360 let result = replace_env_placeholders("${env:}");
362 assert!(result.is_ok());
363 assert_eq!(result.unwrap(), "");
364
365 assert_eq!(replace_env_placeholders("$").unwrap(), "$");
367 assert_eq!(replace_env_placeholders("$$").unwrap(), "$$");
368
369 unsafe {
371 env::set_var("OSTOOL-TEST-VAR-EDGE", "dash-value");
372 env::set_var("OSTOOL_TEST_VAR_EDGE", "underscore-value");
373 }
374 assert_eq!(
375 replace_env_placeholders("${env:OSTOOL-TEST-VAR-EDGE}").unwrap(),
376 "dash-value"
377 );
378 assert_eq!(
379 replace_env_placeholders("${env:OSTOOL_TEST_VAR_EDGE}").unwrap(),
380 "underscore-value"
381 );
382
383 unsafe {
385 env::set_var("OSTOOL_EMPTY_VAR_EDGE", "");
386 }
387 assert_eq!(
388 replace_env_placeholders("${env:OSTOOL_EMPTY_VAR_EDGE}").unwrap(),
389 ""
390 );
391 }
392
393 #[test]
394 fn test_malformed_placeholders() {
395 assert_eq!(replace_env_placeholders("${env:VAR").unwrap(), "${env:VAR");
397 assert_eq!(replace_env_placeholders("${env}").unwrap(), "${env}");
398 assert_eq!(replace_env_placeholders("${:VAR}").unwrap(), "${:VAR}");
399
400 unsafe {
402 env::set_var("OSTOOL_VAR_MALFORMED", "value");
403 }
404
405 assert_eq!(
408 replace_env_placeholders("${env:OSTOOL_VAR_MALFORMED}}").unwrap(),
409 "value}"
410 );
411
412 assert_eq!(replace_env_placeholders("{env:VAR}").unwrap(), "{env:VAR}");
414 assert_eq!(replace_env_placeholders("$env:VAR}").unwrap(), "$env:VAR}");
415 }
416}