From 1ef1d07c99084437aa2597908597800fb21b496d Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Tue, 22 Oct 2024 12:36:55 +0200 Subject: [PATCH 01/16] Build CLI: User Configurations (in progress) * Implement user configurations as singleton with their load method * Provide an enum to represent the shell running by users with methods to provide command to run process in the selected shell. * Replace the implementation global commands function with the one from the user configurations * Extend benchmarks configurations error message to avoid confusion between different kinds of configurations. * Documentations --- cli/Cargo.lock | 158 ++++++++++++++++++++++++++++++++++---- cli/Cargo.toml | 1 + cli/src/benchmark/core.rs | 6 +- cli/src/cli_args.rs | 4 +- cli/src/location.rs | 38 +++++++++ cli/src/main.rs | 5 ++ cli/src/shell.rs | 98 +++++++++++++++++------ cli/src/user_config.rs | 85 ++++++++++++++++++++ 8 files changed, 350 insertions(+), 45 deletions(-) create mode 100644 cli/src/user_config.rs diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 7078c2783..3b04ece1f 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]] @@ -177,6 +177,7 @@ dependencies = [ "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..d56ddc84d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -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/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..e0cf1b376 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,7 +40,8 @@ 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)] +#[serde(rename_all = "snake_case")] /// 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'] diff --git a/cli/src/location.rs b/cli/src/location.rs index 082b2c82d..dc5a34f5b 100644 --- a/cli/src/location.rs +++ b/cli/src/location.rs @@ -56,3 +56,41 @@ 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, +/// creating it if needed. +pub fn chipmunk_home_dir() -> anyhow::Result { + let home_dir = dirs::home_dir() + .map(|home| home.join(".chipmunk")) + .context("Resolving home directory failed")?; + + if !home_dir.exists() { + std::fs::create_dir(&home_dir).with_context(|| { + format!( + "Error while craeting Chipmunk home directory. Path: {}", + home_dir.display() + ) + })?; + } + + Ok(home_dir) +} + +/// Return the path for the build CLI directory in Chipmunk home directory, creating it if needed. +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); + + if !build_cli_path.exists() { + std::fs::create_dir(&build_cli_path).with_context(|| { + format!( + "Error while craeting Chipmunk home directory. Path: {}", + build_cli_path.display() + ) + })?; + } + + Ok(build_cli_path) +} diff --git a/cli/src/main.rs b/cli/src/main.rs index ad0f6a02f..821588b2e 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -18,6 +18,7 @@ mod shell; mod spawner; mod target; mod tracker; +mod user_config; mod version; use anyhow::{bail, Error}; @@ -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(); diff --git a/cli/src/shell.rs b/cli/src/shell.rs index 1622ff9a4..16180d523 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -1,48 +1,96 @@ -//! 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 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 = "snake_case")] +/// Represents the shell running by users providing method to create commands to run process +/// on the given shell. +pub enum UserShell { + Sh, + Bash, + Zsh, + Fish, + NuShell, + Elvish, + Cmd, + PowerShell, +} - if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - cmd.arg("/C"); +impl Default for UserShell { + fn default() -> Self { + if cfg!(windows) { + UserShell::Cmd + } else { + UserShell::Sh + } + } +} - cmd - } else { - let mut cmd = Command::new("sh"); - 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.cmd()); + cmd.arg(self.arg()); cmd } -} -/// 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; - - if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - 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.cmd()); + cmd.arg(self.arg()); cmd - } else { - let mut cmd = Command::new("sh"); - cmd.arg("-c"); + } - cmd + /// Binary name for the shell + const fn cmd(self) -> &'static str { + match self { + UserShell::Sh => "sh", + UserShell::Bash => "bash", + UserShell::Zsh => "zsh", + UserShell::Fish => "fish", + UserShell::NuShell => "nu", + UserShell::Elvish => "elvish", + UserShell::Cmd => "cmd", + 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 { + UserShell::Sh + | UserShell::Bash + | UserShell::Zsh + | UserShell::Fish + | UserShell::NuShell + | UserShell::Elvish => "-c", + UserShell::Cmd => "/C", + UserShell::PowerShell => "-Command", + } } } +/// 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 pub fn generate_completion(shell: Shell) -> anyhow::Result<()> { let mut cmd = CargoCli::command(); diff --git a/cli/src/user_config.rs b/cli/src/user_config.rs new file mode 100644 index 000000000..28d263fa2 --- /dev/null +++ b/cli/src/user_config.rs @@ -0,0 +1,85 @@ +//! 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::Context; +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 { + pub shell: UserShell, + 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")?; + + 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) + } + + /// 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) + } +} From 671e4158bca7ed244b61a91702e24a4b9a8b313e Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Tue, 22 Oct 2024 14:10:14 +0200 Subject: [PATCH 02/16] Build CLI: Add user configuration sub commands * Add commands to print the path of the configurations file, printing the defaults to stdout and writing the default to configuration file if not exist * Remove the part of creating the needed directories for chipmunk and build CLI home directories. --- cli/src/cli_args.rs | 18 ++++++++++++ cli/src/location.rs | 23 ++------------- cli/src/main.rs | 20 +++++++++++-- cli/src/user_config.rs | 65 +++++++++++++++++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 24 deletions(-) diff --git a/cli/src/cli_args.rs b/cli/src/cli_args.rs index e0cf1b376..955bdfa9c 100644 --- a/cli/src/cli_args.rs +++ b/cli/src/cli_args.rs @@ -73,6 +73,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 @@ -180,3 +184,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 = "dump")] + /// 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 dc5a34f5b..a59577fd5 100644 --- a/cli/src/location.rs +++ b/cli/src/location.rs @@ -57,40 +57,21 @@ 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, -/// creating it if needed. +/// 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")?; - if !home_dir.exists() { - std::fs::create_dir(&home_dir).with_context(|| { - format!( - "Error while craeting Chipmunk home directory. Path: {}", - home_dir.display() - ) - })?; - } - Ok(home_dir) } -/// Return the path for the build CLI directory in Chipmunk home directory, creating it if needed. +/// 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); - if !build_cli_path.exists() { - std::fs::create_dir(&build_cli_path).with_context(|| { - format!( - "Error while craeting Chipmunk home directory. Path: {}", - build_cli_path.display() - ) - })?; - } - Ok(build_cli_path) } diff --git a/cli/src/main.rs b/cli/src/main.rs index 821588b2e..57d516c94 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -21,10 +21,10 @@ 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; @@ -104,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, diff --git a/cli/src/user_config.rs b/cli/src/user_config.rs index 28d263fa2..9ac8e36eb 100644 --- a/cli/src/user_config.rs +++ b/cli/src/user_config.rs @@ -3,7 +3,8 @@ use std::{fs::read_to_string, path::PathBuf, sync::OnceLock}; -use anyhow::Context; +use anyhow::{ensure, Context}; +use console::style; use serde::{Deserialize, Serialize}; use crate::{cli_args::UiMode, location::build_cli_home_dir, shell::UserShell}; @@ -58,6 +59,68 @@ impl UserConfiguration { 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 { From 9759091a8b17cdc1e0d1f91bf2fe82a708fd55d9 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Tue, 22 Oct 2024 14:46:05 +0200 Subject: [PATCH 03/16] Build CLI: Validation for user configuration shell * Ensure that the configured shell exists on the system before running any further commands. * Change method name from cmd to bin --- cli/src/cli_args.rs | 2 +- cli/src/shell.rs | 47 +++++++++++++++++++++++++++++++++++++----- cli/src/user_config.rs | 17 +++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/cli/src/cli_args.rs b/cli/src/cli_args.rs index 955bdfa9c..62ee9253c 100644 --- a/cli/src/cli_args.rs +++ b/cli/src/cli_args.rs @@ -41,7 +41,7 @@ pub struct Cli { // Link for the issue: https://github.com/clap-rs/clap/issues/4416. #[derive(Debug, Clone, Copy, Default, clap::ValueEnum, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[serde(rename_all = "kebab-case")] /// 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'] diff --git a/cli/src/shell.rs b/cli/src/shell.rs index 16180d523..246d693b0 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -1,7 +1,7 @@ //! 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; @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use crate::{cli_args::CargoCli, user_config::UserConfiguration}; #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] +#[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 { @@ -35,10 +35,25 @@ impl Default for UserShell { } } +impl Display for UserShell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UserShell::Sh => write!(f, "SH"), + 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::Cmd => write!(f, "Cmd"), + UserShell::PowerShell => write!(f, "Power Shell"), + } + } +} + 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.cmd()); + let mut cmd = std::process::Command::new(self.bin()); cmd.arg(self.arg()); cmd @@ -46,14 +61,14 @@ impl UserShell { /// 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.cmd()); + let mut cmd = tokio::process::Command::new(self.bin()); cmd.arg(self.arg()); cmd } /// Binary name for the shell - const fn cmd(self) -> &'static str { + pub const fn bin(self) -> &'static str { match self { UserShell::Sh => "sh", UserShell::Bash => "bash", @@ -79,6 +94,28 @@ impl UserShell { UserShell::PowerShell => "-Command", } } + + /// Checks if the shell exist on the system by running it with the version argument. + pub fn exist(self) -> bool { + std::process::Command::new(self.bin()) + .arg(self.version_arg()) + .output() + .is_ok_and(|o| o.status.success()) + } + + /// Provides the argument to show the version of the given shell. + const fn version_arg(self) -> &'static str { + match self { + UserShell::Sh + | UserShell::Bash + | UserShell::Zsh + | UserShell::Fish + | UserShell::NuShell + | UserShell::Elvish => "--version", + UserShell::Cmd => "/? ", + UserShell::PowerShell => "-Version", + } + } } /// Creates [`std::process::Command`] running in the corresponding standard shell to each platform. diff --git a/cli/src/user_config.rs b/cli/src/user_config.rs index 9ac8e36eb..e37591b3d 100644 --- a/cli/src/user_config.rs +++ b/cli/src/user_config.rs @@ -30,6 +30,10 @@ impl UserConfiguration { 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"); @@ -145,4 +149,17 @@ impl UserConfiguration { 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(()) + } } From 6d317fe5d9d8891da7ed537aaa2882c7da1d45f2 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Tue, 22 Oct 2024 15:20:37 +0200 Subject: [PATCH 04/16] Build CLI: Use user configs for UI mode * UI mode will be loaded from user configuration if not specified as CLI argument * Release command doesn't consider the UI mode in configuration file. --- cli/src/cli_args.rs | 21 ++++++++++++--------- cli/src/main.rs | 2 +- cli/src/tracker.rs | 9 +++++++-- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/cli/src/cli_args.rs b/cli/src/cli_args.rs index 62ee9253c..37294798e 100644 --- a/cli/src/cli_args.rs +++ b/cli/src/cli_args.rs @@ -41,22 +41,25 @@ pub struct Cli { // Link for the issue: https://github.com/clap-rs/clap/issues/4416. #[derive(Debug, Clone, Copy, Default, clap::ValueEnum, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] /// 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, } @@ -86,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 { @@ -102,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 { @@ -111,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 { @@ -127,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 diff --git a/cli/src/main.rs b/cli/src/main.rs index 57d516c94..bc7aa3b5d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -199,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(); diff --git a/cli/src/tracker.rs b/cli/src/tracker.rs index e4230f6d5..3c6ef61fe 100644 --- a/cli/src/tracker.rs +++ b/cli/src/tracker.rs @@ -17,7 +17,7 @@ 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; @@ -120,10 +120,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"); From bdfcfa09085808a908b555cf38fd6d9f40a319c1 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Tue, 22 Oct 2024 15:30:14 +0200 Subject: [PATCH 05/16] Build CLI: Default values for user configurations Use default values when configs don't exist to avoid forcing the users to specify all options --- cli/src/user_config.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/src/user_config.rs b/cli/src/user_config.rs index e37591b3d..61c31a49c 100644 --- a/cli/src/user_config.rs +++ b/cli/src/user_config.rs @@ -15,7 +15,9 @@ static USER_CONFIGURATION: OnceLock = OnceLock::new(); /// 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, } From 212bbdb7722fc7e53118b644d2ae2ee4da18b396 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Tue, 22 Oct 2024 16:10:22 +0200 Subject: [PATCH 06/16] Build CLI: Fix default shell in user configurations * Default shell is platform specific and they must not be shown on platform where are not supported (like `cmd` on Linux) * Set default shell to `zsh` on MacOs. * Default shell is installed by default and doesn't need an extra check, which can be faulty since `sh` doesn't provide a version argument --- cli/src/shell.rs | 70 ++++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/cli/src/shell.rs b/cli/src/shell.rs index 246d693b0..c2e89f61a 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -10,41 +10,40 @@ use serde::{Deserialize, Serialize}; use crate::{cli_args::CargoCli, user_config::UserConfiguration}; -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Default, 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)] + #[cfg_attr(all(unix, not(target_os = "macos")), default)] Sh, + + #[cfg(windows)] + #[default] + Cmd, + Bash, + #[cfg_attr(target_os = "macos", default)] Zsh, Fish, NuShell, Elvish, - Cmd, PowerShell, } -impl Default for UserShell { - fn default() -> Self { - if cfg!(windows) { - UserShell::Cmd - } else { - UserShell::Sh - } - } -} - impl Display for UserShell { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - UserShell::Sh => write!(f, "SH"), + #[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::Zsh => write!(f, "Zsh"), UserShell::Fish => write!(f, "Fish"), UserShell::NuShell => write!(f, "Nu Shell"), UserShell::Elvish => write!(f, "Envish"), - UserShell::Cmd => write!(f, "Cmd"), UserShell::PowerShell => write!(f, "Power Shell"), } } @@ -70,13 +69,15 @@ impl UserShell { /// 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::Cmd => "cmd", UserShell::PowerShell => "pwsh", } } @@ -84,37 +85,42 @@ impl UserShell { /// Argument provided by each shell to run the provided process command and its arguments. const fn arg(self) -> &'static str { match self { - UserShell::Sh - | UserShell::Bash + #[cfg(unix)] + UserShell::Sh => "-c", + #[cfg(windows)] + UserShell::Cmd => "/C", + UserShell::Bash | UserShell::Zsh | UserShell::Fish | UserShell::NuShell | UserShell::Elvish => "-c", - UserShell::Cmd => "/C", UserShell::PowerShell => "-Command", } } /// Checks if the shell exist on the system by running it with the version argument. pub fn exist(self) -> bool { - std::process::Command::new(self.bin()) - .arg(self.version_arg()) - .output() - .is_ok_and(|o| o.status.success()) - } - - /// Provides the argument to show the version of the given shell. - const fn version_arg(self) -> &'static str { - match self { - UserShell::Sh - | UserShell::Bash + // Default shell is always installed on their respecting operating system and doesn'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::Cmd => "/? ", 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()) } } From 48a08528fb42897c539f48467b233f9428e9d31b Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Wed, 23 Oct 2024 09:55:59 +0200 Subject: [PATCH 07/16] Build CLI: User config integration tests & Name alias * Add test for user configuration commands * Change command naming alias --- cli/integration_tests/run_all.py | 9 ++++++++ cli/integration_tests/user_config.py | 33 ++++++++++++++++++++++++++++ cli/src/cli_args.rs | 2 +- 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 cli/integration_tests/user_config.py 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/cli_args.rs b/cli/src/cli_args.rs index 37294798e..3c2f1510b 100644 --- a/cli/src/cli_args.rs +++ b/cli/src/cli_args.rs @@ -193,7 +193,7 @@ pub enum UserConfigCommand { #[clap(visible_alias = "path")] /// Prints the path to the user configurations file. PrintPath, - #[clap(name = "print-default", visible_alias = "dump")] + #[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, From 2ae6be0cc4505b741ba7c124fb3649b8f3b566dc Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Wed, 23 Oct 2024 10:53:53 +0200 Subject: [PATCH 08/16] Build CLI: Default shell from env variables on unix * Use environment variables to figure out the default shells on unix-based platforms * Use `cmd` on Windows --- cli/src/shell.rs | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/cli/src/shell.rs b/cli/src/shell.rs index c2e89f61a..2fce2fb42 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -1,7 +1,7 @@ //! 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::{fmt::Display, io}; +use std::{fmt::Display, io, sync::LazyLock}; use anyhow::Context; use clap::CommandFactory; @@ -10,21 +10,16 @@ use serde::{Deserialize, Serialize}; use crate::{cli_args::CargoCli, user_config::UserConfiguration}; -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[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)] - #[cfg_attr(all(unix, not(target_os = "macos")), default)] Sh, - #[cfg(windows)] - #[default] Cmd, - Bash, - #[cfg_attr(target_os = "macos", default)] Zsh, Fish, NuShell, @@ -49,6 +44,40 @@ impl Display for UserShell { } } +#[cfg(unix)] +impl Default for UserShell { + fn default() -> Self { + // 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); + + shell + }); + + *DEFAULT_SHELL + } +} + +#[cfg(windows)] +impl Default for UserShell { + fn default() -> Self { + UserShell::Cmd + } +} + impl UserShell { /// Provides [`std::process::Command`] to run a process on the given shell. pub fn std_command(self) -> std::process::Command { From ceda34e42d282ff9043326df378ce6e0ce8e717c Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Wed, 23 Oct 2024 11:26:13 +0200 Subject: [PATCH 09/16] Build CLI User Config: README & Version & Changelog * Update README file with configuration section * Increase tool version & Update Changelog * Move use statement to fix warning on Windows --- cli/CHANGELOG.md | 7 +++++++ cli/Cargo.lock | 2 +- cli/Cargo.toml | 2 +- cli/README.md | 33 +++++++++++++++++++++++++++++++++ cli/src/shell.rs | 5 +++-- 5 files changed, 45 insertions(+), 4 deletions(-) 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 3b04ece1f..5ad830cc8 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -170,7 +170,7 @@ checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cargo-chipmunk" -version = "0.2.7" +version = "0.2.8" dependencies = [ "anyhow", "clap", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d56ddc84d..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" 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/src/shell.rs b/cli/src/shell.rs index 2fce2fb42..f25b2e03a 100644 --- a/cli/src/shell.rs +++ b/cli/src/shell.rs @@ -1,7 +1,7 @@ //! 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::{fmt::Display, io, sync::LazyLock}; +use std::{fmt::Display, io}; use anyhow::Context; use clap::CommandFactory; @@ -47,6 +47,7 @@ impl Display for UserShell { #[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(|| { @@ -129,7 +130,7 @@ impl UserShell { /// Checks if the shell exist on the system by running it with the version argument. pub fn exist(self) -> bool { - // Default shell is always installed on their respecting operating system and doesn't need + // 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 { From abe48a028953a0f987baee30e42ee4d6c4de7104 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Thu, 31 Oct 2024 14:17:32 +0100 Subject: [PATCH 10/16] CI: Remove specified targets to run tests on Build CLI tool will automatically pick all targets that have tests if no target is specified. This will fix ignoring targets the not mentioned targets like CLI --- .github/workflows/lint_master.yml | 2 +- .github/workflows/pullrequest_check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint_master.yml b/.github/workflows/lint_master.yml index 703888b76..46d5b12ee 100644 --- a/.github/workflows/lint_master.yml +++ b/.github/workflows/lint_master.yml @@ -92,4 +92,4 @@ jobs: - name: install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Execute tests - run: cargo chipmunk test core wrapper wasm -u print \ No newline at end of file + run: cargo chipmunk test -u print diff --git a/.github/workflows/pullrequest_check.yml b/.github/workflows/pullrequest_check.yml index fa5644cdb..a4994701d 100644 --- a/.github/workflows/pullrequest_check.yml +++ b/.github/workflows/pullrequest_check.yml @@ -94,4 +94,4 @@ jobs: - name: install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: Execute tests - run: cargo chipmunk test core wrapper wasm -u print \ No newline at end of file + run: cargo chipmunk test -u print From eda7ae3e07da5ec15b90c820d4a64f9bb7d8fd92 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Thu, 31 Oct 2024 14:24:53 +0100 Subject: [PATCH 11/16] CI: Install wasm-pack Using Cargo Installing using cargo will ensure it will work on all platform in addition to caching it with the action cargo-cache --- .github/workflows/lint_master.yml | 2 +- .github/workflows/pullrequest_check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint_master.yml b/.github/workflows/lint_master.yml index 46d5b12ee..83dec0857 100644 --- a/.github/workflows/lint_master.yml +++ b/.github/workflows/lint_master.yml @@ -90,6 +90,6 @@ 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 -u print diff --git a/.github/workflows/pullrequest_check.yml b/.github/workflows/pullrequest_check.yml index a4994701d..b2c88c44c 100644 --- a/.github/workflows/pullrequest_check.yml +++ b/.github/workflows/pullrequest_check.yml @@ -92,6 +92,6 @@ 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 -u print From 11438bb8b37b776377e3782c0ad048dc11ea0d62 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Thu, 31 Oct 2024 16:07:33 +0100 Subject: [PATCH 12/16] Build CLI: Print error logs to stderr with immediate * Print logs of errors to stderr with UI mode `immediate` to improve highlighting the errors within GitHub actions * Add missing copy trait to small enum --- cli/src/spawner.rs | 4 ++-- cli/src/tracker.rs | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/cli/src/spawner.rs b/cli/src/spawner.rs index 3fab8bcee..09a8451a7 100644 --- a/cli/src/spawner.rs +++ b/cli/src/spawner.rs @@ -156,9 +156,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); } } diff --git a/cli/src/tracker.rs b/cli/src/tracker.rs index 3c6ef61fe..bd36039ab 100644 --- a/cli/src/tracker.rs +++ b/cli/src/tracker.rs @@ -23,7 +23,7 @@ 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, @@ -472,8 +479,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; } @@ -495,8 +517,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; } From 60cac9107bf2e4f7c771be16f66139d64d7bb1a1 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Thu, 31 Oct 2024 16:20:45 +0100 Subject: [PATCH 13/16] CI: Change UI option for logs to `immediate` Show logs immediately on CI checks --- .github/workflows/lint_master.yml | 4 ++-- .github/workflows/pullrequest_check.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint_master.yml b/.github/workflows/lint_master.yml index 83dec0857..6a97458bc 100644 --- a/.github/workflows/lint_master.yml +++ b/.github/workflows/lint_master.yml @@ -44,7 +44,7 @@ jobs: - name: install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: JS/TS linting - run: cargo chipmunk lint -u print + run: cargo chipmunk lint -u immediate - name: TypeScript Check - Client Application working-directory: application/client run: yarn run check @@ -92,4 +92,4 @@ jobs: - name: install wasm-pack run: cargo install wasm-pack - name: Execute tests - run: cargo chipmunk test -u print + run: cargo chipmunk test -u immediate diff --git a/.github/workflows/pullrequest_check.yml b/.github/workflows/pullrequest_check.yml index b2c88c44c..237517ee0 100644 --- a/.github/workflows/pullrequest_check.yml +++ b/.github/workflows/pullrequest_check.yml @@ -45,7 +45,7 @@ jobs: - name: install wasm-pack run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - name: JS/TS linting - run: cargo chipmunk lint -u print + run: cargo chipmunk lint -u immediate - name: TypeScript Check - Client Application working-directory: application/client run: yarn run check @@ -94,4 +94,4 @@ jobs: - name: install wasm-pack run: cargo install wasm-pack - name: Execute tests - run: cargo chipmunk test -u print + run: cargo chipmunk test -u immediate From 3dadeb6217d699c1ca0c3f5ceb209a7c1ca11df4 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Mon, 4 Nov 2024 07:43:23 +0100 Subject: [PATCH 14/16] Build CLI: Print skipped job in immediate mode * In immediate mode there is no other indicator that the jobs has been skipped without progress bars and reports --- cli/src/spawner.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/src/spawner.rs b/cli/src/spawner.rs index 09a8451a7..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, @@ -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, From 132ef975633aa05885022e6e1f7d483fbbc5df77 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Mon, 4 Nov 2024 09:04:47 +0100 Subject: [PATCH 15/16] Build CLI: Reduce UI flickering & Fix log message --- cli/src/main.rs | 2 +- cli/src/tracker.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/src/main.rs b/cli/src/main.rs index bc7aa3b5d..9f77221d7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -256,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/tracker.rs b/cli/src/tracker.rs index bd36039ab..c27715c2b 100644 --- a/cli/src/tracker.rs +++ b/cli/src/tracker.rs @@ -255,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 { From 0d646b7e8a42c338f29db7a30e40940b0b3d9a87 Mon Sep 17 00:00:00 2001 From: Ammar Abou Zor Date: Mon, 4 Nov 2024 11:00:59 +0100 Subject: [PATCH 16/16] CI: Set timeout on lint & test steps to 30 minutes * Use cargo to install wasm-pack packages to ensure cross-platform support and utilize caching with cargo-cache action --- .github/workflows/lint_master.yml | 4 +++- .github/workflows/pullrequest_check.yml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint_master.yml b/.github/workflows/lint_master.yml index 6a97458bc..14c096918 100644 --- a/.github/workflows/lint_master.yml +++ b/.github/workflows/lint_master.yml @@ -42,8 +42,9 @@ 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 + timeout-minutes: 30 run: cargo chipmunk lint -u immediate - name: TypeScript Check - Client Application working-directory: application/client @@ -92,4 +93,5 @@ jobs: - name: install wasm-pack run: cargo install wasm-pack - name: Execute tests + timeout-minutes: 30 run: cargo chipmunk test -u immediate diff --git a/.github/workflows/pullrequest_check.yml b/.github/workflows/pullrequest_check.yml index 237517ee0..9e9d828ec 100644 --- a/.github/workflows/pullrequest_check.yml +++ b/.github/workflows/pullrequest_check.yml @@ -43,8 +43,9 @@ 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 + timeout-minutes: 30 run: cargo chipmunk lint -u immediate - name: TypeScript Check - Client Application working-directory: application/client @@ -94,4 +95,5 @@ jobs: - name: install wasm-pack run: cargo install wasm-pack - name: Execute tests + timeout-minutes: 30 run: cargo chipmunk test -u immediate