First Commit
This commit is contained in:
commit
e51561f058
4 changed files with 521 additions and 0 deletions
184
Cargo.lock
generated
Normal file
184
Cargo.lock
generated
Normal file
|
@ -0,0 +1,184 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toke"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "toke"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
regex = "1.10.3"
|
||||
toml = "0.8.10"
|
77
README.md
Normal file
77
README.md
Normal file
|
@ -0,0 +1,77 @@
|
|||
# Toke - TOML-based Command runner
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Example Tokefile
|
||||
|
||||
```toml
|
||||
[vars]
|
||||
cc = "gcc"
|
||||
|
||||
[targets.build]
|
||||
cmd = "${cc} main.c -o main"
|
||||
|
||||
[targets.run]
|
||||
cmd = "./main arg1 arg2"
|
||||
deps = ["build"]
|
||||
```
|
||||
|
||||
Because TOML has several ways of defining the same structure, here is a JSON representation of the above TOML file to make it easier to understand.
|
||||
|
||||
You can write your TOML file in any way you wish as long as it's structure is in the same style as the following JSON.
|
||||
|
||||
```json
|
||||
{
|
||||
"vars": {
|
||||
"cc": "gcc"
|
||||
},
|
||||
"targets": {
|
||||
"build": {
|
||||
"cmd": "${cc} main.c -o main"
|
||||
},
|
||||
"run": {
|
||||
"cmd": "./main arg1 arg2",
|
||||
"deps": ["build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### In this example:
|
||||
|
||||
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 `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.
|
||||
|
||||
```
|
||||
$ ./toke build
|
||||
gcc main.c -o main
|
||||
```
|
||||
|
||||
# How Toke Works
|
||||
|
||||
Toke 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.
|
||||
|
||||
# Contributing
|
||||
|
||||
Contributions are welcome! Feel free to submit issues or pull requests to help improve Toke.
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
Toke is a fun experiment aiming to simplify build systems using the TOML format.
|
||||
Give it a try and see if it suits your project needs better than traditional build systems like Make!
|
250
src/main.rs
Normal file
250
src/main.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
extern crate regex;
|
||||
extern crate toml;
|
||||
|
||||
use clap::Arg;
|
||||
use regex::Regex;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::{exit, Command};
|
||||
|
||||
fn main() {
|
||||
//Check if the user has provided a tokefile filename
|
||||
let matches = clap::Command::new("toke")
|
||||
.version("1.0")
|
||||
.about("A simple command runner tool to execute targets in a tokefile")
|
||||
.arg(
|
||||
Arg::new("tokefile")
|
||||
.short('f')
|
||||
.long("file")
|
||||
.value_parser(clap::value_parser!(String))
|
||||
.help("Sets the path to the tokefile"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("target")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("Sets the target to execute"),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
// Get the target command to execute
|
||||
let target = match matches.get_one::<String>("target") {
|
||||
Some(target) => target,
|
||||
None => {
|
||||
eprintln!("No target provided");
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let file_path = match matches.get_one::<String>("tokefile") {
|
||||
Some(file_path) => file_path.to_owned(),
|
||||
None => {
|
||||
let file_names: Vec<String> =
|
||||
vec!["tokefile", "Tokefile", "tokefile.toml", "Tokefile.toml"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
let tokefile_option =
|
||||
file_names
|
||||
.iter()
|
||||
.find_map(|file_name| match Path::new(file_name).exists() {
|
||||
true => Some(file_name.to_owned()),
|
||||
false => None,
|
||||
});
|
||||
tokefile_option.expect("No tokefile found")
|
||||
}
|
||||
};
|
||||
let tokefile_contents = match fs::read_to_string(file_path) {
|
||||
Ok(contents) => contents.to_string(),
|
||||
Err(err) => {
|
||||
eprintln!("No tokefile found: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Read the contents of the tokefile
|
||||
//Create all the possible file names
|
||||
//Iterate over the file names and try to read the file
|
||||
|
||||
// Parse the tokefile into a TOML Value
|
||||
let parsed_toml = match toml::from_str::<toml::Value>(&tokefile_contents) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
eprintln!("Error parsing tokefile: {}", err);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// Determine if there are dependency cycles in the tokefile
|
||||
detect_cycle(&parsed_toml);
|
||||
|
||||
// Check if the target exists
|
||||
if parsed_toml
|
||||
.get("targets")
|
||||
.and_then(|targets| targets.get(&target).and_then(|t| t.get("cmd")))
|
||||
.is_none()
|
||||
{
|
||||
eprintln!("Target '{}' not found in tokefile", target);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Execute the target command
|
||||
run_command_caller(parsed_toml.clone(), &replaced_commands, target.to_string());
|
||||
}
|
||||
fn detect_cycle(parsed_toml: &toml::Value) {
|
||||
let mut visited_targets = HashSet::new();
|
||||
let empty_table = toml::value::Table::new();
|
||||
let targets = parsed_toml
|
||||
.get("targets")
|
||||
.and_then(|targets| targets.as_table())
|
||||
.unwrap_or(&empty_table);
|
||||
|
||||
for (target_name, _) in targets.iter() {
|
||||
detect_cycle_recursive(&parsed_toml, target_name, &mut visited_targets);
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_cycle_recursive(
|
||||
parsed_toml: &toml::Value,
|
||||
target: &str,
|
||||
visited_targets: &mut HashSet<String>,
|
||||
) {
|
||||
if visited_targets.contains(target) {
|
||||
eprintln!("Cycle detected: {}", target);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
visited_targets.insert(target.to_string());
|
||||
|
||||
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() {
|
||||
detect_cycle_recursive(parsed_toml, dep_str, visited_targets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
replaced_targets
|
||||
})
|
||||
.unwrap_or(toml::value::Table::new());
|
||||
|
||||
toml::Value::Table(replaced_commands)
|
||||
}
|
||||
|
||||
fn replace_variables_in_cmd(cmd: &str, vars: &toml::value::Table) -> String {
|
||||
let mut replaced_cmd = cmd.to_string();
|
||||
|
||||
// Regular expression to match variable instances like "${var_name}"
|
||||
let re = Regex::new(r#"\$\{([^}]+)\}"#).unwrap();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
eprintln!("Target '{}' not found in tokefile", target);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue