diff options
| author | Fuwn <[email protected]> | 2022-05-15 23:44:53 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2022-05-15 23:44:53 -0700 |
| commit | 242b0b9591fa4fd5f8b4a45ec4c179f451c62c14 (patch) | |
| tree | 6dcc82c1e87725224bc3dcd22c974ab0775f4c7f | |
| parent | docs(license): add license for vergen (diff) | |
| download | chorus-main.tar.xz chorus-main.zip | |
| -rw-r--r-- | .github/workflows/check.yaml | 35 | ||||
| -rw-r--r-- | .gitignore | 9 | ||||
| -rw-r--r-- | .license_template | 17 | ||||
| -rw-r--r-- | Cargo.toml | 36 | ||||
| -rw-r--r-- | Makefile.toml | 31 | ||||
| -rw-r--r-- | README.md | 37 | ||||
| -rw-r--r-- | rust-toolchain.toml | 2 | ||||
| -rw-r--r-- | rustfmt.toml | 30 | ||||
| -rw-r--r-- | src/cli.rs | 104 | ||||
| -rw-r--r-- | src/commands/build/macros.rs | 74 | ||||
| -rw-r--r-- | src/commands/build/mod.rs | 132 | ||||
| -rw-r--r-- | src/commands/format.rs | 79 | ||||
| -rw-r--r-- | src/commands/init_new.rs | 100 | ||||
| -rw-r--r-- | src/commands/mod.rs | 25 | ||||
| -rw-r--r-- | src/compilers/gnucobol.rs | 73 | ||||
| -rw-r--r-- | src/compilers/mod.rs | 19 | ||||
| -rw-r--r-- | src/config.rs | 61 | ||||
| -rw-r--r-- | src/file_generator.rs | 72 | ||||
| -rw-r--r-- | src/main.rs | 39 | ||||
| -rw-r--r-- | src/utilities.rs | 43 |
20 files changed, 1018 insertions, 0 deletions
diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml new file mode 100644 index 0000000..c1d1f7a --- /dev/null +++ b/.github/workflows/check.yaml @@ -0,0 +1,35 @@ +name: Check ✅ + +on: + workflow_dispatch: + push: + paths: + - "*" + pull_request: + paths: + - "*" + +env: + CARGO_TERM_COLOR: always + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛒 + uses: actions/checkout@v3 + + - name: Toolchain 🧰 + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: rustfmt, clippy + override: true + + - name: Check ✅ + uses: actions-rs/cargo@v1 + continue-on-error: false + with: + command: check + args: --verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5dd755 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Rust +target +Cargo.lock + +# IDE +.idea + +# Development +hi diff --git a/.license_template b/.license_template new file mode 100644 index 0000000..a8a4846 --- /dev/null +++ b/.license_template @@ -0,0 +1,17 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) {20\d{2}(-20\d{2})?} Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) {20\d{2}(-20\d{2})?} Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..48ffd92 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[package] +name = "chorus" +version = "0.1.1" +authors = ["Fuwn <[email protected]>"] +edition = "2021" +description = "Cargo for COBOL" +readme = "README.md" +homepage = "https://github.com/Fuwn/chorus" +repository = "https://github.com/Fuwn/chorus" +license = "GPL-3.0-only" +keywords = ["cobol"] +categories = ["development-tools"] + +# Slower builds, faster executables +[profile.release] +codegen-units = 1 +lto = "fat" +opt-level = 3 +panic = "abort" +strip = true + +[dependencies] +clap = { version = "3.1.14", features = ["derive"] } # CLI +anyhow = "1.0.57" # `Result` +unicode-xid = "0.2.3" # Unicode Standard Annex #31 +walkdir = "2.3.2" # Recursive directory walking +config = "0.13.1" # Configuration +lazy_static = "1.4.0" # Lazy constants +shellfn = "0.1.1" # Shell execution +path-slash = "0.1.4" # Path normalisation + +# Serialization +serde = "1.0.137" +serde_derive = "1.0.137" diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..4c18a79 --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,31 @@ +# ------------ +# | Wrappers | +# ------------ +[tasks.fmt] +toolchain = "nightly" +command = "cargo" +args = ["fmt"] +private = true + +[tasks.check] +command = "cargo" +args = ["check"] +private = true + +[tasks.clippy] +command = "cargo" +args = ["clippy"] +private = true + +# ------------- +# | Executors | +# ------------- +[tasks.checkf] +dependencies = ["fmt", "check"] + +[tasks.checkfc] +dependencies = ["fmt", "check", "clippy"] + +[tasks.run] +command = "cargo" +args = ["run", "--", "${@}"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..f16ad9d --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +<h1 align="center">Chorus</h1> + +<p align="center">Cargo for COBOL</p> + +[](https://crates.io/crates/chorus) +[](https://github.com/Fuwn/chorus/actions/workflows/check.yaml) + +Chorus is a Cargo-like utility for building and distributing COBOL packages. + +Chorus is under heavy development and functions, kinda. It is nowhere near +finished, but few features work. + +## Installation + +```shell +$ cargo install --force --git https://github.com/Fuwn/chorus +``` + +## Usage + +### Creating a new Chorus package + +```shell +$ chorus new your_project_name +``` + +### Building a Chorus package + +```shell +$ chorus build --path your_project_name +``` + +## License + +This project is licensed with the +[GNU General Public License v3.0 +](https://github.com/Fuwn/rust-crate-template/blob/main/LICENSE). diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..292fe49 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "stable" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1dd4c90 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,30 @@ +condense_wildcard_suffixes = true +edition = "2021" +enum_discrim_align_threshold = 20 +# error_on_line_overflow = true +# error_on_unformatted = true +fn_single_line = true +force_multiline_blocks = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +imports_layout = "HorizontalVertical" +license_template_path = ".license_template" +max_width = 80 +match_arm_blocks = false +imports_granularity = "Crate" +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +reorder_impl_items = true +group_imports = "StdExternalCrate" +reorder_modules = true +report_fixme = "Always" +# report_todo = "Always" +struct_field_align_threshold = 20 +struct_lit_single_line = false +tab_spaces = 2 +use_field_init_shorthand = true +use_try_shorthand = true +where_single_line = true +wrap_comments = true diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..b714ce7 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,104 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use clap::{Parser, Subcommand}; + +use crate::commands; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +struct Chorus { + #[clap(subcommand)] + command: Commands, + + /// Enable debug output + #[clap(short, long, parse(from_occurrences))] + debug: usize, +} + +#[derive(Subcommand)] +enum Commands { + /// Create a new Chorus package in an existing directory + Init { + /// The location to create the new Chorus package at + path: Option<std::path::PathBuf>, + }, + /// Create a new Chorus package in a new directory + New { + /// The location to create the new Chorus project at + path: std::path::PathBuf, + }, + /// Format all COBOL files within a Chorus package + Fmt { + /// The location of the target Chorus package to format + #[clap(short, long)] + path: Option<std::path::PathBuf>, + }, + /// Compile the Chorus package within the current directory + Build { + /// The location of the target Chorus package to build + #[clap(short, long)] + path: Option<std::path::PathBuf>, + #[clap(short, long)] + output: Option<std::path::PathBuf>, + }, +} + +/// Setup and handle command-line interactions +pub fn evaluate() { + // Set up the CLI + let chorus = Chorus::parse(); + + // Handle CLI command + match &chorus.command { + Commands::Init { + path, + } => { + if let Err(why) = commands::init_new(path, true) { + println!("error: could not initialise package: {}", why); + std::process::exit(1); + }; + } + Commands::New { + path, + } => { + if let Err(why) = commands::init_new(&Some(path.clone()), false) { + println!("error: could not create new package: {}", why); + std::process::exit(1); + }; + } + Commands::Fmt { + path, + } => { + if let Err(why) = commands::format(path) { + println!("error: could not format package: {}", why); + std::process::exit(1); + }; + } + Commands::Build { + path, + output, + } => { + if let Err(why) = commands::build(path, output) { + println!("error: could not build package: {}", why); + std::process::exit(1); + }; + } + } +} diff --git a/src/commands/build/macros.rs b/src/commands/build/macros.rs new file mode 100644 index 0000000..4281dff --- /dev/null +++ b/src/commands/build/macros.rs @@ -0,0 +1,74 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +/// "None if empty string, or with preformat" +/// +/// By default, this macro evaluates whether the input `$string` is empty by +/// calling `is_empty`. However, by specifying the type `i32` at the end of the +/// macro invocation, you can tell this macro to evaluate based on `== 0` +/// instead of `is_empty`. +#[macro_export] +macro_rules! niesowp { + ($preformat:literal, $string:expr) => {{ + if $string.is_empty() { + "".to_string() + } else { + format!("{}{}", $preformat, $string) + } + }}; + ($preformat:literal, $number:expr,i32) => {{ + if $number == 0 { + "".to_string() + } else { + format!("{}{}", $preformat, $number) + } + }}; +} + +#[macro_export] +macro_rules! compiler_get_or { + ($compiler:ident, $key:ident, $default:literal) => { + $crate::config::CONFIG.build.$compiler.as_ref().map_or_else( + || $default.into(), + |compiler| { + compiler + .$key + .as_ref() + .map_or_else(|| $default.into(), Clone::clone) + }, + ) + }; +} + +/// A helper to assist with the sanity checks +#[macro_export] +macro_rules! entry_exists { + ($entries:ident, $path:ident, $entry:literal, $context:literal) => { + let entry = $entry.replace("\\", "/"); + + if !$entries.contains(&PathBuf::from_slash( + format!("{}/{}", $path.as_path().display(), entry).to_string(), + )) { + return Err(anyhow::anyhow!( + "{} \"{}\" does not exist", + $context, + format!("{}/{}", $path.as_path().display(), entry) + )); + } + }; +} diff --git a/src/commands/build/mod.rs b/src/commands/build/mod.rs new file mode 100644 index 0000000..4f22303 --- /dev/null +++ b/src/commands/build/mod.rs @@ -0,0 +1,132 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +mod macros; + +use std::path::PathBuf; + +use anyhow::{anyhow, Context}; +use path_slash::PathBufExt; +use walkdir::{DirEntry, WalkDir}; + +use crate::{ + compiler_get_or, + compilers::gnucobol, + config::CONFIG, + create_directory, + entry_exists, + niesowp, +}; + +/// Build an existing Chorus package +pub fn build( + path: &Option<PathBuf>, + output: &Option<PathBuf>, +) -> anyhow::Result<()> { + let our_path: PathBuf = path.clone().unwrap_or_else(|| PathBuf::from(".")); + let entries = WalkDir::new(&our_path) + .into_iter() + .filter_map(Result::ok) + .into_iter() + .map(|e: DirEntry| e.path().to_owned()) + .collect::<Vec<PathBuf>>(); + + // Sanity checks to make sure the target directory is a Chorus package + entry_exists!(entries, our_path, "src", "directory"); + entry_exists!(entries, our_path, "src/main.cbl", "file"); + entry_exists!(entries, our_path, "package.toml", "directory"); + + std::env::set_current_dir(&our_path)?; + + let our_output = output.clone().unwrap_or_else(|| { + PathBuf::from(format!("out/{}", CONFIG.package.name).as_str()) + }); + + if !PathBuf::from("out").exists() { + create_directory!("out"); + } + + if !our_output + .parent() + .ok_or_else(|| { + anyhow!( + "parent directory of output directory \"{}\" does not exist", + our_output.display() + ) + })? + .exists() + { + return Err(anyhow!( + "output directory \"{}\" does not exist", + our_output.display() + )); + } + + dispatch_build( + &compiler_get_or!(gnucobol, format, "free"), + &niesowp!("-", compiler_get_or!(gnucobol, optimization, "")), + &niesowp!("-std=", compiler_get_or!(gnucobol, std, "")), + &our_output, + )?; + + Ok(()) +} + +fn dispatch_build( + format: &str, + optimization: &str, + standard: &str, + our_output: &std::path::Path, +) -> anyhow::Result<()> { + if let Some(mode) = &CONFIG.build.mode { + if mode.eq_ignore_ascii_case("incremental") { + if let Err(why) = gnucobol::compile_executable_object( + "cobc", + &format!("-{} {} {}", format, optimization, standard), + "src/main.cbl", + &format!("{}.o", &our_output.display().to_string()), + ) { + return Err(anyhow!(why.to_string())); + } + if let Err(why) = gnucobol::compile_executable_single( + "cobc", + &format!("-{} {} {}", format, optimization, standard), + &format!("{}.o", &our_output.display().to_string()), + &our_output.display().to_string(), + ) { + return Err(anyhow!(why.to_string())); + } + } else if let Err(why) = gnucobol::compile_executable_single( + "cobc", + &format!("-{} {} {}", format, optimization, standard), + "src/main.cbl", + &our_output.display().to_string(), + ) { + return Err(anyhow!(why.to_string())); + } + } else if let Err(why) = gnucobol::compile_executable_single( + "cobc", + &format!("-{} {} {}", format, optimization, standard), + "src/main.cbl", + &our_output.display().to_string(), + ) { + return Err(anyhow!(why.to_string())); + } + + Ok(()) +} diff --git a/src/commands/format.rs b/src/commands/format.rs new file mode 100644 index 0000000..a0aabc4 --- /dev/null +++ b/src/commands/format.rs @@ -0,0 +1,79 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use std::path::PathBuf; + +use walkdir::{DirEntry, WalkDir}; + +/// Format an existing Chorus package +#[allow(clippy::unnecessary_wraps)] +pub fn format(path: &Option<PathBuf>) -> anyhow::Result<()> { + let our_path = path.clone().unwrap_or_else(|| PathBuf::from(".")); + + // Only try to format if the target Chorus package directory exists + if our_path.exists() { + let mut entries: Vec<DirEntry> = vec![]; + + std::env::set_current_dir(&our_path)?; + + // Iterate over all paths in the target Chorus package + for entry in WalkDir::new(our_path).into_iter().filter_map(Result::ok) { + // Only track COBOL files to format if the file has a valid COBOL extension + if entry + .file_name() + .to_str() + .unwrap_or("") + .rsplit('.') + .next() + .map(|extension: &str| { + #[allow(clippy::blocks_in_if_conditions)] + { + let mut valid_extension = false; + + // If any user defined COBOL extensions exist, check against them; + // otherwise, check against the "cbl" file extension. + if let Some(valid_extensions) = + &crate::config::CONFIG.build.extensions + { + for an_extension in valid_extensions { + if valid_extension { + break; + } + + valid_extension = extension.eq_ignore_ascii_case(an_extension); + } + } else { + valid_extension = extension.eq_ignore_ascii_case("cbl"); + } + + valid_extension + } + }) + == Some(true) + { + entries.push(entry); + } + } + + for entry in entries { + println!("entry: {:?}", entry); + } + } + + Ok(()) +} diff --git a/src/commands/init_new.rs b/src/commands/init_new.rs new file mode 100644 index 0000000..5889b7f --- /dev/null +++ b/src/commands/init_new.rs @@ -0,0 +1,100 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use std::{io::Write, path::PathBuf}; + +use anyhow::Context; + +use crate::file_generator; + +/// A wrapper of `std::fs::File::create` and `write!` which creates, writes, and +/// error checks a file given a base path and file name. +macro_rules! write_new_file { + ($path:ident, $filepath:expr, $content:expr) => {{ + // Save the file path for later use + let file = + format!("{}{}", format!("{}/", $path.as_path().display()), $filepath); + + // Create and write to the new file with error handling + write!( + &mut std::fs::File::create(&file) + .with_context(|| format!("could not create file: \"{}\"", file))?, + "{}", + $content, + ) + .with_context(|| format!("could not write to file: \"{}\"", file))?; + }}; +} + +#[macro_export] +/// A wrapper of `std::fs::create_dir_all` which creates and error checks a +/// directory given a base path and directory name. +macro_rules! create_directory { + ($path:ident, $directory_path:expr) => {{ + // Save the directory path for later use + let directory = format!( + "{}{}", + format!("{}/", $path.as_path().display()), + $directory_path + ); + + // Create the new directory with error handling + std::fs::create_dir(&directory).with_context(|| { + format!("could not create directory: \"{}\"", directory) + })?; + }}; + ($directory_path:expr) => {{ + // Create the new directory with error handling + std::fs::create_dir(&$directory_path).with_context(|| { + format!("could not create directory: \"{}\"", $directory_path) + })?; + }}; +} + +/// Initialise or create a new Chorus package +pub fn init_new(path: &Option<PathBuf>, init: bool) -> anyhow::Result<()> { + let our_path = path.clone().unwrap_or_else(|| PathBuf::from(".")); + + crate::utilities::is_unicode_xid(&our_path.display().to_string())?; + + // Make sure that we are not initialing a new Chorus project in a non-empty + // directory. + if init && our_path.exists() && our_path.read_dir()?.next().is_some() { + return Err(anyhow::anyhow!( + "target directory \"{}\" is not empty", + our_path.as_path().display() + )); + } + + // Set up the new Chorus package with the default file structure of a Chorus + // package. + create_directory!(our_path, ""); + create_directory!(our_path, "src"); + write_new_file!( + our_path, + "src/main.cbl", + file_generator::main_file(&our_path)? + ); + write_new_file!( + our_path, + "package.toml", + file_generator::package_manifest(&our_path)? + ); + + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..c92082c --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,25 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +pub mod build; +pub mod format; +pub mod init_new; + +pub use build::build; +pub use format::format; +pub use init_new::init_new; diff --git a/src/compilers/gnucobol.rs b/src/compilers/gnucobol.rs new file mode 100644 index 0000000..8b19e85 --- /dev/null +++ b/src/compilers/gnucobol.rs @@ -0,0 +1,73 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use std::error::Error; + +use shellfn::shell; + +#[derive(serde_derive::Deserialize)] +pub struct GNUCobol { + pub format: Option<String>, + pub options: Option<Vec<String>>, + pub std: Option<String>, + pub optimization: Option<String>, +} + +/// Compile an executable Chorus package in a single command +#[shell] +pub fn compile_executable_single( + compiler: &str, + flags: &str, + files: &str, + output: &str, +) -> Result<String, Box<dyn Error>> { + r#"$COMPILER -x $FLAGS $FILES -o $OUTPUT"# +} + +/// Compile an ordinary file into an object +#[shell] +pub fn compile_object( + compiler: &str, + flags: &str, + file: &str, + output: &str, +) -> Result<String, Box<dyn Error>> { + r#"$COMPILER -c $FLAGS $FILE -o $OUTPUT"# +} + +/// Compile an executable file into an object +#[shell] +pub fn compile_executable_object( + compiler: &str, + flags: &str, + file: &str, + output: &str, +) -> Result<String, Box<dyn Error>> { + r#"$COMPILER -c -x $FLAGS $FILE -o $OUTPUT"# +} + +/// Compile an ordinary file into a module +#[shell] +pub fn compile_module( + compiler: &str, + flags: &str, + file: &str, + output: &str, +) -> Result<String, Box<dyn Error>> { + r#"$COMPILER -m $FLAGS $FILE -o $OUTPUT"# +} diff --git a/src/compilers/mod.rs b/src/compilers/mod.rs new file mode 100644 index 0000000..2703f56 --- /dev/null +++ b/src/compilers/mod.rs @@ -0,0 +1,19 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +pub mod gnucobol; diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4494903 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,61 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use serde_derive::Deserialize; + +lazy_static::lazy_static! { + pub static ref CONFIG: Config = { + let config = Config::new(); + + if let Err(why) = config { + println!("error: could not read config: {}", why); + std::process::exit(1); + } + + config.unwrap() + }; +} + +#[derive(Deserialize)] +pub struct Package { + pub name: String, +} + +#[derive(Deserialize)] +pub struct Build { + pub compiler: String, + pub extensions: Option<Vec<String>>, + pub mode: Option<String>, + pub gnucobol: Option<crate::compilers::gnucobol::GNUCobol>, + pub r#type: Option<String>, +} + +#[derive(Deserialize)] +pub struct Config { + pub package: Package, + pub build: Build, +} +impl Config { + /// Initialise a new `Config` + pub fn new() -> Result<Self, config::ConfigError> { + config::Config::builder() + .add_source(config::File::with_name("package.toml")) + .build()? + .try_deserialize() + } +} diff --git a/src/file_generator.rs b/src/file_generator.rs new file mode 100644 index 0000000..278da0e --- /dev/null +++ b/src/file_generator.rs @@ -0,0 +1,72 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use std::path::Path; + +use anyhow::Result; + +use crate::utilities::is_unicode_xid; + +/// Keep unique paths, replace local paths (".") with "default" +fn local_or_default(path: &Path) -> String { + if path.display().to_string() == "." { + "default".to_string() + } else { + path.display().to_string() + } +} + +/// Generate a default main file +pub fn main_file(name: &Path) -> Result<String> { + is_unicode_xid(&name.display().to_string())?; + + Ok(format!( + r#"IDENTIFICATION DIVISION. +PROGRAM-ID. {}. + +PROCEDURE DIVISION. +DISPLAY "Hello, World!". +STOP RUN. +"#, + local_or_default(name).to_ascii_uppercase(), + )) +} + +/// Generate a Chorus package manifest +pub fn package_manifest(name: &Path) -> Result<String> { + is_unicode_xid(&name.display().to_string())?; + + Ok(format!( + r#"[package] +name = "{}" + +[build] +compiler = "gnucobol" +extensions = ["cbl", "cob"] +mode = "incremental" +type = "executable" + +[build.gnucobol] +format = "free" +options = ["-Wall"] +std = "default" +optimization = "O3" +"#, + local_or_default(name), + )) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6387be9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,39 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +#![deny( + warnings, + nonstandard_style, + unused, + future_incompatible, + rust_2018_idioms, + unsafe_code, + clippy::all, + clippy::nursery, + clippy::pedantic +)] +#![recursion_limit = "128"] + +mod cli; +mod commands; +mod compilers; +mod config; +mod file_generator; +mod utilities; + +fn main() { cli::evaluate(); } diff --git a/src/utilities.rs b/src/utilities.rs new file mode 100644 index 0000000..e5b3c3c --- /dev/null +++ b/src/utilities.rs @@ -0,0 +1,43 @@ +// This file is part of Chorus <https://github.com/Fuwn/chorus>. +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. +// +// Copyright (C) 2022-2022 Fuwn <[email protected]> +// SPDX-License-Identifier: GPL-3.0-only + +use anyhow::anyhow; +use unicode_xid::UnicodeXID; + +/// Check if a string is a valid Unicode XID identifier +pub fn is_unicode_xid(input: &str) -> anyhow::Result<()> { + let mut input_scroller = input.chars(); + + if !UnicodeXID::is_xid_start(input_scroller.next().unwrap()) { + return Err(anyhow!( + "identifier \"{}\" is not valid unicode xid identifier", + input + )); + } + + for character in input_scroller { + if !UnicodeXID::is_xid_continue(character) { + return Err(anyhow!( + "identifier \"{}\" is not valid unicode xid identifier", + input + )); + } + } + + Ok(()) +} |