diff --git a/.github/workflows/run.yml b/.github/workflows/run.yml new file mode 100644 index 0000000..3616379 --- /dev/null +++ b/.github/workflows/run.yml @@ -0,0 +1,43 @@ +--- +name: Test run + +on: + pull_request: + branches: ["main", "develop"] + +env: + CARGO_TERM_COLOR: always + +jobs: + run-linux: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: ./install_linux.sh + + - name: run + run: cargo run -- --name testrun + + run-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: .\install_win.bat + + - name: run + run: cargo run -- --name testrun + + run-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: ./install_macos.sh + + - name: run + run: cargo run -- --name testrun diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f305fbe..3b18f7a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,20 +1,32 @@ -name: Rust +--- +name: Test build on: - pull_request: - branches: [ "main", "develop" ] + pull_request: + branches: ["main", "develop"] env: - CARGO_TERM_COLOR: always + CARGO_TERM_COLOR: always jobs: - build: + build-linux: + runs-on: ubuntu-latest - runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose --release + build-windows: + runs-on: windows-latest - steps: - - uses: actions/checkout@v3 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose --release + build-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + - name: Build + run: cargo build --verbose --release diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml new file mode 100644 index 0000000..a2a55ba --- /dev/null +++ b/.github/workflows/todo.yml @@ -0,0 +1,41 @@ +--- +name: Create issues from TODOs + +on: + workflow_dispatch: + inputs: + importAll: + default: false + required: false + type: boolean + description: Enable, if you want to import all TODOs. Runs on checked out branch! Only use if you're sure what you are doing. + push: + branches: + - develop + +permissions: + issues: write + repository-projects: read + contents: read + +jobs: + todos: + runs-on: ubuntu-latest + permissions: + issues: write + repository-projects: read + contents: read + + steps: + - uses: actions/checkout@v3 + + - name: Run Issue Bot + uses: derjuulsn/todo-issue@main + with: + excludePattern: "^(node_modules/)" + blobLines: 7 + blobLinesBefore: 4 + keywords: "FIXME,BUG,WONTFIX,TODO,WARN,HACK,PERF,OPTIM,NOTE" + reopenClosed: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 425b757..64af48f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,71 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "argmap" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c007f456524f3f1e06e8929963425b5dadf8616d9110ea0809840c16997994e9" + [[package]] name = "cmake-init" version = "0.1.0" +dependencies = [ + "argmap", + "struct-field-names-as-array", +] + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "struct-field-names-as-array" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ba4bae771f9cc992c4f403636c54d2ef13acde6367583e99d06bb336674dd9" +dependencies = [ + "struct-field-names-as-array-derive", +] + +[[package]] +name = "struct-field-names-as-array-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2dbf8b57f3ce20e4bb171a11822b283bdfab6c4bb0fe64fa729f045f23a0938" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml index 97ab654..bd12fbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,5 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +argmap = "1.1.2" +struct-field-names-as-array = "0.3.0" diff --git a/install_linux.sh b/install_linux.sh new file mode 100755 index 0000000..665c4e5 --- /dev/null +++ b/install_linux.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# building cmake-init +cargo build --release + +# creating necessary directories +mkdir -p "$HOME/.local/bin/" +mkdir -p "$HOME/.local/share/cmake-init/" + + +# copying files +cp ./target/release/cmake-init ~/.local/bin/ +cp -r ./templates ~/.local/share/cmake-init/ + +echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc + +echo "Installation complete!" diff --git a/install_macos.sh b/install_macos.sh new file mode 100755 index 0000000..37f1819 --- /dev/null +++ b/install_macos.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +# building cmake-init +cargo build --release + +# creating necessary directories +mkdir -p "$HOME/.local/bin/" +mkdir -p "$HOME/Library/Application Support/cmake-init/templates" + + +# copying files +cp ./target/release/cmake-init "$HOME/.local/bin/" +cp -r ./templates "$HOME/Library/Application Support/cmake-init/" + +echo "export PATH=$PATH:~/.local/bin" >> ~/.bashrc + +echo "Installation complete!" diff --git a/install_win.bat b/install_win.bat new file mode 100755 index 0000000..9a8972c --- /dev/null +++ b/install_win.bat @@ -0,0 +1,15 @@ +@echo off + +cargo build --release + +mkdir "%APPDATA%\\cmake-init\\templates" +xcopy /s /y "templates" "%APPDATA%\\cmake-init\\templates" + + +REM cope target\release\cmake-init.exe to %USERPROFILE%\bin +mkdir "%USERPROFILE%\\bin" +xcopy /s /y "target\\release\\cmake-init.exe" "%USERPROFILE%\\bin\\cmake-init.exe" + + +REM add %USERPROFILE%\bin to PATH +setx PATH "%USERPROFILE%\\bin;%PATH%" diff --git a/src/args/args.rs b/src/args/args.rs new file mode 100644 index 0000000..8932a3f --- /dev/null +++ b/src/args/args.rs @@ -0,0 +1,169 @@ +use std::{collections::HashMap, process::exit}; +use struct_field_names_as_array::FieldNamesAsArray; + +use crate::args::languages::Languages; + +/// # Args struct +/// +/// This struct is used to parse the arguments passed to the program. +/// +/// ## Fields +/// +/// * `name` - The name of the project +/// * `cmake_min_version` - The minimum version of CMake to use +/// * `lang` - The language chosen for the project +/// * `templates_dir` - The directory containing the template +#[derive(Debug, PartialEq, FieldNamesAsArray)] +pub struct Args { + pub name: String, + pub cmake_min_version: String, + pub lang: Languages, + pub templates_dir: String, +} + +impl Args { + /// Create a new Args struct from the given argument map. + /// If any of the required arguments are missing, print an error and exit. + /// + /// # Arguments + /// + /// * `argv` - The argument map. + pub fn from(argv: HashMap>) -> Args { + Args::validate_args(&argv); + let known_args = Args::FIELD_NAMES_AS_ARRAY; + + for (key, _) in &argv { + if !known_args.contains(&key.as_str()) { + eprintln!("Unknown argument: {}", key); + exit(1); + } + } + + Args { + name: Args::get_arg(&argv, "name", true, None), + cmake_min_version: Args::get_arg(&argv, "cmake-version", false, Some("3.0")), + lang: Languages::from_string(Args::get_arg(&argv, "lang", false, Some("c"))), + templates_dir: Args::get_template(&argv), + } + } + + /// Get the template directory path. + /// If the user has not specified a template directory, use the default. + /// The default template directory is platform dependent. + /// + /// # Arguments + /// + /// * `argv` - The argument map. + fn get_template(argv: &HashMap>) -> String { + #[cfg(target_os = "linux")] + let templates_dir = Args::get_arg( + &argv, + "templates-dir", + false, + Some( + format!( + "{}/.local/share/cmake-init/templates", + std::env::var("HOME").unwrap() + ) + .as_str(), + ), + ); + + #[cfg(target_os = "windows")] + let templates_dir = Args::get_arg( + &argv, + "templates-dir", + false, + Some( + format!( + "{}\\cmake-init\\templates", + std::env::var("APPDATA").unwrap() + ) + .as_str(), + ), + ); + + #[cfg(target_os = "macos")] + let templates_dir = Args::get_arg( + &argv, + "templates-dir", + false, + Some( + format!( + "{}/Library/Application Support/cmake-init/templates", + std::env::var("HOME").unwrap() + ) + .as_str(), + ), + ); + + templates_dir + } + + /// Get the value of the argument at the given index. + /// If the argument is not found, print an error and exit. + /// + /// # Arguments + /// + /// * `argv` - The argument map. + /// * `index` - The index of the argument to get. + pub fn get_arg( + argv: &HashMap>, + index: &str, + required: bool, + default: Option<&str>, + ) -> String { + let default = &default.unwrap_or("").to_string(); + + match argv.get(index).unwrap_or(&vec![]).get(0) { + Some(expr) => expr.to_owned(), + None => { + if required { + eprintln!("{} is required.", index); + exit(1); + } else { + default.to_string() + } + } + } + } + + /// Check the health of the arguments. + /// If there are no arguments, print an error and exit. + /// + /// # Arguments + /// + /// * `argv` - The argument map. + fn validate_args(argv: &HashMap>) { + if argv.len() == 0 { + Args::print_help(); + exit(1); + } else if argv.get("help").is_some() || argv.get("h").is_some() { + Args::print_help(); + exit(0); + } else if argv.get("version").is_some() || argv.get("v").is_some() { + Args::print_version(); + exit(0); + } + } + + /// Print the help message. + /// This is called when the user passes the `--help` flag. + fn print_help() { + println!("Usage: cmake-init --name="); + println!(); + println!("Options:"); + println!(" --name The name of the project."); + println!(" --cmake-version The minimum version of CMake to use."); + println!(" --lang The language chosen for the project(cpp, c)."); + println!(" --templates-dir The directory containing the templates."); + println!(" --help | -h Print this help message."); + println!(" --version | -v Print the version of cmake-init."); + } + + /// Print the version of cmake-init. + /// This is called when the user passes the `--version` flag. + pub fn print_version() { + println!("cmake-init {}", env!("CARGO_PKG_VERSION")); + } +} diff --git a/src/args/languages.rs b/src/args/languages.rs new file mode 100644 index 0000000..706278e --- /dev/null +++ b/src/args/languages.rs @@ -0,0 +1,42 @@ +use std::process::exit; + +/// Enum for the languages that can be used. +/** NOTE: Currently only C and C++ are supported. */ +#[derive(Debug, PartialEq)] +pub enum Languages { + C, + CPP, +} + +impl Languages { + /// Create a new Languages enum from the given string. + /// If the string is not a valid language, print an error and exit. + /// + /// # Arguments + /// + /// * `name` - The name of the language to create. + pub fn from_string(name: String) -> Languages { + match &name as &str { + "c" => Languages::C, + "cpp" => Languages::CPP, + _ => { + eprintln!("{} is not a valid language.", name); + exit(1); + } + } + } +} + +impl ToString for Languages { + /// Convert the language to a string. + /// + /// # Returns + /// + /// * A string representing the language. + fn to_string(&self) -> String { + match self { + Languages::C => String::from("main.c"), + Languages::CPP => String::from("main.cpp"), + } + } +} diff --git a/src/args/mod.rs b/src/args/mod.rs new file mode 100644 index 0000000..3a824d5 --- /dev/null +++ b/src/args/mod.rs @@ -0,0 +1,5 @@ +mod args; +mod languages; + +// exports +pub use args::Args; diff --git a/src/main.rs b/src/main.rs index e7a11a9..a9a93bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,16 @@ +mod args; +mod template; + +use argmap; + +use crate::args::Args; +use crate::template::Template; + fn main() { - println!("Hello, world!"); + let (_, argv) = argmap::parse(std::env::args()); + + let args = Args::from(argv); + + let template = Template::new(args); + template.create(); } diff --git a/src/template.rs b/src/template.rs new file mode 100644 index 0000000..c82820b --- /dev/null +++ b/src/template.rs @@ -0,0 +1,95 @@ +use crate::args::Args; +use std::{path::PathBuf, process::exit}; + +/// # Template struct +/// +/// This struct is used to create the template files +/// and directories for the project. +/// +/// ## Fields +/// +/// * `args` - The arguments passed to the program +/// * `pwd` - The current working directory +pub struct Template { + pub args: Args, + pub pwd: PathBuf, +} + +impl Template { + /// Create a new Template struct from the given arguments. + /// + /// # Arguments + /// + /// * `args` - The arguments passed to the program + pub fn new(args: Args) -> Self { + Self { + args, + pwd: std::env::current_dir().unwrap(), + } + } + + /// Create the template files and directories. + /// If the src directory does not exist, create it. + /// If the main file does not exist, create it. + /// If the CMakeLists.txt file does not exist, create it. + pub fn create(&self) { + // create src directory if it doesn't exist + let template = self.get_template(); + + let src_dir = self.pwd.join("src"); + if !src_dir.exists() { + std::fs::create_dir(src_dir).unwrap(); + } + + // create main file + let main_file = self.pwd.join("src").join(self.args.lang.to_string()); + if !main_file.exists() { + std::fs::write(main_file, template).unwrap(); + } + + // create CMakeLists.txt + self.create_cmakelists(); + } + + /// Get the template file contents. + /// If the template file does not exist, print an error and exit. + fn get_template(&self) -> String { + // read template file + let template_file = self + .pwd + .join(&self.args.templates_dir) + .join(self.args.lang.to_string()); + + std::fs::read_to_string(template_file).unwrap_or_else(|_| { + eprintln!("cannot read template file"); + exit(1); + }) + } + + /// Create the CMakeLists.txt file. + fn create_cmakelists(&self) { + let cmakelists = self.pwd.join("CMakeLists.txt"); + + std::fs::write(cmakelists, self.get_cmake_template().join("\n")).unwrap(); + } + + /// Get the CMakeLists.txt template. + /// This is a three element array of strings. + fn get_cmake_template(&self) -> [String; 3] { + [ + format!( + "project({})", + self.args.name.to_lowercase().replace("-", "_") + ), + format!( + "cmake_minimum_required(VERSION {})", + self.args.cmake_min_version + ), + format!( + "add_executable({} src/{})", + self.args.name.to_lowercase().replace("-", "_"), + self.args.lang.to_string() + ), + ] + } +} diff --git a/templates/main.c b/templates/main.c new file mode 100644 index 0000000..6ca7055 --- /dev/null +++ b/templates/main.c @@ -0,0 +1,7 @@ +#include +#include + +int main(int argc, char *argv[]) { + printf("Hello World!\n"); + return 0; +} diff --git a/templates/main.cpp b/templates/main.cpp new file mode 100644 index 0000000..ddcb539 --- /dev/null +++ b/templates/main.cpp @@ -0,0 +1,6 @@ +#include + +int main() { + std::cout << "Hello World!"; + return 0; +}