Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1493,6 +1493,9 @@ baz:
sleep 1
```

The number of simultaneously running recipes may be limited with the `--jobs`
option<sup>master</sup>.

GNU `parallel` may be used to run recipe lines concurrently:

```just
Expand Down
7 changes: 7 additions & 0 deletions src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ pub struct Arguments {
long
)]
pub(crate) indentation: Option<Indentation>,
#[arg(
env = "JUST_JOBS",
help = "Run at most <N> recipes simultaneously with the [parallel] attribute",
long,
value_name = "N"
)]
pub(crate) jobs: Option<NonZeroU64>,
#[arg(
add = ArgValueCompleter::new(PathCompleter::file()),
env = "JUST_JUSTFILE",
Expand Down
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) struct Config {
pub(crate) highlight: bool,
pub(crate) indentation: Option<Indentation>,
pub(crate) invocation_directory: PathBuf,
pub(crate) jobs: Option<NonZeroU64>,
pub(crate) justfile_names: Option<Vec<String>>,
pub(crate) list_heading: String,
pub(crate) list_prefix: String,
Expand Down Expand Up @@ -66,6 +67,7 @@ impl Config {
highlight: true,
indentation: None,
invocation_directory: env::current_dir().context(config_error::CurrentDir)?,
jobs: None,
justfile_names: None,
list_heading: Arguments::DEFAULT_LIST_HEADING.into(),
list_prefix: Arguments::DEFAULT_LIST_PREFIX.into(),
Expand Down Expand Up @@ -342,6 +344,7 @@ impl Config {
highlight: !arguments.no_highlight,
indentation: arguments.indentation,
invocation_directory,
jobs: arguments.jobs,
justfile_names: arguments.justfile_names,
list_heading: arguments.list_heading,
list_prefix: arguments.list_prefix,
Expand Down
20 changes: 17 additions & 3 deletions src/justfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ impl<'src> Justfile<'src> {

let ran = Ran::new();
let cache = Cache::new(search);
let jobs = Semaphore::new(config.jobs.unwrap_or(NonZeroU64::MAX));
for invocation in invocations {
Self::run_recipe(
&invocation.arguments,
Expand All @@ -238,6 +239,7 @@ impl<'src> Justfile<'src> {
&scopes,
search,
&cache,
&jobs,
)?;
}

Expand Down Expand Up @@ -460,6 +462,7 @@ impl<'src> Justfile<'src> {
scopes: &Scopes<'src, '_>,
search: &Search,
cache: &Cache,
jobs: &Semaphore,
) -> RunResult<'src> {
let mutex = ran.mutex(recipe, arguments);

Expand Down Expand Up @@ -511,9 +514,18 @@ impl<'src> Justfile<'src> {
scopes,
search,
cache,
jobs,
)?;

recipe.run(&context, &env, is_dependency, &positional, &scope, cache)?;
recipe.run(
&context,
&env,
is_dependency,
&positional,
&scope,
cache,
jobs,
)?;

Self::run_dependencies(
config,
Expand All @@ -526,6 +538,7 @@ impl<'src> Justfile<'src> {
scopes,
search,
cache,
jobs,
)?;

*guard = true;
Expand All @@ -544,6 +557,7 @@ impl<'src> Justfile<'src> {
scopes: &Scopes<'src, 'run>,
search: &Search,
cache: &Cache,
jobs: &Semaphore,
) -> RunResult<'src> {
if context.config.no_dependencies {
return Ok(());
Expand Down Expand Up @@ -590,7 +604,7 @@ impl<'src> Justfile<'src> {
for (recipe, arguments) in evaluated {
handles.push(thread_scope.spawn(move || {
Self::run_recipe(
&arguments, config, true, overrides, ran, recipe, scopes, search, cache,
&arguments, config, true, overrides, ran, recipe, scopes, search, cache, jobs,
)
}));
}
Expand All @@ -604,7 +618,7 @@ impl<'src> Justfile<'src> {
} else {
for (recipe, arguments) in evaluated {
Self::run_recipe(
&arguments, config, true, overrides, ran, recipe, scopes, search, cache,
&arguments, config, true, overrides, ran, recipe, scopes, search, cache, jobs,
)?;
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub(crate) use {
search::Search,
search_config::SearchConfig,
search_error::SearchError,
semaphore::Semaphore,
set::Set,
setting::Setting,
settings::Settings,
Expand Down Expand Up @@ -159,14 +160,14 @@ pub(crate) use {
io::{self, Sink, Write},
iter::{self, FromIterator},
mem,
num::ParseIntError,
num::{NonZeroU64, ParseIntError},
ops::Deref,
ops::{Index, RangeInclusive},
path::{self, Component, Path, PathBuf},
process::{self, Command, ExitStatus, Stdio},
slice,
str::{self, Chars, FromStr},
sync::{Arc, LazyLock, Mutex, MutexGuard},
sync::{Arc, Condvar, LazyLock, Mutex, MutexGuard},
thread,
time::Instant,
vec,
Expand Down Expand Up @@ -313,6 +314,7 @@ mod scope;
mod search;
mod search_config;
mod search_error;
mod semaphore;
mod set;
mod setting;
mod settings;
Expand Down
3 changes: 3 additions & 0 deletions src/recipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ impl<'src> Recipe<'src> {
positional: &[String],
scope: &Scope<'src, 'run>,
cache: &Cache,
jobs: &Semaphore,
) -> RunResult<'src> {
let _guard = jobs.acquire();

let color = context.config.color.stderr().banner();
let prefix = color.prefix();
let suffix = color.suffix();
Expand Down
37 changes: 37 additions & 0 deletions src/semaphore.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use super::*;

pub(crate) struct Semaphore(Condvar, Mutex<u64>);

pub(crate) struct Guard<'a>(&'a Semaphore);

impl Drop for Guard<'_> {
fn drop(&mut self) {
*self.0.mutex().lock().unwrap() += 1;
self.0.condvar().notify_one();
}
}

impl Semaphore {
pub(crate) fn new(resource: NonZeroU64) -> Self {
Self(Condvar::new(), Mutex::new(resource.into()))
}

fn condvar(&self) -> &Condvar {
&self.0
}

fn mutex(&self) -> &Mutex<u64> {
&self.1
}

pub(crate) fn acquire(&self) -> Guard {
let mut count = self
.condvar()
.wait_while(self.mutex().lock().unwrap(), |count| *count == 0)
.unwrap();

*count -= 1;

Guard(self)
}
}
72 changes: 72 additions & 0 deletions tests/parallel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,75 @@ fn dependents_block_on_running_dependencies() {
)
.success();
}

#[test]
#[ignore]
fn jobs_limits_concurrent_recipes() {
Test::new()
.args(["--jobs", "1"])
.justfile(
"
set quiet

[parallel]
foo: a b

a:
echo a
sleep 1
echo a

b:
echo b
sleep 1
echo b
",
)
.stdout_regex("(a\na\nb\nb\n|b\nb\na\na\n)")
.success();
}

#[test]
#[ignore]
fn recipes_up_to_job_limit_run_in_parallel() {
let start = Instant::now();

Test::new()
.args(["--jobs", "2"])
.justfile(
"
[parallel]
foo: a b

a:
sleep 1

b:
sleep 1
",
)
.stderr(
"
sleep 1
sleep 1
",
)
.success();

assert!(start.elapsed() < Duration::from_secs(2));
}

#[test]
fn zero_jobs_is_an_error() {
Test::new()
.args(["--jobs", "0"])
.justfile("")
.stderr(
"
error: invalid value '0' for '--jobs <N>': number would be zero for non-zero type

For more information, try '--help'.
",
)
.status(2);
}
Loading