From e4421c4c27bc52a01690010eed4fbf942dadbf5f Mon Sep 17 00:00:00 2001 From: janhencic Date: Wed, 8 Jan 2025 17:52:42 +0100 Subject: [PATCH 1/4] Move `long_help` and `help` clap attributes to doc comments --- src/cli.rs | 71 ++++++++------- src/command/apply.rs | 179 +++++++++++++++++-------------------- src/command/apply_local.rs | 53 ++++++----- src/command/eval.rs | 15 ++-- src/command/exec.rs | 49 +++++----- src/nix/node_filter.rs | 23 +++-- 6 files changed, 182 insertions(+), 208 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9068da6a..bcc6386f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -101,70 +101,69 @@ impl std::fmt::Display for ColorWhen { long_about = LONG_ABOUT, )] struct Opts { + /// Path to a Hive expression, a flake.nix, or a Nix Flake URI #[arg( short = 'f', long, value_name = "CONFIG", - help = "Path to a Hive expression, a flake.nix, or a Nix Flake URI", long_help = CONFIG_HELP, display_order = HELP_ORDER_FIRST, global = true, )] config: Option, - #[arg( - long, - help = "Show debug information for Nix commands", - long_help = "Passes --show-trace to Nix commands", - global = true - )] + + /// Show debug information for Nix commands + /// + /// Passes --show-trace to Nix commands + #[arg(long, global = true)] show_trace: bool, - #[arg( - long, - help = "Allow impure expressions", - long_help = "Passes --impure to Nix commands", - global = true - )] + + /// Allow impure expressions + /// + /// Passes --impure to Nix commands + #[arg(long, global = true)] impure: bool, + + /// Passes an arbitrary option to Nix commands + /// + /// This only works when building locally. #[arg( long, - help = "Passes an arbitrary option to Nix commands", - long_help = r#"Passes arbitrary options to Nix commands - -This only works when building locally. -"#, global = true, num_args = 2, value_names = ["NAME", "VALUE"], )] nix_option: Vec, - #[arg( - long, - default_value_t, - help = "Use direct flake evaluation (experimental)", - long_help = r#"If enabled, flakes will be evaluated using `nix eval`. This requires the flake to depend on Colmena as an input and expose a compatible `colmenaHive` output: - - outputs = { self, colmena, ... }: { - colmenaHive = colmena.lib.makeHive self.outputs.colmena; - colmena = ...; - }; -This is an experimental feature."#, - global = true - )] + /// Use direct flake evaluation (experimental) + /// + /// If enabled, flakes will be evaluated using `nix eval`. This requires the flake to depend on + /// Colmena as an input and expose a compatible `colmenaHive` output: + /// + /// outputs = { self, colmena, ... }: { + /// colmenaHive = colmena.lib.makeHive self.outputs.colmena; + /// colmena = ...; + /// }; + /// + /// This is an experimental feature. + #[arg(long, default_value_t, global = true)] experimental_flake_eval: bool, + + /// When to colorize the output + /// + /// By default, Colmena enables colorized output when the terminal supports it. + /// + /// It's also possible to specify the preference using environment variables. See + /// . #[arg( long, value_name = "WHEN", default_value_t, global = true, display_order = HELP_ORDER_LOW, - help = "When to colorize the output", - long_help = r#"When to colorize the output. By default, Colmena enables colorized output when the terminal supports it. - -It's also possible to specify the preference using environment variables. See . -"#, )] color: ColorWhen, + #[command(subcommand)] command: Command, } diff --git a/src/command/apply.rs b/src/command/apply.rs index 33395dbd..931f940b 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -13,106 +13,94 @@ use crate::progress::SimpleProgressOutput; #[derive(Debug, Args)] pub struct DeployOpts { - #[arg( - value_name = "LIMIT", - default_value_t, - long, - help = "Evaluation node limit", - long_help = r#"Limits the maximum number of hosts to be evaluated at once. - -The evaluation process is RAM-intensive. The default behavior is to limit the maximum number of host evaluated at the same time based on naive heuristics. - -Set to 0 to disable the limit. -"# - )] + /// Evaluation node limit + /// + /// Limits the maximum number of hosts to be evaluated at once. The evaluation process is + /// RAM-intensive. The default behavior is to limit the maximum number of hosts evaluated at + /// the same time based on naive heuristics. + /// + /// Set to 0 to disable the limit. + #[arg(value_name = "LIMIT", default_value_t, long)] eval_node_limit: EvaluationNodeLimit, - #[arg( - value_name = "LIMIT", - default_value_t = 10, - long, - short, - help = "Deploy parallelism limit", - long_help = r#"Limits the maximum number of hosts to be deployed in parallel. - -Set to 0 to disable parallelism limit. -"# - )] + + /// Deploy parallelism limit + /// + /// Limits the maximum number of hosts to be deployed in parallel. + /// + /// Set to 0 to disable parallelism limit. + #[arg(value_name = "LIMIT", default_value_t = 10, long, short)] parallel: usize, - #[arg( - long, - help = "Create GC roots for built profiles", - long_help = r#"Create GC roots for built profiles. -The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. -The links will be created under .gcroots in the directory the Hive configuration is located. -"# - )] + /// Create GC roots for built profiles. + /// + /// The built system profiles will be added as GC roots so that they will not be + /// removed by the garbage collector. The links will be created under `.gcroots` + /// in the directory the Hive configuration is located. + #[arg(long)] keep_result: bool, - #[arg( - short, - long, - help = "Be verbose", - long_help = "Deactivates the progress spinner and prints every line of output." - )] + + /// Be verbose + /// + /// Deactivates the progress spinner and prints every line of output. + #[arg(short, long)] verbose: bool, - #[arg( - long, - help = "Do not upload keys", - long_help = r#"Do not upload secret keys set in `deployment.keys`. -By default, Colmena will upload keys set in `deployment.keys` before deploying the new profile on a node. -To upload keys without building or deploying the rest of the configuration, use `colmena upload-keys`. -"# - )] + /// Do not upload keys + /// + /// By default, Colmena will upload secret keys set in `deployment.keys` before deploying + /// the new profile on a node. To upload keys without building or deploying the rest + /// of the configuration, use `colmena upload-keys`. + #[arg(long)] no_keys: bool, - #[arg( - long, - help = "Reboot nodes after activation", - long_help = "Reboots nodes after activation and waits for them to come back up." - )] + + /// Reboot nodes after activation + /// + /// Reboots nodes after activation and waits for them to come back up. + #[arg(long)] reboot: bool, - #[arg( - long, - alias = "no-substitutes", - help = "Do not use substitutes", - long_help = "Disables the use of substituters when copying closures to the remote host." - )] + + /// Do not use substitutes + /// + /// Disables the use of substituters when copying closures to the remote host. + #[arg(long, alias = "no-substitutes")] no_substitute: bool, - #[arg( - long, - help = "Do not use gzip", - long_help = "Disables the use of gzip when copying closures to the remote host." - )] + + /// Do not use gzip + /// + /// Disables the use of gzip when copying closures to the remote host. + #[arg(long)] no_gzip: bool, - #[arg( - long, - help = "Build the system profiles on the target nodes", - long_help = r#"Build the system profiles on the target nodes themselves. - -If enabled, the system profiles will be built on the target nodes themselves, not on the host running Colmena itself. -This overrides per-node perferences set in `deployment.buildOnTarget`. -To temporarily disable remote build on all nodes, use `--no-build-on-target`. -"# - )] + + /// Build the system profiles on the target nodes + /// + /// If enabled, the system profiles will be built on the target nodes themselves, + /// not on the host running Colmena. This overrides per-node preferences set in + /// `deployment.buildOnTarget`. To temporarily disable remote build on all nodes, + /// use `--no-build-on-target`. + #[arg(long)] build_on_target: bool, + #[arg(long, hide = true)] no_build_on_target: bool, - #[arg( - long, - help = "Ignore all targeted nodes deployment.replaceUnknownProfiles setting", - long_help = r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch -will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."# - )] + + /// Ignore all targeted nodes `deployment.replaceUnknownProfiles` setting + /// + /// If `deployment.replaceUnknownProfiles` is set for a target, using this switch + /// will treat `deployment.replaceUnknownProfiles` as though it was set to `true` + /// and perform unknown profile replacement. + #[arg(long)] force_replace_unknown_profiles: bool, - #[arg( - long, - default_value_t, - help = "The evaluator to use (experimental)", - long_help = r#"If set to `chunked` (default), evaluation of nodes will happen in batches. If set to `streaming`, the experimental streaming evaluator (nix-eval-jobs) will be used and nodes will be evaluated in parallel. -This is an experimental feature."# - )] + /// The evaluator to use (experimental) + /// + /// If set to `chunked` (default), evaluation of nodes will happen in batches. If + /// set to `streaming`, the experimental streaming evaluator (nix-eval-jobs) will + /// be used and nodes will be evaluated in parallel. + /// + /// This is an experimental feature. + #[arg(long, default_value_t)] evaluator: EvaluatorType, + #[command(flatten)] node_filter: NodeFilterOpts, } @@ -120,22 +108,23 @@ This is an experimental feature."# #[derive(Debug, Args)] #[command(name = "apply", about = "Apply configurations on remote machines")] pub struct Opts { + /// Deployment goal + /// + /// Same as the targets for switch-to-configuration, with the following extra + /// pseudo-goals: + /// + /// - build: Only build the system profiles + /// - push: Only copy the closures to remote nodes + /// - keys: Only upload the keys to the remote nodes + /// + /// `switch` is the default goal unless `--reboot` is passed, in which case + /// `boot` is the default. #[arg( - help = "Deployment goal", - long_help = r#"The goal of the deployment. - -Same as the targets for switch-to-configuration, with the following extra pseudo-goals: - -- build: Only build the system profiles -- push: Only copy the closures to remote nodes -- keys: Only upload the keys to the remote nodes - -`switch` is the default goal unless `--reboot` is passed, in which case `boot` is the default. -"#, default_value_t, default_value_if("reboot", ArgPredicate::IsPresent, Some("boot")) )] pub goal: Goal, + #[command(flatten)] pub deploy: DeployOpts, } diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index 9ec38dca..3656393d 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -17,39 +17,36 @@ use crate::progress::SimpleProgressOutput; about = "Apply configurations on the local machine" )] pub struct Opts { - #[arg( - help = "Deployment goal", - value_name = "GOAL", - default_value_t, - long_help = "Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local." - )] + /// Deployment goal + /// + /// Same as the targets for switch-to-configuration. + /// "push" is noop in apply-local. + #[arg(value_name = "GOAL", default_value_t)] goal: Goal, - #[arg(long, help = "Attempt to escalate privileges if not run as root")] + + /// Attempt to escalate privileges if not run as root + #[arg(long)] sudo: bool, - #[arg( - short, - long, - help = "Be verbose", - long_help = "Deactivates the progress spinner and prints every line of output." - )] + + /// Be verbose + /// + /// Deactivates the progress spinner and prints every line of output. + #[arg(short, long)] verbose: bool, - #[arg( - long, - help = "Do not deploy keys", - long_help = r#"Do not deploy secret keys set in `deployment.keys`. - -By default, Colmena will deploy keys set in `deployment.keys` before activating the profile on this host. -"# - )] + + /// Do not deploy keys + /// + /// Do not deploy secret keys set in `deployment.keys`. By default, Colmena will deploy keys + /// set in `deployment.keys` before activating the profile on this host. + #[arg(long)] no_keys: bool, - #[arg(long, help = "Override the node name to use")] + + /// Override the node name to use + #[arg(long)] node: Option, - #[arg( - long, - value_name = "COMMAND", - hide = true, - help = "Removed: Configure deployment.privilegeEscalationCommand in node configuration" - )] + + /// Removed: Configure deployment.privilegeEscalationCommand in node configuration + #[arg(long, value_name = "COMMAND", hide = true)] sudo_command: Option, } diff --git a/src/command/eval.rs b/src/command/eval.rs index 3a7259f8..54acb7f0 100644 --- a/src/command/eval.rs +++ b/src/command/eval.rs @@ -20,15 +20,16 @@ For example, to retrieve the configuration of one node, you may write something "# )] pub struct Opts { - #[arg(short = 'E', value_name = "EXPRESSION", help = "The Nix expression")] + /// The Nix expression + #[arg(short = 'E', value_name = "EXPRESSION")] expression: Option, - #[arg(long, help = "Actually instantiate the expression")] + + /// Actually instantiate the expression + #[arg(long)] instantiate: bool, - #[arg( - value_name = "FILE", - help = "The .nix file containing the expression", - conflicts_with("expression") - )] + + /// The .nix file containing the expression + #[arg(value_name = "FILE", conflicts_with("expression"))] expression_file: Option, } diff --git a/src/command/exec.rs b/src/command/exec.rs index 5f3616a3..67a03fd5 100644 --- a/src/command/exec.rs +++ b/src/command/exec.rs @@ -16,39 +16,30 @@ use crate::util; #[derive(Debug, Args)] #[command(name = "exec", about = "Run a command on remote machines")] pub struct Opts { - #[arg( - short, - long, - default_value_t = 0, - value_name = "LIMIT", - help = "Deploy parallelism limit", - long_help = r#"Limits the maximum number of hosts to run the command in parallel. - -In `colmena exec`, the parallelism limit is disabled (0) by default. -"# - )] + /// Deploy parallelism limit + /// + /// Limits the maximum number of hosts to run the command in parallel. + /// + /// In `colmena exec`, the parallelism limit is disabled (0) by default. + #[arg(short, long, default_value_t = 0, value_name = "LIMIT")] parallel: usize, - #[arg( - short, - long, - help = "Be verbose", - long_help = "Deactivates the progress spinner and prints every line of output." - )] + + /// Be verbose + /// + /// Deactivates the progress spinner and prints every line of output. + #[arg(short, long)] verbose: bool, + #[command(flatten)] nodes: NodeFilterOpts, - #[arg( - trailing_var_arg = true, - required = true, - value_name = "COMMAND", - help = "Command", - long_help = r#"Command to run - -It's recommended to use -- to separate Colmena options from the command to run. For example: - - colmena exec --on @routers -- tcpdump -vni any ip[9] == 89 -"# - )] + + /// Command to run + /// + /// It's recommended to use -- to separate Colmena options from the command to run. For + /// example: + /// + /// colmena exec --on @routers -- tcpdump -vni any ip[9] == 89 + #[arg(trailing_var_arg = true, required = true, value_name = "COMMAND")] command: Vec, } diff --git a/src/nix/node_filter.rs b/src/nix/node_filter.rs index 6e459fa2..6dc6a1b1 100644 --- a/src/nix/node_filter.rs +++ b/src/nix/node_filter.rs @@ -12,19 +12,16 @@ use super::{ColmenaError, ColmenaResult, NodeConfig, NodeName}; #[derive(Debug, Default, Args)] pub struct NodeFilterOpts { - #[arg( - long, - value_name = "NODES", - help = "Node selector", - long_help = r#"Select a list of nodes to deploy to. - -The list is comma-separated and globs are supported. To match tags, prepend the filter by @. Valid examples: - -- host1,host2,host3 -- edge-* -- edge-*,core-* -- @a-tag,@tags-can-have-*"# - )] + /// Node selector + /// + /// Select a list of nodes to deploy to. The list is comma-separated and globs are supported. + /// To match tags, prepend the filter by @. Valid examples: + /// + /// - host1,host2,host3 + /// - edge-* + /// - edge-*,core-* + /// - @a-tag,@tags-can-have-* + #[arg(long, value_name = "NODES")] pub on: Option, } From abcd08b4d175289c198c318f78caddc51ff1091d Mon Sep 17 00:00:00 2001 From: janhencic Date: Wed, 8 Jan 2025 17:55:26 +0100 Subject: [PATCH 2/4] Enable wrap_help feature in Clap --- Cargo.lock | 11 +++++++++++ Cargo.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f417affe..436735a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -193,6 +193,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -1191,6 +1192,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +dependencies = [ + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tinystr" version = "0.7.6" diff --git a/Cargo.toml b/Cargo.toml index c23670c5..22f348c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" [dependencies] async-stream = "0.3.5" async-trait = "0.1.68" -clap = { version = "4.3", features = ["derive"] } +clap = { version = "4.3", features = ["derive", "wrap_help"] } clap_complete = "4.3" clicolors-control = "1" console = "0.15.5" From 97e8dc0e02461e32655d621edeefc42c8b2f3419 Mon Sep 17 00:00:00 2001 From: janhencic Date: Wed, 8 Jan 2025 17:55:46 +0100 Subject: [PATCH 3/4] Set `max_term_width` to 100 in Clap --- src/cli.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.rs b/src/cli.rs index bcc6386f..1325040f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -99,6 +99,7 @@ impl std::fmt::Display for ColorWhen { version = env!("CARGO_PKG_VERSION"), about = "NixOS deployment tool", long_about = LONG_ABOUT, + max_term_width = 100, )] struct Opts { /// Path to a Hive expression, a flake.nix, or a Nix Flake URI From 9e5c0e95bbd6d5a0c23294c355b502a9594d3838 Mon Sep 17 00:00:00 2001 From: janhencic Date: Wed, 8 Jan 2025 18:35:28 +0100 Subject: [PATCH 4/4] Move `long_about` and `about` clap attributes to doc comments --- src/cli.rs | 44 ++++++++++++++++++++------------------ src/command/apply.rs | 3 ++- src/command/apply_local.rs | 6 ++---- src/command/eval.rs | 21 +++++++----------- src/command/exec.rs | 3 ++- 5 files changed, 37 insertions(+), 40 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1325040f..abb668a8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -91,13 +91,13 @@ impl std::fmt::Display for ColorWhen { } } +/// NixOS deployment tool #[derive(Parser)] #[command( name = "Colmena", bin_name = "colmena", author = "Zhaofeng Li ", version = env!("CARGO_PKG_VERSION"), - about = "NixOS deployment tool", long_about = LONG_ABOUT, max_term_width = 100, )] @@ -172,45 +172,47 @@ struct Opts { #[derive(Subcommand)] enum Command { Apply(command::apply::Opts), + #[cfg(target_os = "linux")] ApplyLocal(command::apply_local::Opts), - #[command( - about = "Build configurations but not push to remote machines", - long_about = r#"Build configurations but not push to remote machines -This subcommand behaves as if you invoked `apply` with the `build` goal."# - )] + /// Build configurations but not push to remote machines + /// + /// This subcommand behaves as if you invoked `apply` with the `build` goal. Build { #[command(flatten)] deploy: DeployOpts, }, + Eval(command::eval::Opts), - #[command( - about = "Upload keys to remote hosts", - long_about = r#"Upload keys to remote hosts -This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."# - )] + /// Upload keys to remote hosts + /// + /// This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal. UploadKeys { #[command(flatten)] deploy: DeployOpts, }, + Exec(command::exec::Opts), - #[command( - about = "Start an interactive REPL with the complete configuration", - long_about = r#"Start an interactive REPL with the complete configuration -In the REPL, you can inspect the configuration interactively with tab -completion. The node configurations are accessible under the `nodes` -attribute set."# - )] + /// Start an interactive REPL with the complete configuration + /// + /// In the REPL, you can inspect the configuration interactively with tab + /// completion. The node configurations are accessible under the `nodes` + /// attribute set. Repl, - #[command(about = "Show information about the current Nix installation")] + + /// Show information about the current Nix installation NixInfo, + + /// Run progress spinner tests #[cfg(debug_assertions)] - #[command(about = "Run progress spinner tests", hide = true)] + #[command(hide = true)] TestProgress, - #[command(about = "Generate shell auto-completion files (Internal)", hide = true)] + + /// Generate shell auto-completion files (Internal) + #[command(hide = true)] GenCompletions { shell: Shell, }, diff --git a/src/command/apply.rs b/src/command/apply.rs index 931f940b..721c8649 100644 --- a/src/command/apply.rs +++ b/src/command/apply.rs @@ -105,8 +105,9 @@ pub struct DeployOpts { node_filter: NodeFilterOpts, } +/// Apply configurations on remote machines #[derive(Debug, Args)] -#[command(name = "apply", about = "Apply configurations on remote machines")] +#[command(name = "apply")] pub struct Opts { /// Deployment goal /// diff --git a/src/command/apply_local.rs b/src/command/apply_local.rs index 3656393d..4f950070 100644 --- a/src/command/apply_local.rs +++ b/src/command/apply_local.rs @@ -11,11 +11,9 @@ use crate::nix::Hive; use crate::nix::{host::Local as LocalHost, NodeName}; use crate::progress::SimpleProgressOutput; +/// Apply configurations on the local machine #[derive(Debug, Args)] -#[command( - name = "apply-local", - about = "Apply configurations on the local machine" -)] +#[command(name = "apply-local")] pub struct Opts { /// Deployment goal /// diff --git a/src/command/eval.rs b/src/command/eval.rs index 54acb7f0..4cd9951c 100644 --- a/src/command/eval.rs +++ b/src/command/eval.rs @@ -5,20 +5,15 @@ use clap::Args; use crate::error::ColmenaError; use crate::nix::Hive; +/// Evaluate an expression using the complete configuration +/// +/// Your expression should take an attribute set with keys `pkgs`, `lib` and `nodes` (like a NixOS +/// module) and return a JSON-serializable value. For example, to retrieve the configuration of one +/// node, you may write something like: +/// +/// { nodes, ... }: nodes.node-a.config.networking.hostName #[derive(Debug, Args)] -#[command( - name = "eval", - alias = "introspect", - about = "Evaluate an expression using the complete configuration", - long_about = r#"Evaluate an expression using the complete configuration - -Your expression should take an attribute set with keys `pkgs`, `lib` and `nodes` (like a NixOS module) and return a JSON-serializable value. - -For example, to retrieve the configuration of one node, you may write something like: - - { nodes, ... }: nodes.node-a.config.networking.hostName -"# -)] +#[command(name = "eval", alias = "introspect")] pub struct Opts { /// The Nix expression #[arg(short = 'E', value_name = "EXPRESSION")] diff --git a/src/command/exec.rs b/src/command/exec.rs index 67a03fd5..59278ac5 100644 --- a/src/command/exec.rs +++ b/src/command/exec.rs @@ -13,8 +13,9 @@ use crate::nix::Hive; use crate::progress::SimpleProgressOutput; use crate::util; +/// Run a command on remote machines #[derive(Debug, Args)] -#[command(name = "exec", about = "Run a command on remote machines")] +#[command(name = "exec")] pub struct Opts { /// Deploy parallelism limit ///