diff --git a/doc/src/changelog.md b/doc/src/changelog.md index e936f37..4ae63d6 100644 --- a/doc/src/changelog.md +++ b/doc/src/changelog.md @@ -2,6 +2,11 @@ ## Upcoming +**Features**: + +- `garden exec` can now run commands in parallel using the `-j# | --jobs=#` option. +([#43](https://github.com/garden-rs/garden/issues/43)) + **Packaging**: - Garden's Nix flake was improved and using Garden with Nix home-manager was documented. @@ -21,7 +26,7 @@ **Features**: -- `garden cmd` and custom commands now have a `--jobs=# | -j#` option for +- `garden cmd` and custom commands now have a `-j# | --jobs=#` option for [running commands in parallel](https://garden-rs.gitlab.io/commands.html#parallel-execution). Use `-j0 | --jobs=0` to use all available cores. ([#43](https://github.com/garden-rs/garden/issues/43)) diff --git a/src/cmds/exec.rs b/src/cmds/exec.rs index fcf5bfc..805a701 100644 --- a/src/cmds/exec.rs +++ b/src/cmds/exec.rs @@ -1,5 +1,8 @@ +use std::sync::atomic; + use anyhow::Result; use clap::{Parser, ValueHint}; +use rayon::prelude::*; use crate::{cmd, constants, errors, model, query}; @@ -13,6 +16,9 @@ pub struct ExecOptions { /// Perform a trial run without executing any commands #[arg(long, short = 'N', short_alias = 'n')] dry_run: bool, + /// Run commands in parallel using the specified number of jobs. + #[arg(long = "jobs", short = 'j', value_name = "JOBS")] + num_jobs: Option, /// Be quiet #[arg(short, long)] quiet: bool, @@ -80,33 +86,57 @@ fn exec(app_context: &model::ApplicationContext, exec_options: &ExecOptions) -> // // If the names resolve to trees, each tree is processed independently // with no garden context. + cmd::initialize_threads_option(exec_options.num_jobs)?; // Resolve the tree query into a vector of tree contexts. let config = app_context.get_root_config_mut(); let contexts = query::resolve_trees(app_context, config, None, query); let pattern = glob::Pattern::new(tree_pattern).unwrap_or_default(); - let mut exit_status: i32 = 0; + let exit_status = atomic::AtomicI32::new(errors::EX_OK); // Loop over each context, evaluate the tree environment, // and run the command. - for context in &contexts { - if !is_valid_context(app_context, &pattern, context) { - continue; - } - // Run the command in the current context. - if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context( - app_context, - config, - context, - quiet, - verbose, - dry_run, - command, - ) { - exit_status = status; + if exec_options.num_jobs.is_some() { + contexts.par_iter().for_each(|context| { + let app_context_clone = app_context.clone(); + let app_context = &app_context_clone; + if !is_valid_context(app_context, &pattern, context) { + return; + } + // Run the command in the current context. + if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context( + app_context, + app_context.get_root_config(), + context, + quiet, + verbose, + dry_run, + command, + ) { + exit_status.store(status, atomic::Ordering::Relaxed); + } + }); + } else { + for context in &contexts { + if !is_valid_context(app_context, &pattern, context) { + continue; + } + // Run the command in the current context. + if let Err(errors::GardenError::ExitStatus(status)) = cmd::exec_in_context( + app_context, + config, + context, + quiet, + verbose, + dry_run, + command, + ) { + exit_status.store(status, atomic::Ordering::Relaxed); + } } } // Return the last non-zero exit status. - cmd::result_from_exit_status(exit_status).map_err(|err| err.into()) + cmd::result_from_exit_status(exit_status.load(atomic::Ordering::SeqCst)) + .map_err(|err| err.into()) }