diff --git a/.github/workflows/lint_master.yml b/.github/workflows/lint_master.yml index 703888b76..14c096918 100644 --- a/.github/workflows/lint_master.yml +++ b/.github/workflows/lint_master.yml @@ -42,9 +42,10 @@ jobs: echo ${{steps.cache-cargo.outputs}} cargo install --path=cli - name: install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + run: cargo install wasm-pack - name: JS/TS linting - run: cargo chipmunk lint -u print + timeout-minutes: 30 + run: cargo chipmunk lint -u immediate - name: TypeScript Check - Client Application working-directory: application/client run: yarn run check @@ -90,6 +91,7 @@ jobs: if: steps.cache-cargo.outputs.cache-hit != 'true' run: cargo install --path=cli - name: install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + run: cargo install wasm-pack - name: Execute tests - run: cargo chipmunk test core wrapper wasm -u print \ No newline at end of file + timeout-minutes: 30 + run: cargo chipmunk test -u immediate diff --git a/.github/workflows/pullrequest_check.yml b/.github/workflows/pullrequest_check.yml index fa5644cdb..9e9d828ec 100644 --- a/.github/workflows/pullrequest_check.yml +++ b/.github/workflows/pullrequest_check.yml @@ -43,9 +43,10 @@ jobs: if: steps.cache-cargo.outputs.cache-hit != 'true' run: cargo install --path=cli - name: install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + run: cargo install wasm-pack - name: JS/TS linting - run: cargo chipmunk lint -u print + timeout-minutes: 30 + run: cargo chipmunk lint -u immediate - name: TypeScript Check - Client Application working-directory: application/client run: yarn run check @@ -92,6 +93,7 @@ jobs: if: steps.cache-cargo.outputs.cache-hit != 'true' run: cargo install --path=cli - name: install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + run: cargo install wasm-pack - name: Execute tests - run: cargo chipmunk test core wrapper wasm -u print \ No newline at end of file + timeout-minutes: 30 + run: cargo chipmunk test -u immediate diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 1da396e4b..96ca490e5 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,3 +1,10 @@ +# 0.2.8 + +## Features: + +* User can configure the tool on user levels to set their preferred shell and UI mode. +* Default shell will be retrieved from environment variables in unix-based environments. + # 0.2.7 ## Features: diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 7078c2783..5ad830cc8 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -80,7 +80,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -90,7 +90,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -170,13 +170,14 @@ checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cargo-chipmunk" -version = "0.2.7" +version = "0.2.8" dependencies = [ "anyhow", "clap", "clap_complete", "console", "dir_checksum", + "dirs", "dotenvy", "fs_extra", "futures", @@ -272,7 +273,7 @@ dependencies = [ "lazy_static", "libc", "unicode-width", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -320,6 +321,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -454,6 +476,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.29.0" @@ -617,6 +650,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "libz-sys" version = "1.1.18" @@ -678,7 +721,7 @@ dependencies = [ "hermit-abi", "libc", "wasi", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -702,6 +745,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "parking_lot" version = "0.12.3" @@ -722,7 +771,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -839,6 +888,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex-automata" version = "0.4.7" @@ -964,7 +1024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1044,7 +1104,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1201,7 +1261,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1210,13 +1270,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1225,28 +1309,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1259,24 +1361,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ee2c14753..10e62463d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-chipmunk" -version = "0.2.7" +version = "0.2.8" authors = ["Ammar Abou Zor "] edition = "2021" description = "CLI Tool for chipmunk application development" @@ -33,6 +33,7 @@ serde = {version = "1.0", features = ["derive"]} serde_json = "1.0" glob = "0.3" toml = "0.8" +dirs = "5.0" [dev-dependencies] tempdir.workspace = true diff --git a/cli/README.md b/cli/README.md index 0a3b6250d..7abaf429f 100644 --- a/cli/README.md +++ b/cli/README.md @@ -39,6 +39,7 @@ Usage: cargo chipmunk Commands: environment Provides commands for the needed tools for the development [aliases: env] print-dot Prints an overview of targets dependencies in print-dot format for `Graphviz` [aliases: dot] + configuration Provides commands for the configuration of this tool on user level [aliases: config] lint Runs linting & clippy for all or the specified targets build Build all or the specified targets clean Clean all or the specified targets @@ -97,6 +98,38 @@ Options: Print help (see a summary with '-h') ``` +## User Configurations + +Users can configure their preferences for the shell used to run process commands and the default UI mode. The configuration file should be located in the Chipmunk home directory, within the `build_cli` directory, named `config.toml`. + +Configurations can be managed via the CLI using the `config` subcommands. These subcommands allow you to resolve the path to the configuration file and generate default configurations, with an option to write them directly to the file. + +Below is an example of the configuration file with the available settings: + +```toml +# Defines the shell to be used for executing process commands. +# Options: +# - `sh` (Unix-based systems only) +# - `cmd` (Windows only) +# - `bash` +# - `zsh` +# - `fish` +# - `nu-shell` +# - `elvish` +# - `power-shell` +# If not specified, the system will default to: +# - The value of the `SHELL` environment variable on Unix-based systems. +# - `cmd` on Windows. +shell = "sh" + +# Defines the preferred UI mode. +# Options: +# - `bars`: Displays progress bars, showing the current line of the output of each command. +# - `report`: Displays progress bars and prints a summary of all command logs to stdout after all jobs have finished. +# - `print`: Outputs each job's result to stdout once the job finishes. No progress bars are displayed. +# - `immediate`: Outputs logs immediately as they are produced, which may cause overlapping logs for parallel jobs. No progress bars are displayed. +ui_mode = "bars" +``` ## Benchmarks via Build CLI Tool diff --git a/cli/integration_tests/run_all.py b/cli/integration_tests/run_all.py index f6b000cc5..32433d95d 100644 --- a/cli/integration_tests/run_all.py +++ b/cli/integration_tests/run_all.py @@ -14,6 +14,7 @@ from test_cmd import run_test_command from release import run_release_command from bench import run_benchmark_command +from user_config import run_user_configs_commands def run_all(): @@ -84,6 +85,14 @@ def run_all(): raise print_separator() + ### User Configurations ### + try: + run_user_configs_commands() + except Exception: + print_err("User Configurations") + raise + print_separator() + ### Shell Completion ### try: run_shell_completion_commands() diff --git a/cli/integration_tests/user_config.py b/cli/integration_tests/user_config.py new file mode 100644 index 000000000..2b14465dc --- /dev/null +++ b/cli/integration_tests/user_config.py @@ -0,0 +1,33 @@ +""" +Provides methods to test the User Configurations commands in Chipmunk Build CLI Tool +""" + +from utls import run_command, print_blue_bold, print_green_bold + +CONFIG_BASE_COMMAND = [ + "cargo", + "run", + "-r", + "--", + "chipmunk", + "config", +] + + +def run_user_configs_commands(): + """Runs commands to print the default configurations and the path for the configurations file""" + print_blue_bold("Running print configurations file path Command...") + print_path_cmd = CONFIG_BASE_COMMAND.copy() + print_path_cmd.append("path") + run_command(print_path_cmd) + print_green_bold("*** Print configurations file path Command Succeeded ***") + + print_blue_bold("Running print default configurations Command...") + print_default_cmd = CONFIG_BASE_COMMAND.copy() + print_default_cmd.append("default") + run_command(print_default_cmd) + print_green_bold("*** Print default configurations Command Succeeded ***") + + +if __name__ == "__main__": + run_user_configs_commands() diff --git a/cli/src/benchmark/core.rs b/cli/src/benchmark/core.rs index 6d1697beb..cdc34ff84 100644 --- a/cli/src/benchmark/core.rs +++ b/cli/src/benchmark/core.rs @@ -33,20 +33,20 @@ impl ConfigsInfos { let config_file_path = config_path().join(CONFIG_FILENAME); ensure!( config_file_path.exists(), - "Configuration file doesn't exist. Path: {}", + "Benchmarks Configuration file doesn't exist. Path: {}", config_file_path.display() ); let content = read_to_string(&config_file_path).with_context(|| { format!( - "Error while reading configuration file. Path: {}", + "Error while reading benchmarks configuration file. Path: {}", config_file_path.display() ) })?; let config = toml::from_str(&content).with_context(|| { format!( - "Error while parsing configuration file. Path: {}", + "Error while parsing benchmarks configuration file. Path: {}", config_file_path.display() ) })?; diff --git a/cli/src/cli_args.rs b/cli/src/cli_args.rs index ae080ccd4..3c2f1510b 100644 --- a/cli/src/cli_args.rs +++ b/cli/src/cli_args.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; +use serde::{Deserialize, Serialize}; use crate::{benchmark::BenchTarget, target::Target}; @@ -39,22 +40,26 @@ pub struct Cli { // alaises in options description. // Link for the issue: https://github.com/clap-rs/clap/issues/4416. -#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)] +#[derive(Debug, Clone, Copy, Default, clap::ValueEnum, Serialize, Deserialize)] /// Specifies the UI mode for displaying command logs and progress in the terminal. pub enum UiMode { /// Displays progress bars, showing the current line of the output of each command. [aliases: 'b'] #[default] #[value(name = "bars", alias("b"))] + #[serde(rename = "bars")] ProgressBars, /// Displays progress bars and prints a summary of all command logs to stdout after all jobs have finished. [aliases: 'r'] #[value(name = "report", alias("r"))] + #[serde(rename = "report")] BarsWithReport, /// Outputs each job's result to stdout once the job finishes. No progress bars are displayed. [aliases: 'p'] #[value(name = "print", alias("p"))] + #[serde(rename = "print")] PrintOnJobFinish, /// Outputs logs immediately as they are produced, which may cause overlapping logs for parallel jobs. /// No progress bars are displayed. [aliases: 'i'] #[value(name = "immediate", alias("i"))] + #[serde(rename = "immediate")] PrintImmediately, } @@ -71,6 +76,10 @@ pub enum Command { #[arg(short, long, default_value_t = false)] all_jobs: bool, }, + #[clap(name = "configuration", visible_alias = "config")] + #[command(subcommand)] + /// Provides commands for the configuration of this tool on user level. + UserConfiguration(UserConfigCommand), /// Runs linting & clippy for all or the specified targets Lint { /// Target to lint, by default whole application will be linted @@ -80,8 +89,8 @@ pub enum Command { #[arg(short, long, help = FAIL_FAST_HELP_TEXT)] fail_fast: bool, - #[arg(short, long, default_value_t = UiMode::default(), help = UI_LOG_OPTION_HELP_TEXT, value_enum)] - ui_mode: UiMode, + #[arg(short, long, help = UI_LOG_OPTION_HELP_TEXT, value_enum)] + ui_mode: Option, }, /// Build all or the specified targets Build { @@ -96,8 +105,8 @@ pub enum Command { #[arg(short, long, help = FAIL_FAST_HELP_TEXT)] fail_fast: bool, - #[arg(short, long, default_value_t = UiMode::default(), help = UI_LOG_OPTION_HELP_TEXT, value_enum)] - ui_mode: UiMode, + #[arg(short, long, help = UI_LOG_OPTION_HELP_TEXT, value_enum)] + ui_mode: Option, }, /// Clean all or the specified targets Clean { @@ -105,8 +114,8 @@ pub enum Command { #[arg(index = 1)] target: Option>, - #[arg(short, long, default_value_t = UiMode::default(), help = UI_LOG_OPTION_HELP_TEXT, value_enum)] - ui_mode: UiMode, + #[arg(short, long, help = UI_LOG_OPTION_HELP_TEXT, value_enum)] + ui_mode: Option, }, /// Run tests for all or the specified targets Test { @@ -121,8 +130,8 @@ pub enum Command { #[arg(short, long, help = FAIL_FAST_HELP_TEXT)] fail_fast: bool, - #[arg(short, long, default_value_t = UiMode::default(), help = UI_LOG_OPTION_HELP_TEXT, value_enum)] - ui_mode: UiMode, + #[arg(short, long, help = UI_LOG_OPTION_HELP_TEXT, value_enum)] + ui_mode: Option, /// Sets which test specifications should be run. /// Currently implemented for wrapper target (ts-bindings) only @@ -178,3 +187,17 @@ pub enum EnvironmentCommand { #[clap(visible_alias = "list")] Print, } + +#[derive(Debug, Subcommand, Clone, Copy)] +pub enum UserConfigCommand { + #[clap(visible_alias = "path")] + /// Prints the path to the user configurations file. + PrintPath, + #[clap(name = "print-default", visible_alias = "default")] + /// Dumps the configurations with the default values to be used as a reference and base to + /// user configurations. + DumpDefaultConfiguration, + #[clap(name = "write-default", visible_alias = "write")] + /// Creates user configurations file if doesn't exist then writes the default configurations to it. + WriteDefaultToFile, +} diff --git a/cli/src/location.rs b/cli/src/location.rs index 082b2c82d..a59577fd5 100644 --- a/cli/src/location.rs +++ b/cli/src/location.rs @@ -56,3 +56,22 @@ pub fn init_location() -> Result<(), Error> { pub fn config_path() -> PathBuf { get_root().join("cli").join("config") } + +/// Return the path for the home directory directory where logs and configuration are placed. +pub fn chipmunk_home_dir() -> anyhow::Result { + let home_dir = dirs::home_dir() + .map(|home| home.join(".chipmunk")) + .context("Resolving home directory failed")?; + + Ok(home_dir) +} + +/// Return the path for the build CLI directory in Chipmunk home directory. +pub fn build_cli_home_dir() -> anyhow::Result { + let chipmunk_home = + chipmunk_home_dir().context("Error while resolving Chipmunk home directory")?; + const BUILD_CLI_DIR_NAME: &str = "build_cli"; + let build_cli_path = chipmunk_home.join(BUILD_CLI_DIR_NAME); + + Ok(build_cli_path) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index ad0f6a02f..9f77221d7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,12 +18,13 @@ mod shell; mod spawner; mod target; mod tracker; +mod user_config; mod version; -use anyhow::{bail, Error}; +use anyhow::{bail, Context, Error}; use checksum_records::ChecksumRecords; use clap::Parser; -use cli_args::{CargoCli, Command, UiMode}; +use cli_args::{CargoCli, Command, UiMode, UserConfigCommand}; use console::style; use dev_environment::{print_env_info, validate_dev_tools}; use job_type::JobType; @@ -36,6 +37,7 @@ use tokio::signal; use tracker::{get_tracker, init_tracker}; pub use jobs_runner::jobs_state::JobsState; +use user_config::UserConfiguration; use crate::cli_args::EnvironmentCommand; @@ -48,6 +50,9 @@ async fn main() -> Result<(), Error> { // Validate current directory location. init_location()?; + // Load and validate user configurations + UserConfiguration::init()?; + // Check for newer versions version::check_version(); @@ -99,6 +104,22 @@ async fn main_process(command: Command) -> Result<(), Error> { } return Ok(()); } + Command::UserConfiguration(sub_command) => match sub_command { + UserConfigCommand::PrintPath => { + let config_path = UserConfiguration::file_path() + .context("Error while resolving user configurations file path")?; + + println!("{}", config_path.display()); + + return Ok(()); + } + UserConfigCommand::DumpDefaultConfiguration => { + return UserConfiguration::print_default() + } + UserConfigCommand::WriteDefaultToFile => { + return UserConfiguration::write_default_to_file() + } + }, Command::Lint { target, fail_fast, @@ -178,7 +199,7 @@ async fn main_process(command: Command) -> Result<(), Error> { } else { UiMode::PrintOnJobFinish }; - init_tracker(ui_mode); + init_tracker(Some(ui_mode)); validate_dev_tools()?; do_release(development, code_sign).await?; let tracker = get_tracker(); @@ -235,7 +256,7 @@ async fn main_process(command: Command) -> Result<(), Error> { } } Err(err) => { - eprintln!("Builder error: {:?}", err); + eprintln!("Error: {:?}", err); eprintln!("---------------------------------------------------------------------"); success = false; } diff --git a/cli/src/shell.rs b/cli/src/shell.rs index 1622ff9a4..f25b2e03a 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -1,46 +1,167 @@ -//! Provides the methods to generate completion of the CLI sub-commands and arguments for the given -//! shell. +//! Provides struct representing the shell running by user besides a method to generate +//! completion of the CLI sub-commands and arguments for the given shell. -use std::io; +use std::{fmt::Display, io}; use anyhow::Context; use clap::CommandFactory; use clap_complete::{generate, Shell}; +use serde::{Deserialize, Serialize}; -use crate::cli_args::CargoCli; +use crate::{cli_args::CargoCli, user_config::UserConfiguration}; -/// Creates [`std::process::Command`] running in the corresponding standard shell to each platform. -pub fn shell_std_command() -> std::process::Command { - use std::process::Command; +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +/// Represents the shell running by users providing method to create commands to run process +/// on the given shell. +pub enum UserShell { + #[cfg(unix)] + Sh, + #[cfg(windows)] + Cmd, + Bash, + Zsh, + Fish, + NuShell, + Elvish, + PowerShell, +} + +impl Display for UserShell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(unix)] + UserShell::Sh => write!(f, "Sh"), + #[cfg(windows)] + UserShell::Cmd => write!(f, "Cmd"), + UserShell::Bash => write!(f, "Bash"), + UserShell::Zsh => write!(f, "Zsh"), + UserShell::Fish => write!(f, "Fish"), + UserShell::NuShell => write!(f, "Nu Shell"), + UserShell::Elvish => write!(f, "Envish"), + UserShell::PowerShell => write!(f, "Power Shell"), + } + } +} - if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - cmd.arg("/C"); +#[cfg(unix)] +impl Default for UserShell { + fn default() -> Self { + use std::sync::LazyLock; + // Try to retrieve the default shell from the environment variable if available, + // otherwise use 'sh' + static DEFAULT_SHELL: LazyLock = LazyLock::new(|| { + let shell = std::env::var("SHELL") + .ok() + .and_then(|shell| shell.rsplit('/').next().map(|a| a.to_owned())) + .map(|shell| match shell.to_lowercase().as_str() { + "bash" => UserShell::Bash, + "zsh" => UserShell::Zsh, + "fish" => UserShell::Fish, + "pwsh" => UserShell::PowerShell, + "nu" => UserShell::NuShell, + "elvish" => UserShell::Elvish, + _ => UserShell::Sh, + }) + .unwrap_or(UserShell::Sh); - cmd - } else { - let mut cmd = Command::new("sh"); - cmd.arg("-c"); + shell + }); - cmd + *DEFAULT_SHELL } } -/// Creates [`tokio::process::Command`] running in the corresponding standard shell to each platform. -pub fn shell_tokio_command() -> tokio::process::Command { - use tokio::process::Command; +#[cfg(windows)] +impl Default for UserShell { + fn default() -> Self { + UserShell::Cmd + } +} - if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - cmd.arg("/C"); +impl UserShell { + /// Provides [`std::process::Command`] to run a process on the given shell. + pub fn std_command(self) -> std::process::Command { + let mut cmd = std::process::Command::new(self.bin()); + cmd.arg(self.arg()); cmd - } else { - let mut cmd = Command::new("sh"); - cmd.arg("-c"); + } + + /// Provides an asynchronous [`tokio::process::Command`] to run a process on the given shell. + pub fn tokio_command(self) -> tokio::process::Command { + let mut cmd = tokio::process::Command::new(self.bin()); + cmd.arg(self.arg()); cmd } + + /// Binary name for the shell + pub const fn bin(self) -> &'static str { + match self { + #[cfg(unix)] + UserShell::Sh => "sh", + #[cfg(windows)] + UserShell::Cmd => "cmd", + UserShell::Bash => "bash", + UserShell::Zsh => "zsh", + UserShell::Fish => "fish", + UserShell::NuShell => "nu", + UserShell::Elvish => "elvish", + UserShell::PowerShell => "pwsh", + } + } + + /// Argument provided by each shell to run the provided process command and its arguments. + const fn arg(self) -> &'static str { + match self { + #[cfg(unix)] + UserShell::Sh => "-c", + #[cfg(windows)] + UserShell::Cmd => "/C", + UserShell::Bash + | UserShell::Zsh + | UserShell::Fish + | UserShell::NuShell + | UserShell::Elvish => "-c", + UserShell::PowerShell => "-Command", + } + } + + /// Checks if the shell exist on the system by running it with the version argument. + pub fn exist(self) -> bool { + // Default shells are always installed on their respecting operating system and don't need + // extra checks avoiding other potential problem because `sh` doesn't have a version + // argument. + let version_arg = match self { + #[cfg(unix)] + UserShell::Sh => return true, + #[cfg(windows)] + UserShell::Cmd => return true, + UserShell::Bash + | UserShell::Zsh + | UserShell::Fish + | UserShell::NuShell + | UserShell::Elvish => "--version", + UserShell::PowerShell => "-Version", + }; + + // Other wise run the shell with version argument to check if exists. + std::process::Command::new(self.bin()) + .arg(version_arg) + .output() + .is_ok_and(|o| o.status.success()) + } +} + +/// Creates [`std::process::Command`] running in the corresponding standard shell to each platform. +pub fn shell_std_command() -> std::process::Command { + UserConfiguration::get().shell.std_command() +} + +/// Creates [`tokio::process::Command`] running in the corresponding standard shell to each platform. +pub fn shell_tokio_command() -> tokio::process::Command { + UserConfiguration::get().shell.tokio_command() } /// Generates shell completion for the given shell printing them to stdout diff --git a/cli/src/spawner.rs b/cli/src/spawner.rs index 3fab8bcee..c610d85e8 100644 --- a/cli/src/spawner.rs +++ b/cli/src/spawner.rs @@ -10,6 +10,7 @@ use crate::{ JobsState, }; use anyhow::Context; +use console::style; use core::panic; use std::{ path::PathBuf, @@ -156,9 +157,9 @@ pub async fn spawn( if !stderr_line.trim().is_empty() { if opts.suppress_ui { - tracker.log(job_def, stderr_line); + tracker.log_err(job_def, stderr_line); } else { - tracker.msg(job_def, stderr_line); + tracker.msg_err(job_def, stderr_line); } } @@ -229,6 +230,10 @@ pub async fn spawn_blocking( /// This spawns a new task and return immediately showing that the job has been skipped pub async fn spawn_skip(job_def: JobDefinition, command: String) -> anyhow::Result { + if get_tracker().print_immediately() { + let msg = format!("Job '{}' has been skipped", job_def.job_title()); + println!("{}", style(msg).cyan().bold()); + } Ok(SpawnResult::create_for_skipped( job_def.job_title(), command, diff --git a/cli/src/tracker.rs b/cli/src/tracker.rs index e4230f6d5..c27715c2b 100644 --- a/cli/src/tracker.rs +++ b/cli/src/tracker.rs @@ -17,13 +17,13 @@ use tokio::sync::{ oneshot, }; -use crate::{cli_args::UiMode, jobs_runner::JobDefinition}; +use crate::{cli_args::UiMode, jobs_runner::JobDefinition, user_config::UserConfiguration}; const TIME_BAR_WIDTH: usize = 5; static TRACKER: OnceLock = OnceLock::new(); -#[derive(Clone, Debug)] +#[derive(Debug, Clone, Copy)] pub enum OperationResult { Success, Failed, @@ -79,6 +79,13 @@ enum LogTick { Shutdown, } +#[derive(Debug, Clone, Copy)] +/// Represents the standard output target. +enum OutputTarget { + Stdout, + Stderr, +} + #[derive(Clone, Debug)] pub struct Tracker { ui_tx: UnboundedSender, @@ -120,10 +127,15 @@ impl JobBarState { /// Initialize progress tracker with the given configurations. /// +/// * `ui_mode`: Optionally override the UI mode from user configurations, when not provided +/// then the value from [`UserConfiguration`] will be used. +/// /// # Panics /// /// This functions panics if it is initialized more than once. -pub fn init_tracker(ui_mode: UiMode) { +pub fn init_tracker(ui_mode: Option) { + let ui_mode = ui_mode.unwrap_or_else(|| UserConfiguration::get().ui_mode); + TRACKER .set(Tracker::new(ui_mode)) .expect("Progress Tracker can't be initialized more than once"); @@ -243,6 +255,9 @@ impl Tracker { let max = u64::MAX; let mut bars: BTreeMap = BTreeMap::new(); let mp = MultiProgress::new(); + // Reduces flickering. This works well if progress bars count doesn't change while running. + mp.set_move_cursor(true); + let start_time = Instant::now(); while let Some(tick) = rx.recv().await { match tick { @@ -467,8 +482,23 @@ impl Tracker { /// Send a message of the job to be shown on UI and saved in logs cache. pub fn msg(&self, job_def: JobDefinition, log: String) { + self.msg_intern(job_def, log, OutputTarget::Stdout) + } + + /// Send a error message of the job to be shown on UI and saved in logs cache. + pub fn msg_err(&self, job_def: JobDefinition, log: String) { + self.msg_intern(job_def, log, OutputTarget::Stderr) + } + + /// Internal implementation for sending messages (standard and error) + fn msg_intern(&self, job_def: JobDefinition, log: String, target: OutputTarget) { if self.print_immediately() { - println!("Job '{}': {}", job_def.job_title(), log.trim()); + let msg = format!("Job '{}': {}", job_def.job_title(), log.trim()); + match target { + OutputTarget::Stdout => println!("{msg}"), + OutputTarget::Stderr => eprintln!("{msg}"), + }; + return; } @@ -490,8 +520,22 @@ impl Tracker { /// Send a message of the job to be be saved within logs cache without showing it in UI. pub fn log(&self, job_def: JobDefinition, log: String) { + self.log_intern(job_def, log, OutputTarget::Stdout) + } + + /// Send a error message of the job to be be saved within logs cache without showing it in UI. + pub fn log_err(&self, job_def: JobDefinition, log: String) { + self.log_intern(job_def, log, OutputTarget::Stderr) + } + + /// Internal implementation for sending log messages (standard and error) + fn log_intern(&self, job_def: JobDefinition, log: String, target: OutputTarget) { if self.print_immediately() { - println!("Job '{}': {}", job_def.job_title(), log.trim()); + let msg = format!("Job '{}': {}", job_def.job_title(), log.trim()); + match target { + OutputTarget::Stdout => println!("{msg}"), + OutputTarget::Stderr => eprintln!("{msg}"), + } return; } diff --git a/cli/src/user_config.rs b/cli/src/user_config.rs new file mode 100644 index 000000000..61c31a49c --- /dev/null +++ b/cli/src/user_config.rs @@ -0,0 +1,167 @@ +//! Manages loading and providing the user configurations of the Build CLI Tool like the used +//! [`UserShell`] and the preferred [`UiMode`] + +use std::{fs::read_to_string, path::PathBuf, sync::OnceLock}; + +use anyhow::{ensure, Context}; +use console::style; +use serde::{Deserialize, Serialize}; + +use crate::{cli_args::UiMode, location::build_cli_home_dir, shell::UserShell}; + +static USER_CONFIGURATION: OnceLock = OnceLock::new(); + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +/// Represents the configuration of this tool on the user level, providing settings like +/// [`UserShell`] and [`UiMode`] besides methods to load this configurations from a file. +pub struct UserConfiguration { + #[serde(default)] + pub shell: UserShell, + #[serde(default)] + pub ui_mode: UiMode, +} + +impl UserConfiguration { + /// Loads and initializes the user configurations reading from the configuration file when + /// exists, otherwise loading the default configurations. + /// + /// # Panics + /// This method can't be called more than once. + pub fn init() -> anyhow::Result<()> { + assert!(USER_CONFIGURATION.get().is_none()); + + let config = Self::load().context("Error while loading user configuration")?; + + config + .validate() + .context("Validation of user configuration failed")?; + + USER_CONFIGURATION + .set(config) + .expect("User configuration can't be load more than once"); + + Ok(()) + } + + /// Provides a reference to the loaded configuration on the user levels. + /// + /// # Panics + /// This function panics if called before running [`UserConfiguration::init()`]. + pub fn get() -> &'static UserConfiguration { + USER_CONFIGURATION + .get() + .expect("Developer Error: User configuration getter called before initialization") + } + + /// Provides the path for the configuration file. + pub fn file_path() -> anyhow::Result { + let build_cli_dir = + build_cli_home_dir().context("Error while resolving build cli home directory")?; + + const USER_CONFIG_FILE_NAME: &str = "config.toml"; + + let config_path = build_cli_dir.join(USER_CONFIG_FILE_NAME); + + Ok(config_path) + } + + /// Serializes the default configurations to `toml` pretty format. + fn default_serialized() -> anyhow::Result { + let config = Self::default(); + let config_print = toml::to_string_pretty(&config) + .context("Error while serializing user configurations to toml format")?; + + Ok(config_print) + } + + /// Prints the default configurations in to `stdout` in `toml` format, to be used as a + /// reference in creating configuration files + pub fn print_default() -> anyhow::Result<()> { + let config_print = Self::default_serialized()?; + + println!("{config_print}"); + + Ok(()) + } + + /// Writes the default configurations to the configuration file if it doesn't exit, creating + /// all the directories to the file path if needed. + /// + /// # Errors + /// This function errors if the file already exists, besides other IO and serialization errors. + pub fn write_default_to_file() -> anyhow::Result<()> { + let file_path = + Self::file_path().context("Error while resolving user configuration file")?; + + ensure!( + !file_path.exists(), + "Abort because configuration file already exists. Path: {}", + file_path.display() + ); + + let config_serialized = Self::default_serialized()?; + + // Create directories if needed. + let parent_dir = file_path + .parent() + .expect("User config path always has parent directory"); + if !parent_dir.exists() { + std::fs::create_dir_all(parent_dir).with_context(|| { + format!("Error while creating directory: {}", parent_dir.display()) + })?; + } + + std::fs::write(&file_path, config_serialized.as_bytes()).with_context(|| { + format!( + "Error while writing user configuration for file. Path: {}", + file_path.display() + ) + })?; + + println!( + "{}", + style("Default user configuration written to file successfully").green() + ); + println!("Config file path: {}", file_path.display()); + + Ok(()) + } + + /// Loads the configuration from the config file if exists, otherwise it'll provide the default + /// user configurations + fn load() -> anyhow::Result { + let config_file = Self::file_path()?; + if !config_file.exists() { + return Ok(UserConfiguration::default()); + } + + let config_content = read_to_string(&config_file).with_context(|| { + format!( + "Error while reading user config file content. Path: {}", + config_file.display() + ) + })?; + + let config = toml::from_str(&config_content).with_context(|| { + format!( + "Error while parsing user configuration file. Path: {}", + config_file.display() + ) + })?; + + Ok(config) + } + + fn validate(&self) -> anyhow::Result<()> { + ensure!( + self.shell.exist(), + "Configured shell doesn't exist on the system. Shell: {}, Shell binary name: {}\n\ + Please check your configuration file in: {}", + self.shell, + self.shell.bin(), + Self::file_path().unwrap_or_default().display() + ); + + Ok(()) + } +}