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    trees: String,
17    /// Perform a trial run without executing any commands
18    #[arg(long, short = 'N', short_alias = 'n')]
19    dry_run: bool,
20    /// Run commands in parallel using the specified number of jobs.
21    #[arg(long = "jobs", short = 'j', value_name = "JOBS")]
22    num_jobs: Option<usize>,
23    /// Be quiet
24    #[arg(short, long)]
25    quiet: bool,
26    /// Increase verbosity level (default: 0)
27    #[arg(short, long, action = clap::ArgAction::Count)]
28    verbose: u8,
29    /// Tree query for the gardens, groups or trees to run the command
30    #[arg(value_hint=ValueHint::Other)]
31    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    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/// Return true if the context has not been filtered out and refers to a valid configured tree.
49fn is_valid_context(
50    app_context: &model::ApplicationContext,
51    pattern: &glob::Pattern,
52    context: &model::TreeContext,
53) -> bool {
54    if !pattern.matches(&context.tree) {
55        return false;
56    }
57    let tree_opt = match context.config {
58        Some(graft_id) => app_context.get_config(graft_id).trees.get(&context.tree),
59        None => app_context.get_root_config().trees.get(&context.tree),
60    };
61    let tree = match tree_opt {
62        Some(tree) => tree,
63        None => return false,
64    };
65    // Skip symlink trees.
66    if tree.is_symlink {
67        return false;
68    }
69
70    true
71}
72
73/// Execute a command over every tree in the evaluated tree query.
74fn exec(app_context: &model::ApplicationContext, exec_options: &ExecOptions) -> Result<()> {
75    let quiet = exec_options.quiet;
76    let verbose = exec_options.verbose;
77    let dry_run = exec_options.dry_run;
78    let query = &exec_options.query;
79    let tree_pattern = &exec_options.trees;
80    let command = &exec_options.command;
81    // Strategy: resolve the trees down to a set of tree indexes paired with
82    // an optional garden context.
83    //
84    // If the names resolve to gardens, each garden is processed independently.
85    // Trees that exist in multiple matching gardens will be processed multiple
86    // times.
87    //
88    // If the names resolve to trees, each tree is processed independently
89    // with no garden context.
90    cmd::initialize_threads_option(exec_options.num_jobs)?;
91
92    // Resolve the tree query into a vector of tree contexts.
93    let config = app_context.get_root_config_mut();
94    let contexts = query::resolve_trees(app_context, config, None, query);
95    let pattern = glob::Pattern::new(tree_pattern).unwrap_or_default();
96    let exit_status = atomic::AtomicI32::new(errors::EX_OK);
97
98    // Loop over each context, evaluate the tree environment,
99    // and run the command.
100    if exec_options.num_jobs.is_some() {
101        contexts.par_iter().for_each(|context| {
102            let app_context_clone = app_context.clone();
103            let app_context = &app_context_clone;
104            if !is_valid_context(app_context, &pattern, context) {
105                return;
106            }
107            // Run the command in the current context.
108            if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context(
109                app_context,
110                app_context.get_root_config(),
111                context,
112                quiet,
113                verbose,
114                dry_run,
115                command,
116            ) {
117                exit_status.store(status, atomic::Ordering::Release);
118            }
119        });
120    } else {
121        for context in &contexts {
122            if !is_valid_context(app_context, &pattern, context) {
123                continue;
124            }
125            // Run the command in the current context.
126            if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context(
127                app_context,
128                config,
129                context,
130                quiet,
131                verbose,
132                dry_run,
133                command,
134            ) {
135                exit_status.store(status, atomic::Ordering::Release);
136            }
137        }
138    }
139
140    // Return the last non-zero exit status.
141    errors::exit_status_into_result(exit_status.load(atomic::Ordering::Acquire))
142}