1use shell_words::split;
2use std::{
3 collections::HashMap,
4 io,
5 path::Path,
6 process::{ExitStatus, Stdio},
7};
8use thiserror::Error;
9use tokio::process::Command;
10
11use crate::{
12 config::Config,
13 lua_installation::LuaInstallation,
14 lua_rockspec::{Build, BuildInfo, CommandBuildSpec},
15 progress::{Progress, ProgressBar},
16 tree::{RockLayout, Tree},
17 variables::VariableSubstitutionError,
18};
19
20use super::external_dependency::ExternalDependencyInfo;
21use super::utils;
22
23#[derive(Error, Debug)]
24pub enum CommandError {
25 #[error("'build_command' and 'install_command' cannot be empty.")]
26 EmptyCommand,
27 #[error("error parsing command:\n{command}\n\nerror: {err}")]
28 ParseError {
29 err: shell_words::ParseError,
30 command: String,
31 },
32 #[error("error executing command:\n{command}\n\nerror: {err}")]
33 Io { err: io::Error, command: String },
34 #[error("failed to execute command:\n{command}\n\nstatus: {status}\nstdout: {stdout}\nstderr: {stderr}")]
35 CommandFailure {
36 command: String,
37 status: ExitStatus,
38 stdout: String,
39 stderr: String,
40 },
41 #[error(transparent)]
42 VariableSubstitutionError(#[from] VariableSubstitutionError),
43}
44
45impl Build for CommandBuildSpec {
46 type Err = CommandError;
47
48 async fn run(
49 self,
50 output_paths: &RockLayout,
51 no_install: bool,
52 lua: &LuaInstallation,
53 external_dependencies: &HashMap<String, ExternalDependencyInfo>,
54 config: &Config,
55 _tree: &Tree,
56 build_dir: &Path,
57 progress: &Progress<ProgressBar>,
58 ) -> Result<BuildInfo, Self::Err> {
59 progress.map(|bar| bar.set_message("Running build_command..."));
60 run_command(
61 &self.build_command,
62 output_paths,
63 lua,
64 external_dependencies,
65 config,
66 build_dir,
67 )
68 .await?;
69 if !no_install {
70 progress.map(|bar| bar.set_message("Running install_command..."));
71 run_command(
72 &self.install_command,
73 output_paths,
74 lua,
75 external_dependencies,
76 config,
77 build_dir,
78 )
79 .await?;
80 }
81 Ok(BuildInfo::default())
82 }
83}
84
85async fn run_command(
86 command: &str,
87 output_paths: &RockLayout,
88 lua: &LuaInstallation,
89 external_dependencies: &HashMap<String, ExternalDependencyInfo>,
90 config: &Config,
91 build_dir: &Path,
92) -> Result<(), CommandError> {
93 let substituted_cmd =
94 utils::substitute_variables(command, output_paths, lua, external_dependencies, config)?;
95 let cmd_parts = split(&substituted_cmd).map_err(|err| CommandError::ParseError {
96 err,
97 command: substituted_cmd.clone(),
98 })?;
99 let (program, args) = cmd_parts.split_first().ok_or(CommandError::EmptyCommand)?;
100 match Command::new(program)
101 .args(args)
102 .current_dir(build_dir)
103 .stdout(Stdio::piped())
104 .stderr(Stdio::piped())
105 .spawn()
106 {
107 Err(err) => {
108 return Err(CommandError::Io {
109 err,
110 command: substituted_cmd,
111 })
112 }
113 Ok(child) => match child.wait_with_output().await {
114 Ok(output) if output.status.success() => {}
115 Ok(output) => {
116 return Err(CommandError::CommandFailure {
117 command: substituted_cmd,
118 status: output.status,
119 stdout: String::from_utf8_lossy(&output.stdout).into(),
120 stderr: String::from_utf8_lossy(&output.stderr).into(),
121 });
122 }
123 Err(err) => {
124 return Err(CommandError::Io {
125 err,
126 command: substituted_cmd,
127 })
128 }
129 },
130 }
131 Ok(())
132}