1use 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#[derive(Debug, Default, Clone)]
24pub enum Input {
25 #[default]
27 Null,
28 File(PathBuf),
30 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#[derive(Debug, Clone)]
78pub enum Output {
79 Null,
81 File {
87 path: PathBuf,
89 append: bool,
91 },
92 Log(#[doc("The log level to emit output to")] Level),
94 Bytes,
96}
97
98impl Output {
99 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#[derive(Debug, Default, Clone)]
140pub struct ExecSpec {
141 pub working_dir: PathBuf,
143 pub executable: OsString,
145 pub clargs: Vec<OsString>,
147 pub env: HashMap<String, String>,
152 pub input: Input,
154 pub output: Output,
156 pub output_err: Output,
158}
159
160impl ExecSpec {
161 pub fn working_dir(&self) -> &Path {
164 &self.working_dir
165 }
166 pub fn executable(&self) -> &OsStr {
168 &self.executable
169 }
170
171 pub fn args(&self) -> &[OsString] {
173 &self.clargs[..]
174 }
175
176 pub fn env(&self) -> &HashMap<String, String> {
178 &self.env
179 }
180
181 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 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 #[deprecated]
219 pub fn finish(&mut self) -> io::Result<ExitStatus> {
220 panic!("unimplemented")
221 }
222}
223
224impl VisitProject<Result<(), io::Error>> for ExecSpec {
225 fn visit(&mut self, project: &Project) -> Result<(), io::Error> {
227 self.execute(project.project_dir()).map(|_| ())
228 }
229}
230
231pub struct ExecSpecBuilder {
233 pub working_dir: Option<PathBuf>,
235 pub executable: Option<OsString>,
237 pub clargs: Vec<OsString>,
239 pub env: HashMap<String, String>,
245 stdin: Input,
247 output: Output,
248 output_err: Output,
249}
250
251#[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 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 pub fn default_env() -> HashMap<String, String> {
282 std::env::vars().into_iter().collect()
283 }
284
285 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 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 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 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 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 pub fn with_arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
325 self.arg(arg);
326 self
327 }
328
329 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 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 pub fn with_exec<E: AsRef<OsStr>>(mut self, exec: E) -> Self {
346 self.exec(exec);
347 self
348 }
349
350 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 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 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 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 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 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 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 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
433pub 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 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 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 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
668pub struct ExecResult {
670 code: ExitStatus,
671 bytes: Option<Vec<u8>>,
672 bytes_err: Option<Vec<u8>>,
673}
674
675impl ExecResult {
676 pub fn code(&self) -> ExitStatus {
678 self.code
679 }
680
681 pub fn success(&self) -> bool {
683 self.code.success()
684 }
685
686 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 pub fn bytes(&self) -> Option<&[u8]> {
698 self.bytes.as_ref().map(|s| &s[..])
699 }
700
701 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 pub fn bytes_err(&self) -> Option<&[u8]> {
711 self.bytes_err.as_ref().map(|s| &s[..])
712 }
713
714 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}