1use crate::{constants, display, errors, eval, model, syntax};
2
3pub fn status(exec: subprocess::Exec) -> i32 {
5 if let Err(status) = subprocess_result(exec.join()) {
6 status
7 } else {
8 errors::EX_OK
9 }
10}
11
12pub fn subprocess_result(result: subprocess::Result<subprocess::ExitStatus>) -> Result<(), i32> {
14 match result {
15 Ok(subprocess::ExitStatus::Exited(status)) => {
16 if status == 0 {
17 Ok(())
18 } else {
19 Err(status as i32)
20 }
21 }
22 Ok(subprocess::ExitStatus::Signaled(status)) => Err(status as i32),
23 Ok(subprocess::ExitStatus::Other(status)) => Err(status),
24 Ok(subprocess::ExitStatus::Undetermined) => Err(errors::EX_ERROR),
25 Err(subprocess::PopenError::IoError(err)) => {
26 if err.kind() == std::io::ErrorKind::NotFound {
27 Err(errors::EX_UNAVAILABLE)
28 } else {
29 Err(errors::EX_IOERR)
30 }
31 }
32 Err(_) => Err(errors::EX_ERROR),
33 }
34}
35
36fn stdout(capture: &subprocess::CaptureData) -> String {
38 capture.stdout_str().trim_end().to_string()
39}
40
41fn command_error_from_popen_error(
43 command: String,
44 popen_err: subprocess::PopenError,
45) -> errors::CommandError {
46 let status = match popen_err {
47 subprocess::PopenError::IoError(err) => err.raw_os_error().unwrap_or(1),
48 _ => 1,
49 };
50 errors::CommandError::ExitStatus { command, status }
51}
52
53pub(crate) fn capture_stdout(
55 exec: subprocess::Exec,
56) -> Result<subprocess::CaptureData, errors::CommandError> {
57 let command = exec.to_cmdline_lossy();
58 let capture = exec
59 .stdout(subprocess::Redirection::Pipe)
60 .stderr(subprocess::NullFile {}) .capture();
62
63 match capture {
64 Ok(result) => {
65 let status = exit_status(result.exit_status);
66 if status == 0 {
67 Ok(result)
68 } else {
69 Err(errors::CommandError::ExitStatus { command, status })
70 }
71 }
72 Err(err) => Err(command_error_from_popen_error(command, err)),
73 }
74}
75
76pub(crate) fn exit_status(status: subprocess::ExitStatus) -> i32 {
78 match status {
79 subprocess::ExitStatus::Exited(status) => status as i32,
80 subprocess::ExitStatus::Signaled(status) => status as i32,
81 subprocess::ExitStatus::Other(status) => status,
82 subprocess::ExitStatus::Undetermined => errors::EX_ERROR,
83 }
84}
85
86pub fn stdout_to_string(exec: subprocess::Exec) -> Result<String, errors::CommandError> {
88 Ok(stdout(&capture_stdout(exec)?))
89}
90
91pub fn exec_cmd<S>(command: &[S]) -> subprocess::Exec
93where
94 S: AsRef<std::ffi::OsStr>,
95{
96 if command.len() > 1 {
97 subprocess::Exec::cmd(&command[0]).args(&command[1..])
98 } else {
99 subprocess::Exec::cmd(&command[0])
100 }
101}
102
103pub fn exec_in_dir<P, S>(command: &[S], path: &P) -> subprocess::Exec
105where
106 P: AsRef<std::path::Path> + std::convert::AsRef<std::ffi::OsStr> + ?Sized,
107 S: AsRef<std::ffi::OsStr>,
108{
109 exec_cmd(command).cwd(path).env(constants::ENV_PWD, path)
110}
111
112pub(crate) fn run_command<P, S>(command: &[S], path: &P) -> i32
115where
116 P: AsRef<std::path::Path> + std::convert::AsRef<std::ffi::OsStr> + ?Sized,
117 S: AsRef<std::ffi::OsStr>,
118{
119 status(exec_in_dir(command, path))
120}
121
122pub(crate) fn exec_in_context<S>(
130 app_context: &model::ApplicationContext,
131 config: &model::Configuration,
132 context: &model::TreeContext,
133 quiet: bool,
134 verbose: u8,
135 dry_run: bool,
136 command: &[S],
137) -> Result<(), errors::GardenError>
138where
139 S: AsRef<std::ffi::OsStr>,
140{
141 let display_options = display::DisplayOptions {
142 branches: config.tree_branches,
143 verbose,
144 quiet,
145 ..std::default::Default::default()
146 };
147 let graft_config = context
148 .config
149 .map(|graft_id| app_context.get_config(graft_id));
150
151 let path;
152 if let Some(graft_cfg) = graft_config {
153 if let Some(tree) = graft_cfg.trees.get(&context.tree) {
154 path = tree.path_as_ref()?;
155
156 if !display::print_tree(tree, &display_options) {
158 return Ok(());
159 }
160 } else {
161 return Ok(());
162 }
163 } else if let Some(tree) = config.trees.get(&context.tree) {
164 path = tree.path_as_ref()?;
165
166 if !display::print_tree(tree, &display_options) {
168 return Ok(());
169 }
170 } else {
171 return Ok(());
172 }
173 let env = eval::environment(app_context, config, context);
175 let command_vec = resolve_command(command, &env);
176 if verbose > 1 || dry_run {
177 display::print_command_string_vec(&command_vec);
178 }
179 if dry_run {
180 return Ok(());
181 }
182
183 let mut exec = exec_in_dir(&command_vec, &path);
185
186 for (name, value) in &env {
188 exec = exec.env(name, value);
189 }
190
191 errors::result_from_exit_status(status(exec))
192}
193
194fn resolve_command<S>(command: &[S], env: &[(String, String)]) -> Vec<String>
198where
199 S: AsRef<std::ffi::OsStr>,
200{
201 let mut cmd_path = std::path::PathBuf::from(&command[0]);
202 if !cmd_path.is_absolute() {
204 for (name, value) in env {
205 if name == constants::ENV_PATH {
207 if let Some(path_buf) = std::env::split_paths(&value).find_map(|dir| {
208 let full_path = dir.join(&cmd_path);
209 if full_path.is_file() {
210 Some(full_path)
211 } else {
212 None
213 }
214 }) {
215 cmd_path = path_buf;
216 }
217 break;
219 }
220 }
221 }
222
223 let mut command_vec = Vec::with_capacity(command.len());
226 command_vec.push(cmd_path.to_string_lossy().to_string());
227 for arg in &command[1..] {
228 let curpath = std::path::PathBuf::from(arg);
229 command_vec.push(curpath.to_string_lossy().into());
230 }
231
232 command_vec
233}
234
235pub(crate) fn current_exe() -> String {
237 match std::env::current_exe() {
238 Err(_) => constants::GARDEN.into(),
239 Ok(path) => path.to_string_lossy().into(),
240 }
241}
242
243pub(crate) fn get_command_values(
247 app_context: &model::ApplicationContext,
248 context: &model::TreeContext,
249 name: &str,
250) -> Vec<String> {
251 let config = match context.config {
252 Some(config_id) => app_context.get_config(config_id),
253 None => app_context.get_root_config(),
254 };
255 let mut vec_variables = Vec::new();
256
257 for (command_name, var) in &config.commands {
259 if name == command_name {
260 vec_variables.push(var.clone());
261 }
262 }
263
264 if let Some(tree) = config.trees.get(&context.tree) {
266 for (command_name, var) in &tree.commands {
267 if name == command_name {
268 vec_variables.push(var.clone());
269 }
270 }
271 }
272
273 if let Some(garden_name) = &context.garden {
275 if let Some(garden) = &config.gardens.get(garden_name) {
276 for (command_name, var) in &garden.commands {
277 if name == command_name {
278 vec_variables.push(var.clone());
279 }
280 }
281 }
282 }
283
284 let mut commands = Vec::with_capacity(vec_variables.len() * 2);
285 for variables in vec_variables.iter_mut() {
286 let values = eval::variables_for_shell(app_context, config, variables, context);
287 commands.extend(values);
288 }
289
290 commands
291}
292
293pub(crate) fn expand_command_names(
296 app_context: &model::ApplicationContext,
297 context: &model::TreeContext,
298 name: &str,
299) -> Vec<String> {
300 let pre_name = syntax::pre_command(name);
301 let post_name = syntax::post_command(name);
302 let pre_commands = get_command_values(app_context, context, &pre_name);
303 let post_commands = get_command_values(app_context, context, &post_name);
304
305 let mut command_names = Vec::with_capacity(pre_commands.len() + 1 + post_commands.len());
306 for cmd_name in pre_commands.iter() {
308 if cmd_name != name {
309 command_names.extend(expand_command_names(app_context, context, cmd_name));
311 }
312 }
313 command_names.push(name.to_string());
314 for cmd_name in post_commands.iter() {
316 if cmd_name != name {
317 command_names.extend(expand_command_names(app_context, context, cmd_name));
319 }
320 }
321
322 command_names
323}
324
325pub(crate) fn shell_quote(arg: &str) -> String {
328 shlex::try_quote(arg)
329 .map(|quoted_arg| quoted_arg.to_string())
330 .unwrap_or_else(|_| arg.to_string())
331}
332
333pub fn shlex_split(shell: &str) -> Vec<String> {
335 if shell.is_empty() {
336 return Vec::new();
337 }
338 match shlex::split(shell) {
339 Some(shell_command) if !shell_command.is_empty() => shell_command,
340 _ => {
341 vec![shell.to_string()]
342 }
343 }
344}
345
346pub(crate) fn default_num_jobs() -> usize {
348 match std::thread::available_parallelism() {
349 Ok(value) => std::cmp::max(value.get(), 3), Err(_) => 4,
351 }
352}
353
354pub(crate) fn initialize_threads(num_jobs: usize) -> anyhow::Result<()> {
356 let num_jobs = if num_jobs == 0 {
357 default_num_jobs()
358 } else {
359 num_jobs
360 };
361 rayon::ThreadPoolBuilder::new()
362 .num_threads(num_jobs)
363 .build_global()?;
364
365 Ok(())
366}
367
368pub fn initialize_threads_option(num_jobs: Option<usize>) -> anyhow::Result<()> {
370 let Some(num_jobs_value) = num_jobs else {
371 return Ok(());
372 };
373
374 initialize_threads(num_jobs_value)
375}