From ce43c422fddccfe88c07ee22a2b8726dd0bd5f61 Mon Sep 17 00:00:00 2001 From: Danil Date: Thu, 11 Jul 2024 15:46:11 +0200 Subject: [PATCH] feat(zk_toolbox): Small adjustment for zk toolbox (#2424) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Add aliases Remove redundant l1 build Create .env files Update submodules ## Why ❔ ## Checklist - [ ] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] Code has been formatted via `zk fmt` and `zk lint`. --------- Signed-off-by: Danil Co-authored-by: Matías Ignacio González --- .github/workflows/ci-zk-toolbox-reusable.yml | 5 +++ core/lib/contracts/src/lib.rs | 4 +- zk_toolbox/crates/common/src/cmd.rs | 8 +++- zk_toolbox/crates/common/src/git.rs | 31 +++++++++++++ zk_toolbox/crates/common/src/lib.rs | 1 + zk_toolbox/crates/config/src/ecosystem.rs | 9 ++-- .../zk_inception/src/commands/chain/init.rs | 21 +++------ .../zk_inception/src/commands/chain/mod.rs | 2 + .../zk_inception/src/commands/containers.rs | 3 ++ .../src/commands/ecosystem/create.rs | 44 ++++++------------- .../src/commands/ecosystem/init.rs | 4 +- .../src/commands/ecosystem/mod.rs | 1 + .../src/commands/prover/init_bellman_cuda.rs | 18 +++----- .../zk_inception/src/commands/prover/mod.rs | 2 + zk_toolbox/crates/zk_inception/src/main.rs | 9 ++-- .../crates/zk_inception/src/messages.rs | 7 ++- .../zk_supervisor/src/commands/test/mod.rs | 4 +- zk_toolbox/crates/zk_supervisor/src/main.rs | 4 +- 18 files changed, 103 insertions(+), 74 deletions(-) create mode 100644 zk_toolbox/crates/common/src/git.rs diff --git a/.github/workflows/ci-zk-toolbox-reusable.yml b/.github/workflows/ci-zk-toolbox-reusable.yml index 102c3d56c331..7ff5eb3f1cf4 100644 --- a/.github/workflows/ci-zk-toolbox-reusable.yml +++ b/.github/workflows/ci-zk-toolbox-reusable.yml @@ -73,6 +73,7 @@ jobs: echo $(pwd)/bin >> $GITHUB_PATH echo IN_DOCKER=1 >> .env + - name: Start services run: | ci_localnet_up @@ -80,6 +81,10 @@ jobs: - name: Initialize ecosystem run: | + ci_run git config --global --add safe.directory /usr/src/zksync + ci_run git config --global --add safe.directory /usr/src/zksync/contracts/system-contracts + ci_run git config --global --add safe.directory /usr/src/zksync/contracts + ci_run zk_inception ecosystem init --deploy-paymaster --deploy-erc20 \ --deploy-ecosystem --l1-rpc-url=http://reth:8545 \ --server-db-url=postgres://postgres:notsecurepassword@postgres:5432 \ diff --git a/core/lib/contracts/src/lib.rs b/core/lib/contracts/src/lib.rs index 3374631a1814..b431085aad0b 100644 --- a/core/lib/contracts/src/lib.rs +++ b/core/lib/contracts/src/lib.rs @@ -39,8 +39,8 @@ const STATE_TRANSITION_CONTRACT_FILE: (&str, &str) = ( "IStateTransitionManager.sol/IStateTransitionManager.json", ); const ZKSYNC_HYPERCHAIN_CONTRACT_FILE: (&str, &str) = ( - "state-transition/", - "chain-interfaces/IZkSyncHyperchain.sol/IZkSyncHyperchain.json", + "state-transition/chain-interfaces", + "IZkSyncHyperchain.sol/IZkSyncHyperchain.json", ); const DIAMOND_INIT_CONTRACT_FILE: (&str, &str) = ( "state-transition", diff --git a/zk_toolbox/crates/common/src/cmd.rs b/zk_toolbox/crates/common/src/cmd.rs index a0a4b7d10ba9..ca0f285882a3 100644 --- a/zk_toolbox/crates/common/src/cmd.rs +++ b/zk_toolbox/crates/common/src/cmd.rs @@ -93,7 +93,13 @@ impl<'a> Cmd<'a> { let output = if global_config().verbose || self.force_run { logger::debug(format!("Running: {}", self.inner)); logger::new_empty_line(); - run_low_level_process_command(self.inner.into())? + let output = run_low_level_process_command(self.inner.into())?; + if let Ok(data) = String::from_utf8(output.stderr.clone()) { + if !data.is_empty() { + logger::info(data) + } + } + output } else { // Command will be logged manually. self.inner.set_quiet(true); diff --git a/zk_toolbox/crates/common/src/git.rs b/zk_toolbox/crates/common/src/git.rs new file mode 100644 index 000000000000..7ebedf0f6283 --- /dev/null +++ b/zk_toolbox/crates/common/src/git.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use xshell::{cmd, Shell}; + +use crate::cmd::Cmd; + +pub fn clone( + shell: &Shell, + path: PathBuf, + repository: &str, + name: &str, +) -> anyhow::Result { + let _dir = shell.push_dir(path); + Cmd::new(cmd!( + shell, + "git clone --recurse-submodules {repository} {name}" + )) + .run()?; + Ok(shell.current_dir().join(name)) +} + +pub fn submodule_update(shell: &Shell, link_to_code: PathBuf) -> anyhow::Result<()> { + let _dir_guard = shell.push_dir(link_to_code); + Cmd::new(cmd!( + shell, + "git submodule update --init --recursive +" + )) + .run()?; + Ok(()) +} diff --git a/zk_toolbox/crates/common/src/lib.rs b/zk_toolbox/crates/common/src/lib.rs index 022f8df7052e..2ab5c5f10e13 100644 --- a/zk_toolbox/crates/common/src/lib.rs +++ b/zk_toolbox/crates/common/src/lib.rs @@ -9,6 +9,7 @@ pub mod docker; pub mod ethereum; pub mod files; pub mod forge; +pub mod git; pub mod server; pub mod wallets; diff --git a/zk_toolbox/crates/config/src/ecosystem.rs b/zk_toolbox/crates/config/src/ecosystem.rs index de709c14f239..60ca22e9a9b0 100644 --- a/zk_toolbox/crates/config/src/ecosystem.rs +++ b/zk_toolbox/crates/config/src/ecosystem.rs @@ -103,7 +103,9 @@ impl EcosystemConfig { pub fn from_file(shell: &Shell) -> Result { let path = PathBuf::from(CONFIG_NAME); if !shell.path_exists(path) { - return Err(EcosystemConfigFromFileError::NotExists); + return Err(EcosystemConfigFromFileError::NotExists { + path: shell.current_dir(), + }); } let mut config = EcosystemConfig::read(shell, CONFIG_NAME) @@ -229,8 +231,9 @@ impl EcosystemConfig { /// Result of checking if the ecosystem exists. #[derive(Error, Debug)] pub enum EcosystemConfigFromFileError { - #[error("Ecosystem configuration not found")] - NotExists, + #[error("Ecosystem configuration not found (Could not find 'ZkStack.toml' in {path:?}: Make sure you have created an ecosystem & are in the new folder `cd path/to/ecosystem/name`)" + )] + NotExists { path: PathBuf }, #[error("Invalid ecosystem configuration")] InvalidConfig { source: anyhow::Error }, } diff --git a/zk_toolbox/crates/zk_inception/src/commands/chain/init.rs b/zk_toolbox/crates/zk_inception/src/commands/chain/init.rs index b30b20227d90..985885f30fe4 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/chain/init.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/chain/init.rs @@ -1,9 +1,8 @@ use anyhow::Context; use common::{ - cmd::Cmd, config::global_config, forge::{Forge, ForgeScriptArgs}, - logger, + git, logger, spinner::Spinner, }; use config::{ @@ -15,7 +14,7 @@ use config::{ traits::{ReadConfig, SaveConfig, SaveConfigWithBasePath}, ChainConfig, ContractsConfig, EcosystemConfig, }; -use xshell::{cmd, Shell}; +use xshell::Shell; use crate::{ accept_ownership::accept_admin, @@ -26,9 +25,9 @@ use crate::{ initialize_bridges, }, messages::{ - msg_initializing_chain, MSG_ACCEPTING_ADMIN_SPINNER, MSG_BUILDING_L1_CONTRACTS, - MSG_CHAIN_INITIALIZED, MSG_CHAIN_NOT_FOUND_ERR, MSG_GENESIS_DATABASE_ERR, - MSG_REGISTERING_CHAIN_SPINNER, MSG_SELECTED_CONFIG, + msg_initializing_chain, MSG_ACCEPTING_ADMIN_SPINNER, MSG_CHAIN_INITIALIZED, + MSG_CHAIN_NOT_FOUND_ERR, MSG_GENESIS_DATABASE_ERR, MSG_REGISTERING_CHAIN_SPINNER, + MSG_SELECTED_CONFIG, }, utils::forge::{check_the_balance, fill_forge_private_key}, }; @@ -43,6 +42,7 @@ pub(crate) async fn run(args: InitArgs, shell: &Shell) -> anyhow::Result<()> { logger::note(MSG_SELECTED_CONFIG, logger::object_to_string(&chain_config)); logger::info(msg_initializing_chain("")); + git::submodule_update(shell, config.link_to_code.clone())?; init(&mut args, shell, &config, &chain_config).await?; @@ -57,7 +57,6 @@ pub async fn init( chain_config: &ChainConfig, ) -> anyhow::Result<()> { copy_configs(shell, &ecosystem_config.link_to_code, &chain_config.configs)?; - build_l1_contracts(shell, ecosystem_config)?; let mut genesis_config = chain_config.get_genesis_config()?; genesis_config.update_from_chain_config(chain_config); @@ -161,11 +160,3 @@ async fn register_chain( contracts.set_chain_contracts(®ister_chain_output); Ok(()) } - -fn build_l1_contracts(shell: &Shell, ecosystem_config: &EcosystemConfig) -> anyhow::Result<()> { - let _dir_guard = shell.push_dir(ecosystem_config.path_to_foundry()); - let spinner = Spinner::new(MSG_BUILDING_L1_CONTRACTS); - Cmd::new(cmd!(shell, "yarn build")).run()?; - spinner.finish(); - Ok(()) -} diff --git a/zk_toolbox/crates/zk_inception/src/commands/chain/mod.rs b/zk_toolbox/crates/zk_inception/src/commands/chain/mod.rs index aabb0d714c53..fa4f81d76312 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/chain/mod.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/chain/mod.rs @@ -22,8 +22,10 @@ pub enum ChainCommands { /// Run server genesis Genesis(GenesisArgs), /// Initialize bridges on l2 + #[command(alias = "bridge")] InitializeBridges(ForgeScriptArgs), /// Initialize bridges on l2 + #[command(alias = "paymaster")] DeployPaymaster(ForgeScriptArgs), } diff --git a/zk_toolbox/crates/zk_inception/src/commands/containers.rs b/zk_toolbox/crates/zk_inception/src/commands/containers.rs index bba19fb89f94..b34b598afbe1 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/containers.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/containers.rs @@ -74,5 +74,8 @@ fn copy_dockerfile(shell: &Shell, link_to_code: PathBuf) -> anyhow::Result<()> { let data = docker_compose_text.replace(original_source, new_source); shell.write_file(DOCKER_COMPOSE_FILE, data)?; + // For some reasons our docker-compose sometimes required .env file while we are investigating this behaviour + // it's better to create file and don't make the life of customers harder + shell.write_file(".env", "")?; Ok(()) } diff --git a/zk_toolbox/crates/zk_inception/src/commands/ecosystem/create.rs b/zk_toolbox/crates/zk_inception/src/commands/ecosystem/create.rs index a94c189d2b2b..b7fdfee855f6 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/ecosystem/create.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/ecosystem/create.rs @@ -1,16 +1,13 @@ -use std::{ - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{path::PathBuf, str::FromStr}; use anyhow::bail; -use common::{cmd::Cmd, logger, spinner::Spinner}; +use common::{git, logger, spinner::Spinner}; use config::{ create_local_configs_dir, create_wallets, get_default_era_chain_id, traits::SaveConfigWithBasePath, EcosystemConfig, EcosystemConfigFromFileError, ZKSYNC_ERA_GIT_REPO, }; -use xshell::{cmd, Shell}; +use xshell::Shell; use crate::{ commands::{ @@ -22,7 +19,7 @@ use crate::{ }, }, messages::{ - MSG_CLONING_ERA_REPO_SPINNER, MSG_CREATED_ECOSYSTEM, MSG_CREATING_DEFAULT_CHAIN_SPINNER, + msg_created_ecosystem, MSG_CLONING_ERA_REPO_SPINNER, MSG_CREATING_DEFAULT_CHAIN_SPINNER, MSG_CREATING_ECOSYSTEM, MSG_CREATING_INITIAL_CONFIGURATIONS_SPINNER, MSG_ECOSYSTEM_ALREADY_EXISTS_ERR, MSG_ECOSYSTEM_CONFIG_INVALID_ERR, MSG_SELECTED_CONFIG, MSG_STARTING_CONTAINERS_SPINNER, @@ -35,7 +32,7 @@ pub fn run(args: EcosystemCreateArgs, shell: &Shell) -> anyhow::Result<()> { Err(EcosystemConfigFromFileError::InvalidConfig { .. }) => { bail!(MSG_ECOSYSTEM_CONFIG_INVALID_ERR) } - Err(EcosystemConfigFromFileError::NotExists) => create(args, shell)?, + Err(EcosystemConfigFromFileError::NotExists { .. }) => create(args, shell)?, }; Ok(()) @@ -55,12 +52,17 @@ fn create(args: EcosystemCreateArgs, shell: &Shell) -> anyhow::Result<()> { let link_to_code = if args.link_to_code.is_empty() { let spinner = Spinner::new(MSG_CLONING_ERA_REPO_SPINNER); - let link_to_code = clone_era_repo(shell)?; + let link_to_code = git::clone( + shell, + shell.current_dir(), + ZKSYNC_ERA_GIT_REPO, + "zksync-era", + )?; spinner.finish(); link_to_code } else { let path = PathBuf::from_str(&args.link_to_code)?; - update_submodules_recursive(shell, &path)?; + git::submodule_update(shell, path.clone())?; path }; @@ -109,26 +111,6 @@ fn create(args: EcosystemCreateArgs, shell: &Shell) -> anyhow::Result<()> { spinner.finish(); } - logger::outro(MSG_CREATED_ECOSYSTEM); - Ok(()) -} - -fn clone_era_repo(shell: &Shell) -> anyhow::Result { - Cmd::new(cmd!( - shell, - "git clone --recurse-submodules {ZKSYNC_ERA_GIT_REPO}" - )) - .run()?; - Ok(shell.current_dir().join("zksync-era")) -} - -fn update_submodules_recursive(shell: &Shell, link_to_code: &Path) -> anyhow::Result<()> { - let _dir_guard = shell.push_dir(link_to_code); - Cmd::new(cmd!( - shell, - "git submodule update --init --recursive -" - )) - .run()?; + logger::outro(msg_created_ecosystem(ecosystem_name)); Ok(()) } diff --git a/zk_toolbox/crates/zk_inception/src/commands/ecosystem/init.rs b/zk_toolbox/crates/zk_inception/src/commands/ecosystem/init.rs index 7579a4ac6231..4fa6c8c47d8d 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/ecosystem/init.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/ecosystem/init.rs @@ -8,7 +8,7 @@ use common::{ cmd::Cmd, config::global_config, forge::{Forge, ForgeScriptArgs}, - logger, + git, logger, spinner::Spinner, Prompt, }; @@ -54,6 +54,8 @@ use crate::{ pub async fn run(args: EcosystemInitArgs, shell: &Shell) -> anyhow::Result<()> { let ecosystem_config = EcosystemConfig::from_file(shell)?; + git::submodule_update(shell, ecosystem_config.link_to_code.clone())?; + let initial_deployment_config = match ecosystem_config.get_initial_deployment_config() { Ok(config) => config, Err(_) => create_initial_deployments_config(shell, &ecosystem_config.config)?, diff --git a/zk_toolbox/crates/zk_inception/src/commands/ecosystem/mod.rs b/zk_toolbox/crates/zk_inception/src/commands/ecosystem/mod.rs index e4074ed3070b..1e4b4f9bd2af 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/ecosystem/mod.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/ecosystem/mod.rs @@ -21,6 +21,7 @@ pub enum EcosystemCommands { /// deploying necessary contracts and performing on-chain operations Init(EcosystemInitArgs), /// Change the default chain + #[command(alias = "cd")] ChangeDefaultChain(ChangeDefaultChain), } diff --git a/zk_toolbox/crates/zk_inception/src/commands/prover/init_bellman_cuda.rs b/zk_toolbox/crates/zk_inception/src/commands/prover/init_bellman_cuda.rs index fd8efcd6eeb8..c6c5d3ef23d9 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/prover/init_bellman_cuda.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/prover/init_bellman_cuda.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use common::{check_prover_prequisites, cmd::Cmd, logger, spinner::Spinner}; +use common::{check_prover_prequisites, cmd::Cmd, git, logger, spinner::Spinner}; use config::{traits::SaveConfigWithBasePath, EcosystemConfig}; use xshell::{cmd, Shell}; @@ -38,19 +38,15 @@ pub(crate) async fn run(shell: &Shell, args: InitBellmanCudaArgs) -> anyhow::Res fn clone_bellman_cuda(shell: &Shell) -> anyhow::Result { let spinner = Spinner::new(MSG_CLONING_BELLMAN_CUDA_SPINNER); - Cmd::new(cmd!( + let path = git::clone( shell, - "git clone https://github.com/matter-labs/era-bellman-cuda" - )) - .run()?; + shell.current_dir(), + "https://github.com/matter-labs/era-bellman-cuda", + BELLMAN_CUDA_DIR, + )?; spinner.finish(); - Ok(shell - .current_dir() - .join(BELLMAN_CUDA_DIR) - .to_str() - .context(MSG_BELLMAN_CUDA_DIR_ERR)? - .to_string()) + Ok(path.to_str().context(MSG_BELLMAN_CUDA_DIR_ERR)?.to_string()) } fn build_bellman_cuda(shell: &Shell, bellman_cuda_dir: &str) -> anyhow::Result<()> { diff --git a/zk_toolbox/crates/zk_inception/src/commands/prover/mod.rs b/zk_toolbox/crates/zk_inception/src/commands/prover/mod.rs index d69e1e772e91..31c3a02e3806 100644 --- a/zk_toolbox/crates/zk_inception/src/commands/prover/mod.rs +++ b/zk_toolbox/crates/zk_inception/src/commands/prover/mod.rs @@ -15,10 +15,12 @@ pub enum ProverCommands { /// Initialize prover Init(Box), /// Generate setup keys + #[command(alias = "sk")] GenerateSK, /// Run prover Run(ProverRunArgs), /// Initialize bellman-cuda + #[command(alias = "cuda")] InitBellmanCuda(Box), } diff --git a/zk_toolbox/crates/zk_inception/src/main.rs b/zk_toolbox/crates/zk_inception/src/main.rs index 0f8ade3690a8..741d6df12e4e 100644 --- a/zk_toolbox/crates/zk_inception/src/main.rs +++ b/zk_toolbox/crates/zk_inception/src/main.rs @@ -33,20 +33,21 @@ struct Inception { #[derive(Subcommand, Debug)] pub enum InceptionSubcommands { /// Ecosystem related commands - #[command(subcommand)] + #[command(subcommand, alias = "e")] Ecosystem(EcosystemCommands), /// Chain related commands - #[command(subcommand)] + #[command(subcommand, alias = "c")] Chain(ChainCommands), /// Prover related commands - #[command(subcommand)] + #[command(subcommand, alias = "p")] Prover(ProverCommands), /// Run server Server(RunServerArgs), // Run External Node - #[command(subcommand)] + #[command(subcommand, alias = "en")] ExternalNode(ExternalNodeCommands), /// Run containers for local development + #[command(subcommand, alias = "up")] Containers, } diff --git a/zk_toolbox/crates/zk_inception/src/messages.rs b/zk_toolbox/crates/zk_inception/src/messages.rs index 7e27a9ac366d..d0b146c9a4c5 100644 --- a/zk_toolbox/crates/zk_inception/src/messages.rs +++ b/zk_toolbox/crates/zk_inception/src/messages.rs @@ -22,7 +22,11 @@ pub(super) const MSG_L1_NETWORK_PROMPT: &str = "Select the L1 network"; pub(super) const MSG_START_CONTAINERS_PROMPT: &str = "Do you want to start containers after creating the ecosystem?"; pub(super) const MSG_CREATING_ECOSYSTEM: &str = "Creating ecosystem"; -pub(super) const MSG_CREATED_ECOSYSTEM: &str = "Ecosystem created successfully"; + +pub fn msg_created_ecosystem(name: &str) -> String { + format!("Ecosystem {name} created successfully (All subsequent commands should be executed from ecosystem folder `cd {name}`)") +} + pub(super) const MSG_CLONING_ERA_REPO_SPINNER: &str = "Cloning zksync-era repository..."; pub(super) const MSG_CREATING_INITIAL_CONFIGURATIONS_SPINNER: &str = "Creating initial configurations..."; @@ -185,7 +189,6 @@ pub(super) const MSG_FAILED_TO_FIND_ECOSYSTEM_ERR: &str = "Failed to find ecosys /// Server related messages pub(super) const MSG_STARTING_SERVER: &str = "Starting server"; pub(super) const MSG_FAILED_TO_RUN_SERVER_ERR: &str = "Failed to start server"; -pub(super) const MSG_BUILDING_L1_CONTRACTS: &str = "Building L1 contracts..."; pub(super) const MSG_PREPARING_EN_CONFIGS: &str = "Preparing External Node config"; /// Forge utils related messages diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs index c930ab0cc0e2..857190dba3b0 100644 --- a/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs @@ -10,9 +10,9 @@ mod revert; #[derive(Subcommand, Debug)] pub enum TestCommands { - #[clap(about = MSG_INTEGRATION_TESTS_ABOUT)] + #[clap(about = MSG_INTEGRATION_TESTS_ABOUT, alias = "i")] Integration(IntegrationArgs), - #[clap(about = MSG_REVERT_TEST_ABOUT)] + #[clap(about = MSG_REVERT_TEST_ABOUT, alias = "r")] Revert(RevertArgs), } diff --git a/zk_toolbox/crates/zk_supervisor/src/main.rs b/zk_toolbox/crates/zk_supervisor/src/main.rs index 17ad5c577996..d6cc82c0994d 100644 --- a/zk_toolbox/crates/zk_supervisor/src/main.rs +++ b/zk_toolbox/crates/zk_supervisor/src/main.rs @@ -30,9 +30,9 @@ struct Supervisor { #[derive(Subcommand, Debug)] enum SupervisorSubcommands { - #[command(subcommand, about = MSG_SUBCOMMAND_DATABASE_ABOUT)] + #[command(subcommand, about = MSG_SUBCOMMAND_DATABASE_ABOUT, alias = "db")] Database(DatabaseCommands), - #[command(subcommand, about = MSG_SUBCOMMAND_TESTS_ABOUT)] + #[command(subcommand, about = MSG_SUBCOMMAND_TESTS_ABOUT, alias = "t")] Test(TestCommands), #[command(subcommand, about = MSG_SUBCOMMAND_CLEAN)] Clean(CleanCommands),