Skip to main content

garden/cmds/
exec.rs

1use std::sync::atomic;
2
3use anyhow::Result;
4use clap::{Parser, ValueHint};
5use rayon::prelude::*;
6
7use crate::cli::GardenOptions;
8use crate::{cmd, constants, errors, model, query};
9
10/// Evaluate garden expressions
11#[derive(Parser, Clone, Debug)]
12#[command(author, about, long_about)]
13pub struct ExecOptions {
14    /// Filter trees by name post-query using a glob pattern
15    #[arg(long, short, default_value = "*")]
16    pub(crate) trees: String,
17    /// Perform a trial run without executing any commands
18    #[arg(long, short = 'N', short_alias = 'n')]
19    pub(crate) dry_run: bool,
20    /// Run commands in parallel using the specified number of jobs.
21    #[arg(long = "jobs", short = 'j', value_name = "JOBS")]
22    pub(crate) num_jobs: Option<usize>,
23    /// Be quiet
24    #[arg(short, long)]
25    pub(crate) quiet: bool,
26    /// Increase verbosity level (default: 0)
27    #[arg(short, long, action = clap::ArgAction::Count)]
28    pub(crate) verbose: u8,
29    /// Tree query for the gardens, groups or trees to run the command
30    #[arg(value_hint=ValueHint::Other)]
31    pub(crate) query: String,
32    /// Command to run in the resolved environments
33    #[arg(allow_hyphen_values = true, trailing_var_arg = true, required = true, value_hint=ValueHint::CommandWithArguments)]
34    pub(crate) command: Vec<String>,
35}
36
37/// Main entry point for the "garden exec" command
38pub fn main(app_context: &model::ApplicationContext, exec_options: &mut ExecOptions) -> Result<()> {
39    exec_options.verbose += app_context.options.verbose;
40    if app_context.options.debug_level(constants::DEBUG_LEVEL_EXEC) > 0 {
41        debug!("query: {}", exec_options.query);
42        debug!("command: {:?}", exec_options.command);
43    }
44    exec_options.quiet |= app_context.options.quiet;
45    exec(app_context, exec_options)
46}
47
48/// Execute a command over every tree in the evaluated tree query.
49fn exec(app_context: &model::ApplicationContext, exec_options: &ExecOptions) -> Result<()> {
50    let quiet = exec_options.quiet;
51    let verbose = exec_options.verbose;
52    let dry_run = exec_options.dry_run;
53    let query = &exec_options.query;
54    let tree_pattern = &exec_options.trees;
55    let command = &exec_options.command;
56    // Strategy: resolve the trees down to a set of tree indexes paired with
57    // an optional garden context.
58    //
59    // If the names resolve to gardens, each garden is processed independently.
60    // Trees that exist in multiple matching gardens will be processed multiple
61    // times.
62    //
63    // If the names resolve to trees, each tree is processed independently
64    // with no garden context.
65    cmd::initialize_threads_option(exec_options.num_jobs)?;
66
67    // Resolve the tree query into a vector of tree contexts.
68    let config = app_context.get_root_config_mut();
69    let contexts = query::resolve_trees(app_context, config, None, query);
70    let pattern = glob::Pattern::new(tree_pattern).unwrap_or_default();
71    let exit_status = atomic::AtomicI32::new(errors::EX_OK);
72
73    // Loop over each context, evaluate the tree environment,
74    // and run the command.
75    if exec_options.num_jobs.is_some() {
76        contexts.par_iter().for_each(|context| {
77            let app_context_clone = app_context.clone();
78            let app_context = &app_context_clone;
79            if !model::is_valid_context(app_context, &pattern, context) {
80                return;
81            }
82            // Run the command in the current context.
83            if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context(
84                app_context,
85                app_context.get_root_config(),
86                context,
87                quiet,
88                verbose,
89                dry_run,
90                command,
91            ) {
92                exit_status.store(status, atomic::Ordering::Release);
93            }
94        });
95    } else {
96        for context in &contexts {
97            if !model::is_valid_context(app_context, &pattern, context) {
98                continue;
99            }
100            // Run the command in the current context.
101            if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context(
102                app_context,
103                config,
104                context,
105                quiet,
106                verbose,
107                dry_run,
108                command,
109            ) {
110                exit_status.store(status, atomic::Ordering::Release);
111            }
112        }
113    }
114
115    // Return the last non-zero exit status.
116    errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
117}