assemble_std/specs/
exec_spec.rs

1//! The exec spec helps with defining executables
2
3use assemble_core::exception::BuildException;
4use assemble_core::logging::{Origin, LOGGING_CONTROL};
5use assemble_core::prelude::{ProjectError, ProjectResult};
6use assemble_core::project::VisitProject;
7use assemble_core::{BuildResult, Project};
8use log::Level;
9use std::collections::HashMap;
10use std::ffi::{OsStr, OsString};
11use std::fs::File;
12use std::io::{BufWriter, ErrorKind, Read, Write};
13use std::path::{Path, PathBuf};
14use std::process::{Child, Command, ExitStatus, Stdio};
15use std::str::Bytes;
16use std::string::FromUtf8Error;
17use std::sync::{Arc, RwLock};
18use std::thread::JoinHandle;
19use std::{io, thread};
20use assemble_core::error::PayloadError;
21
22/// Input for exec
23#[derive(Debug, Default, Clone)]
24pub enum Input {
25    /// No input
26    #[default]
27    Null,
28    /// Get input bytes from a file
29    File(PathBuf),
30    /// Get input bytes from a byte vector
31    Bytes(Vec<u8>),
32}
33
34impl From<&[u8]> for Input {
35    fn from(b: &[u8]) -> Self {
36        Self::Bytes(b.to_vec())
37    }
38}
39
40impl From<Vec<u8>> for Input {
41    fn from(c: Vec<u8>) -> Self {
42        Self::Bytes(c)
43    }
44}
45
46impl<'a> From<Bytes<'a>> for Input {
47    fn from(b: Bytes<'a>) -> Self {
48        Self::Bytes(b.collect())
49    }
50}
51
52impl From<String> for Input {
53    fn from(str: String) -> Self {
54        Self::from(str.bytes())
55    }
56}
57
58impl From<&str> for Input {
59    fn from(str: &str) -> Self {
60        Self::from(str.bytes())
61    }
62}
63
64impl From<&Path> for Input {
65    fn from(p: &Path) -> Self {
66        Self::File(p.to_path_buf())
67    }
68}
69
70impl From<PathBuf> for Input {
71    fn from(file: PathBuf) -> Self {
72        Self::File(file)
73    }
74}
75
76/// Output types for exec
77#[derive(Debug, Clone)]
78pub enum Output {
79    /// Throw the output away
80    Null,
81    /// Stream the output into a file
82    ///
83    /// If append is true, then a new file isn't created if one at the path
84    /// already exists. and text is appended. Otherwise a new file
85    /// is created, replacing any old file.
86    File {
87        /// The path of the file to emit output to
88        path: PathBuf,
89        /// whether to append to the file or not
90        append: bool,
91    },
92    /// Stream the output into the logger at a given level
93    Log(#[doc("The log level to emit output to")] Level),
94    /// Stream the output into a byte vector
95    Bytes,
96}
97
98impl Output {
99    /// Create a new output with a file as the target
100    pub fn new<P: AsRef<Path>>(path: P, append: bool) -> Self {
101        Self::File {
102            path: path.as_ref().to_path_buf(),
103            append,
104        }
105    }
106}
107
108impl From<Level> for Output {
109    fn from(lvl: Level) -> Self {
110        Output::Log(lvl)
111    }
112}
113
114impl From<&Path> for Output {
115    fn from(path: &Path) -> Self {
116        Self::File {
117            path: path.to_path_buf(),
118            append: false,
119        }
120    }
121}
122
123impl From<PathBuf> for Output {
124    fn from(path: PathBuf) -> Self {
125        Self::File {
126            path,
127            append: false,
128        }
129    }
130}
131
132impl Default for Output {
133    fn default() -> Self {
134        Self::Log(Level::Info)
135    }
136}
137
138/// The exec spec helps define something to execute by the project
139#[derive(Debug, Default, Clone)]
140pub struct ExecSpec {
141    /// The working directory to run the executable in
142    pub working_dir: PathBuf,
143    /// The executable
144    pub executable: OsString,
145    /// The command line args for the executable
146    pub clargs: Vec<OsString>,
147    /// The environment variables for the executable.
148    ///
149    /// # Warning
150    /// **ONLY** the environment variables in this map will be passed to the executable.
151    pub env: HashMap<String, String>,
152    /// The input to the program, if needed
153    pub input: Input,
154    /// Where the program's stdout is emitted
155    pub output: Output,
156    /// Where the program's stderr is emitted
157    pub output_err: Output,
158}
159
160impl ExecSpec {
161    /// The working directory of the exec spec. If the path is relative, then the relative
162    /// path is calculated relative to the the base directory of a project.
163    pub fn working_dir(&self) -> &Path {
164        &self.working_dir
165    }
166    /// The executable to run
167    pub fn executable(&self) -> &OsStr {
168        &self.executable
169    }
170
171    /// Command line args for the exec spec
172    pub fn args(&self) -> &[OsString] {
173        &self.clargs[..]
174    }
175
176    /// The environment variables for the exec spec
177    pub fn env(&self) -> &HashMap<String, String> {
178        &self.env
179    }
180
181    /// Try to executes an exec-spec, using the given path to resolve the current directory. If creating the program is successful, returns an
182    /// [`ExecSpecHandle`](ExecSpecHandle). This is a non-blocking method, as the actual
183    /// command is ran in a separate thread.
184    ///
185    /// Execution of the spec begins as soon as this method is called. However, all
186    /// scheduling is controlled by the OS.
187    ///
188    /// # Error
189    /// This method will return an error if the given path can not be canonicalized into an
190    /// absolute path, or the executable specified by this spec does not exist.
191    pub fn execute_spec<P>(self, path: P) -> ProjectResult<ExecHandle>
192    where
193        P: AsRef<Path>,
194    {
195        let path = path.as_ref();
196        let working_dir = self.resolve_working_dir(path);
197        let origin = LOGGING_CONTROL.get_origin();
198        ExecHandle::create(self, &working_dir, origin)
199    }
200
201    /// Resolve a working directory
202    fn resolve_working_dir(&self, path: &Path) -> PathBuf {
203        if self.working_dir().is_absolute() {
204            self.working_dir.to_path_buf()
205        } else {
206            path.join(&self.working_dir)
207        }
208    }
209
210    #[doc(hidden)]
211    #[deprecated]
212    pub(crate) fn execute(&mut self, _path: impl AsRef<Path>) -> io::Result<&Child> {
213        panic!("unimplemented")
214    }
215
216    /// Waits for the running child process to finish. Will return [`Some(exit_status)`](Some) only
217    /// if a child process has already been started. Otherwise, a [`None`](None) result will be given
218    #[deprecated]
219    pub fn finish(&mut self) -> io::Result<ExitStatus> {
220        panic!("unimplemented")
221    }
222}
223
224impl VisitProject<Result<(), io::Error>> for ExecSpec {
225    /// Executes the exec spec in the project.
226    fn visit(&mut self, project: &Project) -> Result<(), io::Error> {
227        self.execute(project.project_dir()).map(|_| ())
228    }
229}
230
231/// Builds exec specs
232pub struct ExecSpecBuilder {
233    /// The working directory to run the executable in
234    pub working_dir: Option<PathBuf>,
235    /// The executable
236    pub executable: Option<OsString>,
237    /// The command line args for the executable
238    pub clargs: Vec<OsString>,
239    /// The environment variables for the executable. By default, the exec spec will
240    /// inherit from the parent process.
241    ///
242    /// # Warning
243    /// **ONLY** The environment variables in this map will be passed to the executable.
244    pub env: HashMap<String, String>,
245    /// The stdin for the program. null by default.
246    stdin: Input,
247    output: Output,
248    output_err: Output,
249}
250
251/// An exec spec configuration error
252#[derive(Debug, thiserror::Error)]
253#[error("{}", error)]
254pub struct ExecSpecBuilderError {
255    error: String,
256}
257
258impl From<&str> for ExecSpecBuilderError {
259    fn from(s: &str) -> Self {
260        Self {
261            error: s.to_string(),
262        }
263    }
264}
265
266impl ExecSpecBuilder {
267    /// Create a new [ExecSpecBuilder](Self).
268    pub fn new() -> Self {
269        Self {
270            working_dir: Some(PathBuf::new()),
271            executable: None,
272            clargs: vec![],
273            env: Self::default_env(),
274            stdin: Input::default(),
275            output: Output::default(),
276            output_err: Output::Log(Level::Warn),
277        }
278    }
279
280    /// The default environment variables
281    pub fn default_env() -> HashMap<String, String> {
282        std::env::vars().into_iter().collect()
283    }
284
285    /// Changes the environment variables to the contents of this map.
286    ///
287    /// # Warning
288    /// This will clear all previously set values in the environment map
289    pub fn with_env<I: IntoIterator<Item = (String, String)>>(&mut self, env: I) -> &mut Self {
290        self.env = env.into_iter().collect();
291        self
292    }
293
294    /// Adds variables to the environment
295    pub fn extend_env<I: IntoIterator<Item = (String, String)>>(&mut self, env: I) -> &mut Self {
296        self.env.extend(env);
297        self
298    }
299
300    /// Adds variables to the environment
301    pub fn add_env<'a>(&mut self, env: &str, value: impl Into<Option<&'a str>>) -> &mut Self {
302        self.env
303            .insert(env.to_string(), value.into().unwrap_or("").to_string());
304        self
305    }
306
307    /// Add an arg to the command
308    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
309        self.clargs.push(arg.as_ref().to_os_string());
310        self
311    }
312
313    /// Add many args to the command
314    pub fn args<I, S: AsRef<OsStr>>(&mut self, args: I) -> &mut Self
315    where
316        I: IntoIterator<Item = S>,
317    {
318        self.clargs
319            .extend(args.into_iter().map(|s| s.as_ref().to_os_string()));
320        self
321    }
322
323    /// Add an arg to the command
324    pub fn with_arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
325        self.arg(arg);
326        self
327    }
328
329    /// Add many args to the command
330    pub fn with_args<I, S: AsRef<OsStr>>(mut self, args: I) -> Self
331    where
332        I: IntoIterator<Item = S>,
333    {
334        self.args(args);
335        self
336    }
337
338    /// Set the executable for the exec spec
339    pub fn exec<E: AsRef<OsStr>>(&mut self, exec: E) -> &mut Self {
340        self.executable = Some(exec.as_ref().to_os_string());
341        self
342    }
343
344    /// Set the executable for the exec spec
345    pub fn with_exec<E: AsRef<OsStr>>(mut self, exec: E) -> Self {
346        self.exec(exec);
347        self
348    }
349
350    /// Set the working directory for the exec spec. If the path is relative, it will be
351    /// resolved to the project directory.
352    pub fn working_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
353        self.working_dir = Some(path.as_ref().to_path_buf());
354        self
355    }
356
357    /// Set the standard input for the executable. doesn't need to be set
358    pub fn stdin<In>(&mut self, input: In) -> &mut Self
359    where
360        In: Into<Input>,
361    {
362        let input = input.into();
363        self.stdin = input;
364        self
365    }
366
367    /// Set the standard input for the executable. doesn't need to be set
368    pub fn with_stdin<In>(mut self, input: In) -> Self
369    where
370        In: Into<Input>,
371    {
372        self.stdin(input);
373        self
374    }
375
376    /// Sets the output type for this exec spec
377    pub fn stdout<O>(&mut self, output: O) -> &mut Self
378    where
379        O: Into<Output>,
380    {
381        self.output = output.into();
382        self
383    }
384
385    /// Sets the output type for this exec spec
386    pub fn with_stdout<O>(mut self, output: O) -> Self
387    where
388        O: Into<Output>,
389    {
390        self.stdout(output);
391        self
392    }
393
394    /// Sets the output type for this exec spec
395    pub fn stderr<O>(&mut self, output: O) -> &mut Self
396    where
397        O: Into<Output>,
398    {
399        self.output_err = output.into();
400        self
401    }
402
403    /// Sets the output type for this exec spec
404    pub fn with_stderr<O>(mut self, output: O) -> Self
405    where
406        O: Into<Output>,
407    {
408        self.stderr(output);
409        self
410    }
411
412    /// Build the exec spec from the builder
413    ///
414    /// # Error
415    /// Will return an error if the working directory or the executable isn't set.
416    pub fn build(self) -> Result<ExecSpec, ExecSpecBuilderError> {
417        Ok(ExecSpec {
418            working_dir: self
419                .working_dir
420                .ok_or(ExecSpecBuilderError::from("Working directory not set"))?,
421            executable: self
422                .executable
423                .ok_or(ExecSpecBuilderError::from("Executable not set"))?,
424            clargs: self.clargs,
425            env: self.env,
426            input: self.stdin,
427            output: self.output,
428            output_err: self.output_err,
429        })
430    }
431}
432
433/// A handle into an exec spec. Can be queried to get output.
434pub struct ExecHandle {
435    spec: ExecSpec,
436    output: Arc<RwLock<ExecSpecOutputHandle>>,
437    handle: JoinHandle<io::Result<ExitStatus>>,
438}
439
440impl ExecHandle {
441    fn create(spec: ExecSpec, working_dir: &Path, origin: Origin) -> ProjectResult<Self> {
442        let mut command = Command::new(&spec.executable);
443        command.current_dir(working_dir).env_clear().envs(&spec.env);
444        command.args(spec.args());
445
446        let input = match &spec.input {
447            Input::Null => Stdio::null(),
448            Input::File(file) => {
449                let file = File::open(file)?;
450                Stdio::from(file)
451            }
452            Input::Bytes(b) => {
453                let mut file = tempfile::tempfile()?;
454                file.write_all(&b[..])?;
455                Stdio::from(file)
456            }
457        };
458        command.stdin(input);
459        command.stdout(Stdio::piped());
460        command.stderr(Stdio::piped());
461
462        let realized_output = RealizedOutput::try_from(spec.output.clone())?;
463        let realized_output_err = RealizedOutput::try_from(spec.output.clone())?;
464
465        let output_handle = Arc::new(RwLock::new(ExecSpecOutputHandle {
466            origin,
467            realized_output: Arc::new(RwLock::new(BufWriter::new(realized_output))),
468            realized_output_err: Arc::new(RwLock::new(BufWriter::new(realized_output_err))),
469        }));
470
471        let join_handle = execute(command, &output_handle)?;
472
473        Ok(Self {
474            spec,
475            output: output_handle,
476            handle: join_handle,
477        })
478    }
479
480    /// Wait for the exec spec handle to finish
481    pub fn wait(self) -> ProjectResult<ExecResult> {
482        let result = self
483            .handle
484            .join()
485            .map_err(|_| ProjectError::custom("Couldn't join thread"))??;
486        let output = self.output.read().map_err(PayloadError::new)?;
487        let bytes = output.bytes();
488        let bytes_err = output.bytes_err();
489        Ok(ExecResult {
490            code: result,
491            bytes,
492            bytes_err,
493        })
494    }
495}
496
497fn execute(
498    mut command: Command,
499    output: &Arc<RwLock<ExecSpecOutputHandle>>,
500) -> ProjectResult<JoinHandle<io::Result<ExitStatus>>> {
501    trace!("attempting to execute command: {:?}", command);
502    trace!("working_dir: {:?}", command.get_current_dir());
503    trace!(
504        "env: {:#?}",
505        command
506            .get_envs()
507            .into_iter()
508            .map(|(key, val): (&OsStr, Option<&OsStr>)| ((
509                key.to_string_lossy().to_string(),
510                val.map(|v| v.to_string_lossy().to_string())
511                    .unwrap_or_default()
512            )))
513            .collect::<HashMap<_, _>>()
514    );
515
516    let spawned = command.spawn()?;
517    let output = output.clone();
518    Ok(thread::spawn(move || {
519        let mut spawned = spawned;
520        let output = output;
521        let origin = output.read().unwrap().origin.clone();
522
523        let output_handle = output.write().expect("couldn't get output");
524
525        thread::scope(|scope| {
526            let mut stdout = spawned.stdout.take().unwrap();
527            let mut stderr = spawned.stderr.take().unwrap();
528
529            let output = output_handle.realized_output.clone();
530            let output_err = output_handle.realized_output_err.clone();
531
532            let origin1 = origin.clone();
533            let out_join = scope.spawn(move || -> io::Result<u64> {
534                LOGGING_CONTROL.with_origin(origin1, || {
535                    let mut output = output.write().expect("couldnt get output");
536                    io::copy(&mut stdout, &mut *output)
537                })
538            });
539            let err_join = scope.spawn(move || -> io::Result<u64> {
540                LOGGING_CONTROL.with_origin(origin, || {
541                    let mut output = output_err.write().expect("couldnt get output");
542                    io::copy(&mut stderr, &mut *output)
543                })
544            });
545
546            let out = spawned.wait()?;
547            out_join.join().map_err(|_| {
548                io::Error::new(ErrorKind::Interrupted, "emitting to output failed")
549            })??;
550            err_join.join().map_err(|_| {
551                io::Error::new(ErrorKind::Interrupted, "emitting to error failed")
552            })??;
553            Ok(out)
554        })
555    }))
556}
557
558struct ExecSpecOutputHandle {
559    origin: Origin,
560    realized_output: Arc<RwLock<BufWriter<RealizedOutput>>>,
561    realized_output_err: Arc<RwLock<BufWriter<RealizedOutput>>>,
562}
563
564impl ExecSpecOutputHandle {
565    /// Gets the bytes output if output mode is byte vector
566    pub fn bytes(&self) -> Option<Vec<u8>> {
567        if let RealizedOutput::Bytes(vec) = self.realized_output.read().unwrap().get_ref() {
568            Some(vec.clone())
569        } else {
570            None
571        }
572    }
573
574    /// Gets the bytes output if output mode is byte vector
575    pub fn bytes_err(&self) -> Option<Vec<u8>> {
576        if let RealizedOutput::Bytes(vec) = self.realized_output_err.read().unwrap().get_ref() {
577            Some(vec.clone())
578        } else {
579            None
580        }
581    }
582}
583
584impl Write for ExecSpecOutputHandle {
585    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
586        LOGGING_CONTROL.with_origin(self.origin.clone(), || {
587            self.realized_output.write().unwrap().write(buf)
588        })
589    }
590
591    fn flush(&mut self) -> io::Result<()> {
592        LOGGING_CONTROL.with_origin(self.origin.clone(), || {
593            self.realized_output.write().unwrap().flush()
594        })
595    }
596}
597
598impl TryFrom<Output> for RealizedOutput {
599    type Error = io::Error;
600
601    fn try_from(value: Output) -> Result<Self, Self::Error> {
602        match value {
603            Output::Null => Ok(Self::Null),
604            Output::File { path, append } => {
605                let file = File::options()
606                    .create(true)
607                    .write(true)
608                    .append(append)
609                    .open(path)?;
610
611                Ok(Self::File(file))
612            }
613            Output::Log(log) => Ok(Self::Log {
614                lvl: log,
615                buffer: vec![],
616            }),
617            Output::Bytes => Ok(Self::Bytes(vec![])),
618        }
619    }
620}
621
622enum RealizedOutput {
623    Null,
624    File(File),
625    Log { lvl: Level, buffer: Vec<u8> },
626    Bytes(Vec<u8>),
627}
628
629impl Write for RealizedOutput {
630    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
631        match self {
632            RealizedOutput::Null => Ok(buf.len()),
633            RealizedOutput::File(f) => f.write(buf),
634            RealizedOutput::Log { lvl: l, buffer } => {
635                buffer.extend(IntoIterator::into_iter(buf));
636                while let Some(pos) = buffer.iter().position(|&l| l == b'\n' || l == 0) {
637                    let line = &buffer[..pos];
638                    let string = String::from_utf8_lossy(line);
639                    log!(*l, "{}", string);
640                    buffer.drain(..=pos);
641                }
642                Ok(buf.len())
643            }
644            RealizedOutput::Bytes(b) => {
645                b.extend(buf);
646                Ok(buf.len())
647            }
648        }
649    }
650
651    fn flush(&mut self) -> io::Result<()> {
652        match self {
653            RealizedOutput::File(file) => file.flush(),
654            RealizedOutput::Log { lvl, buffer } => {
655                while let Some(pos) = buffer.iter().position(|&l| l == b'\n' || l == 0) {
656                    let line = &buffer[..pos];
657                    let string = String::from_utf8_lossy(line);
658                    log!(*lvl, "{}", string);
659                    buffer.drain(..=pos);
660                }
661                Ok(())
662            }
663            _ => Ok(()),
664        }
665    }
666}
667
668/// Gets the result of the exec spec
669pub struct ExecResult {
670    code: ExitStatus,
671    bytes: Option<Vec<u8>>,
672    bytes_err: Option<Vec<u8>>,
673}
674
675impl ExecResult {
676    /// Gets the exit code for the exec spec
677    pub fn code(&self) -> ExitStatus {
678        self.code
679    }
680
681    /// Gets whether the exec spec is a success
682    pub fn success(&self) -> bool {
683        self.code.success()
684    }
685
686    /// Make this an error if exit code is not success
687    pub fn expect_success(self) -> BuildResult<Self> {
688        if !self.success() {
689            Err(BuildException::new("expected a successful return code").into())
690        } else {
691            Ok(self)
692        }
693    }
694
695    /// Gets the output, in bytes, if the original exec spec specified the bytes
696    /// output type
697    pub fn bytes(&self) -> Option<&[u8]> {
698        self.bytes.as_ref().map(|s| &s[..])
699    }
700
701    /// Try to convert the output bytes into a string
702    pub fn utf8_string(&self) -> Option<Result<String, FromUtf8Error>> {
703        self.bytes()
704            .map(|s| Vec::from_iter(s.iter().copied()))
705            .map(String::from_utf8)
706    }
707
708    /// Gets the output, in bytes, if the original exec spec specified the bytes
709    /// output type
710    pub fn bytes_err(&self) -> Option<&[u8]> {
711        self.bytes_err.as_ref().map(|s| &s[..])
712    }
713
714    /// Try to convert the output bytes into a string
715    pub fn utf8_string_err(&self) -> Option<Result<String, FromUtf8Error>> {
716        self.bytes_err()
717            .map(|s| Vec::from_iter(s.iter().copied()))
718            .map(String::from_utf8)
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn create_exec_spec() {
728        let mut builder = ExecSpecBuilder::new();
729        builder.exec("echo").arg("hello, world");
730        let exec = builder.build().unwrap();
731        assert_eq!(exec.executable, "echo");
732    }
733
734    #[test]
735    fn can_execute_spec() {
736        let spec = ExecSpecBuilder::new()
737            .with_exec("echo")
738            .with_args(["hello", "world"])
739            .with_stdout(Output::Bytes)
740            .build()
741            .expect("Couldn't build exec spec");
742
743        let result = { spec }.execute_spec("/").expect("Couldn't create handle");
744        let wait = result.wait().expect("couldn't finish exec spec");
745        let bytes = String::from_utf8(wait.bytes.unwrap()).unwrap();
746        assert_eq!("hello world", bytes.trim());
747    }
748
749    #[test]
750    fn invalid_exec_can_be_detected() {
751        let spec = ExecSpecBuilder::new()
752            .with_exec("please-dont-exist")
753            .with_stdout(Output::Null)
754            .build()
755            .expect("couldn't build");
756
757        let spawn = spec.execute_spec("/");
758
759        assert!(matches!(spawn, Err(_)), "Should return an error");
760    }
761
762    #[test]
763    fn emit_to_log() {}
764}