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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cargo-q"
version = "0.1.3"
version = "0.1.4"
edition = "2021"
description = "A cargo subcommand for running multiple cargo commands in a time"
keywords = ["cargo", "subcommand", "plugin"]
Expand Down
76 changes: 54 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,80 @@
# cargo-q

Cargo subcommand to run multiple Cargo commands in a time.
A Cargo subcommand that allows running multiple Cargo commands in a time.

<details>
<summary>TODO</summary>

- ✅ Add sequential execution
- ✅ Add ; as command separator
- ✅ Add & as command separator
- ❌ Add > as command separator
- ❌ Add parallel execution
- ✅ Add ; as command separator for independent commands
- ✅ Add & as command separator for dependent commands
- ✅ Add parallel execution between independent commands
- ❌ Add > as command separator for dependent commands
- ❌ Support mixed separators

</details>

## Installation

```bash
cargo install cargo-q
```

## Features

- Run multiple Cargo commands sequentially
- Use different separators for command execution:
- Space: Run commands sequentially (independent execution)
- `;`: Run independent commands sequentially
- `&`: Run commands with dependencies (each command depends on previous command's success)
- Support parallel execution for independent commands
- Verbose mode for detailed output

## Usage

### Run a command
### Run a Single Command

```bash
cargo q cmd
cargo q check
```

### Run multiple commands
### Run Multiple Commands

#### Sequential Execution (Space Separator)
```bash
# default quiet mode
cargo q "check test" # run `check` first then test whether `check` is successful
cargo q 'check test' # ' and " are the same
cargo q "test --features feature1 ; run" # if a command has dash or parameters, use ; as separator
# Run commands sequentially and independently
cargo q "check test" # Runs check, then test
cargo q 'check test' # Single and double quotes both work
```

cargo q "check & test & run" # run `check` first, then `test` if `check` is successful, and `run` if both are successful
cargo q "check&test&run" # same as above
#### Independent Commands (`;` Separator)
```bash
# Run commands sequentially and independently
cargo q "test --features feature1 ; run" # Commands with parameters need ; separator
```

#### Dependent Commands (`&` Separator)
```bash
# Run commands with explicit dependencies
cargo q "check & test & run" # Each command runs only if previous command succeeds
cargo q "check&test&run" # Spaces around & are optional
```

cargo q "test > analyze" # run `test` first, then `analyze` with `test`'s output
cargo q "test>analyze" # same as above
### Parallel Execution

# verbose mode
cargo q -v "check test" # run `check` first, then `test` if `check` is successful
cargo q --verbose "check test" # same as above
```bash
# Run independent commands in parallel
cargo q -p "build -r; build" # Run both commands in parallel
cargo q --parallel "check; test" # Same as above
```

### Run commands in parallel
### Verbose Output

```bash
cargo q -p "build -r; build" # run `build -r` and `build` in parallel
cargo q --parallel "build -r; build" # same as above
cargo q -v "check test" # Show detailed output
cargo q --verbose "check test" # Same as above
```

## License

Licensed under Apache-2.0 license ([LICENSE](LICENSE) or http://opensource.org/licenses/Apache-2.0)
59 changes: 54 additions & 5 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use crate::parser::Strategy;
use crate::process::{ColorExt, ExecutionSummary};
use crate::routine::Routine;
use crate::thread_pool::ThreadPool;
use std::io::{self, Error, ErrorKind};
use std::sync::{Arc, Mutex};

const MAX_THREADS: usize = 8;

pub(crate) struct Executor {
pub(super) parallel: bool,
Expand All @@ -21,13 +25,58 @@ impl Executor {
}

pub fn execute(&self) -> io::Result<()> {
if self.parallel {
return Err(Error::new(
ErrorKind::Unsupported,
"Parallel execution not yet implemented",
));
match (self.parallel, self.strategy) {
(true, Strategy::Independent) => self.execute_parallel(),
(true, _) => Err(Error::new(
ErrorKind::InvalidInput,
"Parallel execution only supports independent commands now",
)),
(false, _) => self.execute_sequential(),
}
}

fn execute_parallel(&self) -> io::Result<()> {
let summary = Arc::new(Mutex::new(ExecutionSummary::new(self.routines.len())));
let total_commands = self.routines.len();
let pool = ThreadPool::new(total_commands.min(MAX_THREADS));

for (idx, cmd) in self.routines.iter().enumerate() {
let summary = Arc::clone(&summary);
let cmd_str = if cmd.args.is_empty() {
cmd.name.clone()
} else {
format!("{} {}", cmd.name, cmd.args.join(" "))
};

println!(
"\n {} {}",
format!("[{}/{}]", idx + 1, total_commands).bold(),
cmd_str
);

let cmd = cmd.clone();
let verbose = self.verbose;

pool.execute(move || match cmd.run(verbose) {
Ok((success, output)) => {
if success {
summary.lock().unwrap().increment_success();
} else if !output.stderr.is_empty() {
eprintln!("error: Command failed");
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
}
Err(e) => {
eprintln!("error: Failed to execute command: {}", e);
}
});
}

// Pool will be dropped here, which waits for all jobs to complete
Ok(())
}

fn execute_sequential(&self) -> io::Result<()> {
let total_commands = self.routines.len();
let mut summary = ExecutionSummary::new(total_commands);

Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod executor;
mod parser;
mod process;
mod routine;
mod thread_pool;

use cli::Cli;
use parser::Parser;
Expand Down
2 changes: 1 addition & 1 deletion src/routine.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io;
use std::process::{Command, Output, Stdio};

#[derive(Debug, Default)]
#[derive(Debug, Default, Clone)]
pub(crate) struct Routine {
pub(crate) name: String,
pub(crate) args: Vec<String>,
Expand Down
76 changes: 76 additions & 0 deletions src/thread_pool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};

type Job = Box<dyn FnOnce() + Send + 'static>;

pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<Sender<Job>>,
}

struct Worker {
_id: usize,
thread: Option<JoinHandle<()>>,
}

impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);

let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);

for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}

ThreadPool {
workers,
sender: Some(sender),
}
}

pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}

impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());

for worker in &mut self.workers {
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}

impl Worker {
fn new(id: usize, receiver: Arc<Mutex<Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || loop {
let message = receiver.lock().unwrap().recv();

match message {
Ok(job) => {
job();
}
Err(_) => {
break;
}
}
});

Worker {
_id: id,
thread: Some(thread),
}
}
}
Loading