First Commit

This commit is contained in:
Afonso Franco 2024-02-25 01:54:40 +00:00
commit e51561f058
Signed by: afonso
SSH key fingerprint: SHA256:JiuxZNdA5bRWXPMUJChI0AQ75yC+cXY4xM0IaVwEVys
4 changed files with 521 additions and 0 deletions

184
Cargo.lock generated Normal file
View 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
View 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
View 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
View 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);
}
}
}