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 = "jake"
version = "0.9.0"
version = "0.10.0"
edition = "2024"
license-file = "LICENSE"
readme = "README.md"
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ It is based on TOML tasks definition (stored in a file called `jakefile.toml`) a
- You can load `.env` file (in the same working directory or anywhere up in the directory tree), by passing the `--env` flag
- Execute scripts from a `package.json` file with the `--js` flag.
- Execute inline scripts in Python, JavaScript, TypeScript, Ruby, PHP, Lua, and Perl
- Run tasks from any directory with `--cwd <path>`, making `jake` a workspace-wide task runner
- Dry-run mode: `jake --dry-run <task>` prints commands without executing them.

## Installation
Expand Down Expand Up @@ -199,6 +200,16 @@ console.log('hey there, great to run TS!')

Supported language identifiers: `py`/`python`, `js`/`javascript`, `ts`/`typescript`, `rb`/`ruby`, `php`, `lua`, `perl`.

### Running tasks from a custom working directory

You can use `--cwd` to run tasks from a specific directory without changing your shell's current directory. This is especially useful in monorepos or multi-package workspaces:

```bash
jake build --cwd ./packages/backend
```

When `--cwd` is provided, `jake` will look for `jakefile.toml` (or `package.json` if using `--js`) in that directory and use it as the working directory for command execution.

### package.json Scripts

If you are in a JS/TS application using a `package.json` for its scripts, as in this example:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cle-does-things/jake",
"version": "0.9.0",
"version": "0.10.0",
"description": "A Make-like task executor for Unix operating systems",
"main": "start.js",
"directories": {
Expand Down
4 changes: 2 additions & 2 deletions pre-install.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ const features = process.env.npm_config_features
? `--features ${process.env.npm_config_features.replace(",", " ")}`
: "";

console.log(`Installing and compiling jake 0.9.0 ${features} ...`);
exec(`cargo install jake --vers 0.9.0 ${features}`, (error, stdout, stderr) => {
console.log(`Installing and compiling jake 0.10.0 ${features} ...`);
exec(`cargo install jake --vers 0.10.0 ${features}`, (error, stdout, stderr) => {
console.log(stdout);
if (error || stderr) {
console.log(error || stderr);
Expand Down
2 changes: 2 additions & 0 deletions reference/contents/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ additional options from the command line.
file can be executed by passing the `--js` flag
- **Inline scripts**: write scripts in Python, JavaScript, TypeScript, Ruby, PHP, Lua, and Perl
directly in `jakefile.toml` using the `script` key
- **Custom working directory**: with `--cwd`, you can point `jake` to any directory to use as the working directory for command execution, enabling workspace-wide task running
- **Dry-run**: `--dry-run` prints the commands that would be run without executing them

### Comparison
Expand All @@ -48,6 +49,7 @@ The table below compares `jake` against [`just`](https://github.com/casey/just)
| Initialize a boilerplate jake/just/makefile | ✅ | ❌ | ❌ |
| Execute inline scripts in `jakefile.toml` | ✅ | ❌ | ❌ |
| Execute scripts in `package.json` | ✅ | ❌ | ❌ |
| Custom working directory (`--cwd`) | ✅ | ❌ | ❌ |
| Dry-run (print commands only) | ✅ | ✅ | ✅ |

⚠️ `make` supports passing variables from the command line but not named options in the same
Expand Down
10 changes: 10 additions & 0 deletions reference/contents/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,16 @@ drwxr-xr-x@ 6 user staff 192 Feb 13 10:22 target

The value passed to `--options` is appended to the task's command at execution time, so `jake list --options "-la"` effectively runs `ls -la`.

**Run tasks from a custom working directory**

Use `--cwd` to point `jake` to a specific directory where `jakefile.toml` (or `package.json`) resides. This is useful in monorepos or workspace setups where you want to run tasks from a different directory without changing your shell's working directory:

```bash
jake build --cwd ./packages/backend
```

The specified directory is used as the current working directory for command execution, and `jake` will look for `jakefile.toml` (or `package.json` when using `--js`) inside it.

**Load a `.env` file and execute a task**

If a task requires an environment variable, e.g.:
Expand Down
65 changes: 51 additions & 14 deletions src/load.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::env;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use crate::code_blocks::ScriptLanguage;
Expand Down Expand Up @@ -34,30 +34,40 @@ fn resolve_jakefile_path() -> Result<String> {
))
}

pub fn parse_jakefile(file_path: Option<&str>) -> Result<Table> {
pub fn parse_jakefile(file_path: Option<&str>) -> Result<(Table, Option<String>)> {
let owned_path;
let path = match file_path {
let (path, cwd) = match file_path {
None => {
let p = resolve_jakefile_path()?;
owned_path = p;
Path::new(&owned_path)
(Path::new(&owned_path), None)
}
Some(p) => {
owned_path = p.to_string();
Path::new(&owned_path)
(
Path::new(&owned_path),
Some(PathBuf::from(&owned_path).parent().map_or(
env::current_dir()?.canonicalize()?,
|p| {
p.to_path_buf()
.canonicalize()
.expect("Should be able to canonicalize path")
},
)),
)
}
};
if path.exists() {
let content = std::fs::read_to_string(path)?;
let table = content.parse::<Table>()?;
Ok(table)
Ok((table, cwd.map(|c| c.to_string_lossy().to_string())))
} else {
Err(anyhow!("jakefile.toml does not exist"))
}
}

pub fn list_jakefile_tasks(file_path: Option<&str>) -> Result<Vec<String>> {
let parsed = parse_jakefile(file_path)?;
let (parsed, _) = parse_jakefile(file_path)?;
let mut commands: Vec<String> = vec![];

for key in parsed.keys() {
Expand Down Expand Up @@ -195,20 +205,20 @@ pub fn execute_command(
} else {
flags.split_whitespace().collect()
};
let available_tasks = parse_jakefile(jakefile_path)?;
let (available_tasks, cwd) = parse_jakefile(jakefile_path)?;
let mut execution_order: Vec<String> = vec![];
let mut state_map: HashMap<String, NodeState> = HashMap::new();
resolve_dependencies(&available_tasks, task, &mut execution_order, &mut state_map)?;
for command in &execution_order[..execution_order.len() - 1] {
executor.execute(command.as_str(), load_env)?;
executor.execute(command.as_str(), load_env, cwd.clone())?;
}
// the actual command to execute is the last one in the execution order
let cmd = &execution_order[execution_order.len() - 1];
if cmd_options.is_empty() {
executor.execute(cmd, load_env)?;
executor.execute(cmd, load_env, cwd)?;
} else {
let full_command = cmd.to_owned() + " " + cmd_options.join(" ").as_str();
executor.execute(&full_command, load_env)?;
executor.execute(&full_command, load_env, cwd)?;
}
Ok(())
}
Expand All @@ -219,7 +229,7 @@ pub fn execute_default_command(
executor: &dyn Executor,
load_env: bool,
) -> Result<()> {
let available_tasks = parse_jakefile(jakefile_path)?;
let (available_tasks, _) = parse_jakefile(jakefile_path)?;
if available_tasks.contains_key("default") {
execute_command(jakefile_path, "default", flags, executor, load_env)?;
} else {
Expand Down Expand Up @@ -251,7 +261,12 @@ mod tests {
}

impl Executor for MockCommandExecutor {
fn execute(&self, full_command: &str, _load_env: bool) -> anyhow::Result<()> {
fn execute(
&self,
full_command: &str,
_load_env: bool,
_cwd: Option<String>,
) -> anyhow::Result<()> {
std::fs::write("test.mock", full_command)?;
Ok(())
}
Expand All @@ -266,7 +281,8 @@ mod tests {
println!("An error occurred: {}", e.to_string());
assert!(false); // fail here
}
Ok(t) => {
Ok((t, c)) => {
assert!(c.is_some_and(|s| s.contains("testfiles")));
assert!(t.contains_key("say-hello"));
assert!(t.contains_key("say-hello-back"));
assert!(t.contains_key("say-bye"));
Expand Down Expand Up @@ -725,4 +741,25 @@ mod tests {
.starts_with("Command exited with a non-zero exit code:")
}));
}

#[test]
#[serial]
fn test_resolves_cwd_from_path() {
let (_, cwd) =
parse_jakefile(Some("testfiles/jakefile.toml")).expect("Should be able to resolve cwd");
let expected_cwd = PathBuf::from("./testfiles")
.canonicalize()
.expect("Should be able to canonicalize");
assert!(cwd.is_some_and(|p| {
println!("{:?} {:?}", p, expected_cwd);
PathBuf::from(p) == expected_cwd
}));
}

#[test]
#[serial]
fn test_cwd_resolves_to_none() {
let (_, cwd) = parse_jakefile(None).expect("Should be able to resolve cwd");
assert!(cwd.is_none());
}
}
49 changes: 45 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use crate::{
initialize::write_jakefile,
load::{execute_command, execute_default_command, is_posix_os, list_jakefile_tasks},
Expand All @@ -16,7 +18,7 @@ mod package_json;

/// Make-like task executor for Unix-based operating systems
#[derive(Parser, Debug)]
#[command(version = "0.9.0")]
#[command(version = "0.10.0")]
#[command(name = "jake")]
#[command(about, long_about = None)]
struct Args {
Expand Down Expand Up @@ -46,6 +48,12 @@ struct Args {
/// Print commands that would be run without executing them
#[arg(long, default_value_t = false)]
dry_run: bool,

/// Directory under which jakefile.toml can be found
/// and that should be used as current working directory
/// for the command execution
#[arg(long, default_value = None)]
cwd: Option<String>,
}

fn main() -> anyhow::Result<()> {
Expand All @@ -72,7 +80,13 @@ fn main() -> anyhow::Result<()> {
};
if args.js {
if let Some(script_name) = args.task {
execute_script(None, script_name, args.env, executor.as_ref())?;
let package_json_path = args.cwd.map(|p| {
PathBuf::from(p)
.join("package.json")
.to_string_lossy()
.to_string()
});
execute_script(package_json_path, script_name, args.env, executor.as_ref())?;
} else {
return Err(anyhow!(
"No script name provided, please provide one or, if you wish to execute the default command from jakefile.toml, do not pass the `--js` flag."
Expand All @@ -81,8 +95,35 @@ fn main() -> anyhow::Result<()> {
return Ok(());
}
match args.task {
Some(t) => execute_command(None, &t, &args.options, executor.as_ref(), args.env)?,
None => execute_default_command(None, &args.options, executor.as_ref(), args.env)?,
Some(t) => {
let jakefile_path = args.cwd.map(|p| {
PathBuf::from(p)
.join("jakefile.toml")
.to_string_lossy()
.to_string()
});
execute_command(
jakefile_path.as_deref(),
&t,
&args.options,
executor.as_ref(),
args.env,
)?
}
None => {
let jakefile_path = args.cwd.map(|p| {
PathBuf::from(p)
.join("jakefile.toml")
.to_string_lossy()
.to_string()
});
execute_default_command(
jakefile_path.as_deref(),
&args.options,
executor.as_ref(),
args.env,
)?
}
}
Ok(())
}
29 changes: 25 additions & 4 deletions src/models.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use std::collections::HashSet;
use std::{collections::HashSet, env, path::PathBuf};

use crate::env_vars::dotenv_to_hashmap;

pub trait Executor {
fn execute(&self, full_command: &str, load_env: bool) -> anyhow::Result<()>;
fn execute(
&self,
full_command: &str,
load_env: bool,
cwd: Option<String>,
) -> anyhow::Result<()>;
}

pub struct CommandExecutor;
Expand All @@ -24,21 +29,36 @@ impl DryRunExecutor {
}

impl Executor for DryRunExecutor {
fn execute(&self, full_command: &str, _load_env: bool) -> anyhow::Result<()> {
fn execute(
&self,
full_command: &str,
_load_env: bool,
_cwd: Option<String>,
) -> anyhow::Result<()> {
println!("{}", full_command);
Ok(())
}
}

impl Executor for CommandExecutor {
fn execute(&self, full_command: &str, load_env: bool) -> anyhow::Result<()> {
fn execute(
&self,
full_command: &str,
load_env: bool,
cwd: Option<String>,
) -> anyhow::Result<()> {
let current_dir = match cwd {
Some(c) => PathBuf::from(c.replace("\\", "/")).canonicalize()?,
None => env::current_dir()?,
};
let mut cmd = if !load_env {
std::process::Command::new("sh")
.arg("-c")
.arg(full_command)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.current_dir(current_dir)
.spawn()?
} else {
let env_vars = dotenv_to_hashmap()?;
Expand All @@ -49,6 +69,7 @@ impl Executor for CommandExecutor {
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.current_dir(current_dir)
.spawn()?
};
let status = cmd.wait()?;
Expand Down
Loading
Loading