Compare commits
3 commits
34e56d2bdd
...
7b5833675e
Author | SHA1 | Date | |
---|---|---|---|
7b5833675e | |||
27e953e62b | |||
12107671e8 |
3 changed files with 189 additions and 97 deletions
83
README.md
83
README.md
|
@ -3,10 +3,28 @@
|
|||
Toke (TOML Make) is a simple command runner inspired by Make but uses TOML (Tom's Obvious, Minimal Language) instead of Makefiles.
|
||||
This project was created for fun as an alternative to Make, because I was looking for a simple command runner/build system but didn't like the Makefile syntax.
|
||||
|
||||
# How to Use Toke
|
||||
# How to use toke
|
||||
|
||||
Toke works by reading a TOML file named `tokefile.toml` (or any variation of it like `Tokefile.toml`, `tokefile`, etc.) in your project directory. This file contains definitions of variables, targets, and their respective commands.
|
||||
|
||||
You can also pass in a file to be used instead of the default one.
|
||||
|
||||
```sh
|
||||
toke -f my_custom_named_toke_file
|
||||
```
|
||||
|
||||
To run a toke target, just run `toke target_name_here`
|
||||
|
||||
For example:
|
||||
|
||||
```sh
|
||||
toke build
|
||||
```
|
||||
|
||||
Toke then reads the TOML file, resolves variables, and then executes the specified commands for the target you provide.
|
||||
|
||||
It also checks for dependency cycles in your targets to prevent infinite loops.
|
||||
|
||||
## Example Tokefile
|
||||
|
||||
```toml
|
||||
|
@ -14,6 +32,7 @@ Toke works by reading a TOML file named `tokefile.toml` (or any variation of it
|
|||
cc = "gcc"
|
||||
|
||||
[targets.build]
|
||||
vars.cc = "clang"
|
||||
cmd = "${cc} main.c -o main"
|
||||
|
||||
[targets.run]
|
||||
|
@ -32,6 +51,9 @@ You can write your TOML file in any way you wish as long as it's structure is in
|
|||
},
|
||||
"targets": {
|
||||
"build": {
|
||||
"vars": {
|
||||
"cc": "clang"
|
||||
},
|
||||
"cmd": "${cc} main.c -o main"
|
||||
},
|
||||
"run": {
|
||||
|
@ -48,22 +70,65 @@ We define a variable `cc` which is set to `"gcc"`.
|
|||
|
||||
We have two targets: `build` and `run`.
|
||||
|
||||
The `build` target compiles the code with gcc.
|
||||
The `build` target compiles the code with clang.
|
||||
|
||||
The `run` target runs the code with some arguments. It also depends on the `build` target.
|
||||
|
||||
To run a specific target, simply pass its name as an argument when running the Toke program.
|
||||
# Variables
|
||||
|
||||
```
|
||||
$ ./toke build
|
||||
gcc main.c -o main
|
||||
## Global variables
|
||||
|
||||
You can define global variables in the vars table, as seen in the above example.
|
||||
|
||||
## Local variables
|
||||
|
||||
You can specify local variables for each target. These local variables are defined under the `vars` key within each target section. If the local variable name matches a global variable, it will overwrite the global variable value for that specific target.
|
||||
|
||||
Here is an example:
|
||||
|
||||
```toml
|
||||
[vars]
|
||||
cc = "g++"
|
||||
|
||||
[target.target1]
|
||||
vars.cc = "gcc"
|
||||
cmd="${cc} ${cflags} main.c -o main"
|
||||
|
||||
[target.target2]
|
||||
vars.cc = "clang"
|
||||
vars.cflags = "-Wall"
|
||||
cmd="${cc} ${cflags} main.c -o main"
|
||||
|
||||
[target.target3]
|
||||
vars.cflags = "-O3"
|
||||
cmd="${cc} ${cflags} main.c -o main"
|
||||
```
|
||||
|
||||
# How Toke Works
|
||||
In this example:
|
||||
|
||||
Toke reads the TOML file, resolves variables, and then executes the specified commands for the target you provide.
|
||||
`target1` uses `gcc` for the `cc` variable, overriding the global value.
|
||||
|
||||
It also checks for dependency cycles in your targets to prevent infinite loops.
|
||||
`target2` specifies `clang` for the `cc` variable and adds `-Wall` to `cflags`.
|
||||
|
||||
`target3` only sets `cflags` to `-O3`.
|
||||
|
||||
## Command line overrides
|
||||
|
||||
Additionally, you can override both global and local variables via command line arguments when invoking `toke`. Command line arguments follow the format `VARIABLE=value`. When provided, these values will overwrite any corresponding global or local variables.
|
||||
|
||||
Here's an example of using command line arguments:
|
||||
|
||||
```sh
|
||||
toke build CC=gcc CFLAGS=-O2
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
`CC=gcc` overrides the value of the `cc` variable.
|
||||
|
||||
`CFLAGS=-O2` overrides the value of the `cflags` variable.
|
||||
|
||||
These overrides allow for flexible customization.
|
||||
|
||||
# Contributing
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
cc = "gcc"
|
||||
|
||||
[targets.build]
|
||||
vars.cc = "gcc"
|
||||
cmd = "${cc} main.c -o main"
|
||||
|
||||
[targets.run]
|
||||
|
|
202
src/main.rs
202
src/main.rs
|
@ -3,10 +3,11 @@ extern crate toml;
|
|||
|
||||
use clap::Arg;
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::{exit, Command};
|
||||
use toml::map::Map;
|
||||
|
||||
fn main() {
|
||||
//Check if the user has provided a tokefile filename
|
||||
|
@ -26,6 +27,13 @@ fn main() {
|
|||
.required(true)
|
||||
.help("Sets the target to execute"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("variables")
|
||||
.value_delimiter(' ')
|
||||
.num_args(1..)
|
||||
.index(2)
|
||||
.help("User-defined variables in the format KEY=VALUE (these override variables in the tokefile)"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
// Get the target command to execute
|
||||
|
@ -41,7 +49,7 @@ fn main() {
|
|||
Some(file_path) => file_path.to_owned(),
|
||||
None => {
|
||||
let file_names: Vec<String> =
|
||||
vec!["tokefile", "Tokefile", "tokefile.toml", "Tokefile.toml"]
|
||||
["tokefile", "Tokefile", "tokefile.toml", "Tokefile.toml"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
@ -55,6 +63,7 @@ fn main() {
|
|||
tokefile_option.expect("No tokefile found")
|
||||
}
|
||||
};
|
||||
|
||||
let tokefile_contents = match fs::read_to_string(file_path) {
|
||||
Ok(contents) => contents.to_string(),
|
||||
Err(err) => {
|
||||
|
@ -63,12 +72,25 @@ fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
// Read the contents of the tokefile
|
||||
//Create all the possible file names
|
||||
//Iterate over the file names and try to read the file
|
||||
let cli_vars = match matches.get_many::<String>("variables") {
|
||||
Some(vars) => vars.collect::<Vec<_>>(),
|
||||
None => vec![],
|
||||
};
|
||||
// Split the variables into a HashMap
|
||||
let cli_vars: HashMap<String, String> = cli_vars
|
||||
.iter()
|
||||
.map(|var| {
|
||||
let parts: Vec<&str> = var.split('=').collect();
|
||||
if parts.len() != 2 {
|
||||
eprintln!("Invalid variable format: {}", var);
|
||||
exit(1);
|
||||
}
|
||||
(parts[0].to_string(), parts[1].to_string())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Parse the tokefile into a TOML Value
|
||||
let parsed_toml = match toml::from_str::<toml::Value>(&tokefile_contents) {
|
||||
let mut parsed_toml = match toml::from_str::<toml::Value>(&tokefile_contents) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("Error parsing tokefile: {}", err);
|
||||
|
@ -76,14 +98,8 @@ fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
// Extract variables from the tokefile
|
||||
let empty_table = toml::value::Table::new();
|
||||
let binding = parsed_toml.to_owned();
|
||||
let get = binding.get("vars");
|
||||
let vars = get.and_then(|vars| vars.as_table()).unwrap_or(&empty_table);
|
||||
|
||||
// Replace variable instances in commands
|
||||
let replaced_commands = replace_variables(parsed_toml.clone(), vars);
|
||||
replace_variables(&mut parsed_toml, cli_vars);
|
||||
|
||||
// Determine if there are dependency cycles in the tokefile
|
||||
detect_cycle(&parsed_toml);
|
||||
|
@ -91,7 +107,7 @@ fn main() {
|
|||
// Check if the target exists
|
||||
if parsed_toml
|
||||
.get("targets")
|
||||
.and_then(|targets| targets.get(&target).and_then(|t| t.get("cmd")))
|
||||
.and_then(|targets| targets.get(target))
|
||||
.is_none()
|
||||
{
|
||||
eprintln!("Target '{}' not found in tokefile", target);
|
||||
|
@ -99,7 +115,7 @@ fn main() {
|
|||
}
|
||||
|
||||
// Execute the target command
|
||||
run_command_caller(parsed_toml.clone(), &replaced_commands, target.to_string());
|
||||
run_command(&parsed_toml, target.to_string());
|
||||
}
|
||||
fn detect_cycle(parsed_toml: &toml::Value) {
|
||||
let mut visited_targets = HashSet::new();
|
||||
|
@ -110,7 +126,7 @@ fn detect_cycle(parsed_toml: &toml::Value) {
|
|||
.unwrap_or(&empty_table);
|
||||
|
||||
for (target_name, _) in targets.iter() {
|
||||
detect_cycle_recursive(&parsed_toml, target_name, &mut visited_targets);
|
||||
detect_cycle_recursive(parsed_toml, target_name, &mut visited_targets);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,29 +162,63 @@ fn detect_cycle_recursive(
|
|||
visited_targets.remove(target);
|
||||
}
|
||||
|
||||
fn replace_variables(parsed_toml: toml::Value, vars: &toml::value::Table) -> toml::Value {
|
||||
let replaced_commands = parsed_toml
|
||||
.get("targets")
|
||||
.map(|targets| {
|
||||
let mut replaced_targets = toml::value::Table::new();
|
||||
if let Some(targets) = targets.as_table() {
|
||||
for (target_name, target) in targets.iter() {
|
||||
if let Some(target_table) = target.as_table() {
|
||||
if let Some(cmd_value) = target_table.get("cmd") {
|
||||
if let Some(cmd_str) = cmd_value.as_str() {
|
||||
let replaced_cmd = replace_variables_in_cmd(cmd_str, vars);
|
||||
replaced_targets
|
||||
.insert(target_name.clone(), toml::Value::String(replaced_cmd));
|
||||
}
|
||||
}
|
||||
}
|
||||
fn replace_variables(parsed_toml: &mut toml::Value, cli_vars: HashMap<String, String>) {
|
||||
// Parse global variables
|
||||
let map = toml::value::Table::new();
|
||||
let value = &parsed_toml.clone();
|
||||
let get = value.get("vars");
|
||||
let global_vars = get.and_then(|vars| vars.as_table()).unwrap_or(&map);
|
||||
|
||||
// Get the targets table or return an error if it doesn't exist
|
||||
let targets = match parsed_toml
|
||||
.get_mut("targets")
|
||||
.and_then(|targets| targets.as_table_mut())
|
||||
{
|
||||
Some(targets) => targets,
|
||||
None => {
|
||||
eprintln!("No targets found in tokefile");
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over each target
|
||||
for (_, target_value) in targets.iter_mut() {
|
||||
if let Some(target_table) = target_value.as_table_mut() {
|
||||
// Parse local variables for the target
|
||||
let map = toml::value::Table::new();
|
||||
let local_vars = target_table
|
||||
.get("vars")
|
||||
.and_then(|vars| vars.as_table())
|
||||
.unwrap_or(&map);
|
||||
|
||||
// Merge global and local variables
|
||||
let mut merged_vars = merge_vars(global_vars, local_vars);
|
||||
|
||||
// Override variables if they were provided via the CLI
|
||||
for (key, value) in cli_vars.iter() {
|
||||
merged_vars.insert(key.clone(), toml::Value::String(value.clone()));
|
||||
}
|
||||
|
||||
// Replace variables in the target's cmd value
|
||||
if let Some(cmd_value) = target_table.get_mut("cmd") {
|
||||
if let Some(cmd_str) = cmd_value.as_str() {
|
||||
*cmd_value =
|
||||
toml::Value::String(replace_variables_in_cmd(cmd_str, &merged_vars));
|
||||
}
|
||||
}
|
||||
replaced_targets
|
||||
})
|
||||
.unwrap_or(toml::value::Table::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toml::Value::Table(replaced_commands)
|
||||
fn merge_vars(
|
||||
global_vars: &Map<String, toml::Value>,
|
||||
local_vars: &Map<String, toml::Value>,
|
||||
) -> toml::value::Table {
|
||||
let mut merged_vars = global_vars.clone();
|
||||
for (key, value) in local_vars.iter() {
|
||||
merged_vars.insert(key.clone(), value.clone());
|
||||
}
|
||||
merged_vars
|
||||
}
|
||||
|
||||
fn replace_variables_in_cmd(cmd: &str, vars: &toml::value::Table) -> String {
|
||||
|
@ -177,7 +227,7 @@ fn replace_variables_in_cmd(cmd: &str, vars: &toml::value::Table) -> String {
|
|||
// Regular expression to match variable instances like "${var_name}"
|
||||
let re = Regex::new(r#"\$\{([^}]+)\}"#).unwrap();
|
||||
|
||||
for capture in re.captures_iter(&cmd) {
|
||||
for capture in re.captures_iter(cmd) {
|
||||
if let Some(var_name) = capture.get(1).map(|m| m.as_str()) {
|
||||
if let Some(var_value) = vars.get(var_name).and_then(|v| v.as_str()) {
|
||||
replaced_cmd = replaced_cmd.replace(&format!("${{{}}}", var_name), var_value);
|
||||
|
@ -188,63 +238,39 @@ fn replace_variables_in_cmd(cmd: &str, vars: &toml::value::Table) -> String {
|
|||
replaced_cmd
|
||||
}
|
||||
|
||||
fn run_command_caller(parsed_toml: toml::Value, commands: &toml::Value, target: String) {
|
||||
//Create a hashset to keep track of visited targets
|
||||
let visited_targets = HashSet::new();
|
||||
run_command(parsed_toml, commands, target, &visited_targets);
|
||||
}
|
||||
|
||||
fn run_command(
|
||||
parsed_toml: toml::Value,
|
||||
commands: &toml::Value,
|
||||
target: String,
|
||||
visited_targets: &HashSet<String>,
|
||||
) {
|
||||
//Check if the target exists
|
||||
match commands.get(target.clone()) {
|
||||
Some(some_target) => {
|
||||
//Execute it's dependencies first, by order of appearance
|
||||
if let Some(target) = parsed_toml
|
||||
.get("targets")
|
||||
.and_then(|targets| targets.get(&target))
|
||||
{
|
||||
if let Some(target_table) = target.as_table() {
|
||||
if let Some(dep_value) = target_table.get("deps") {
|
||||
if let Some(dep_array) = dep_value.as_array() {
|
||||
for dep in dep_array {
|
||||
if let Some(dep_str) = dep.as_str() {
|
||||
run_command(
|
||||
parsed_toml.clone(),
|
||||
commands,
|
||||
dep_str.to_string(),
|
||||
visited_targets,
|
||||
);
|
||||
}
|
||||
}
|
||||
fn run_command(parsed_toml: &toml::Value, target: String) {
|
||||
if let Some(targets) = parsed_toml
|
||||
.get("targets")
|
||||
.and_then(|targets| targets.as_table())
|
||||
{
|
||||
if let Some(target_table) = targets.get(&target) {
|
||||
if let Some(dep_value) = target_table.get("deps") {
|
||||
if let Some(dep_array) = dep_value.as_array() {
|
||||
for dep in dep_array {
|
||||
if let Some(dep_str) = dep.as_str() {
|
||||
run_command(parsed_toml, dep_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(cmd) = some_target.as_str() {
|
||||
eprintln!("{}", cmd);
|
||||
let status = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd)
|
||||
.status()
|
||||
.expect("Failed to execute command");
|
||||
if !status.success() {
|
||||
eprintln!(
|
||||
"Command '{}' failed with exit code {:?}",
|
||||
cmd,
|
||||
status.code()
|
||||
);
|
||||
exit(1);
|
||||
if let Some(cmd_value) = target_table.get("cmd") {
|
||||
if let Some(cmd_str) = cmd_value.as_str() {
|
||||
eprintln!("{}", cmd_str);
|
||||
let status = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(cmd_str)
|
||||
.status()
|
||||
.expect("Failed to execute command");
|
||||
if !status.success() {
|
||||
eprintln!(
|
||||
"Command '{}' failed with exit code {:?}",
|
||||
cmd_str,
|
||||
status.code()
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
eprintln!("Target '{}' not found in tokefile", target);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue