commit e51561f0589d901b1eff4364c6aed5a612fc4eb3 Author: afonso Date: Sun Feb 25 01:54:40 2024 +0000 First Commit diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..98016b2 --- /dev/null +++ b/Cargo.lock @@ -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", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e42f20a --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md new file mode 100644 index 0000000..36add4d --- /dev/null +++ b/README.md @@ -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! diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0d830f7 --- /dev/null +++ b/src/main.rs @@ -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::("target") { + Some(target) => target, + None => { + eprintln!("No target provided"); + exit(1); + } + }; + + let file_path = match matches.get_one::("tokefile") { + Some(file_path) => file_path.to_owned(), + None => { + let file_names: Vec = + 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::(&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, +) { + 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, +) { + //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); + } + } +}