Migrate to Clap 4.0

It was kind of a bumpy experience. Gradual migration to the Derive API
is coming soon.
This commit is contained in:
Zhaofeng Li 2022-10-09 15:26:37 -06:00
parent bf95e2dce8
commit 872949504b
16 changed files with 209 additions and 192 deletions

36
Cargo.lock generated
View file

@ -86,33 +86,31 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.22" version = "4.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" checksum = "4ed45cc2c62a3eff523e718d8576ba762c83a3146151093283ac62ae11933a73"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
"clap_lex", "clap_lex",
"indexmap",
"strsim", "strsim",
"termcolor", "termcolor",
"textwrap",
] ]
[[package]] [[package]]
name = "clap_complete" name = "clap_complete"
version = "3.2.5" version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8" checksum = "11cba7abac9b56dfe2f035098cdb3a43946f276e6db83b72c4e692343f9aab9a"
dependencies = [ dependencies = [
"clap", "clap",
] ]
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.2.4" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
@ -355,12 +353,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.0" version = "0.4.0"
@ -420,16 +412,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "indexmap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]] [[package]]
name = "indicatif" name = "indicatif"
version = "0.17.1" version = "0.17.1"
@ -849,12 +831,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "textwrap"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.37" version = "1.0.37"

View file

@ -10,8 +10,8 @@ edition = "2021"
async-stream = "0.3.2" async-stream = "0.3.2"
async-trait = "0.1.42" async-trait = "0.1.42"
atty = "0.2" atty = "0.2"
clap = "3.0.0" clap = "4.0.11"
clap_complete = "3.0.0" clap_complete = "4.0.2"
clicolors-control = "1" clicolors-control = "1"
console = "0.15.0" console = "0.15.0"
const_format = "0.2.22" const_format = "0.2.22"

View file

@ -2,7 +2,10 @@
use std::env; use std::env;
use clap::{value_parser, Arg, ArgMatches, ColorChoice, Command as ClapCommand}; use clap::{
builder::PossibleValue, value_parser, Arg, ArgMatches, ColorChoice, Command as ClapCommand,
ValueEnum,
};
use clap_complete::Shell; use clap_complete::Shell;
use const_format::concatcp; use const_format::concatcp;
use env_logger::fmt::WriteStyle; use env_logger::fmt::WriteStyle;
@ -93,7 +96,34 @@ macro_rules! handle_command {
}; };
} }
pub fn build_cli(include_internal: bool) -> ClapCommand<'static> { /// When to display color.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ColorWhen {
/// Detect automatically.
Auto,
/// Always display colors.
Always,
/// Never display colors.
Never,
}
impl ValueEnum for ColorWhen {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Auto, Self::Always, Self::Never]
}
fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
match self {
Self::Auto => Some(PossibleValue::new("auto")),
Self::Always => Some(PossibleValue::new("always")),
Self::Never => Some(PossibleValue::new("never")),
}
}
}
pub fn build_cli(include_internal: bool) -> ClapCommand {
let version = env!("CARGO_PKG_VERSION"); let version = env!("CARGO_PKG_VERSION");
let mut app = ClapCommand::new("Colmena") let mut app = ClapCommand::new("Colmena")
.bin_name("colmena") .bin_name("colmena")
@ -123,13 +153,13 @@ pub fn build_cli(include_internal: bool) -> ClapCommand<'static> {
.help("Show debug information for Nix commands") .help("Show debug information for Nix commands")
.long_help("Passes --show-trace to Nix commands") .long_help("Passes --show-trace to Nix commands")
.global(true) .global(true)
.takes_value(false)) .num_args(0))
.arg(Arg::new("impure") .arg(Arg::new("impure")
.long("impure") .long("impure")
.help("Allow impure expressions") .help("Allow impure expressions")
.long_help("Passes --impure to Nix commands") .long_help("Passes --impure to Nix commands")
.global(true) .global(true)
.takes_value(false)) .num_args(0))
.arg(Arg::new("color") .arg(Arg::new("color")
.long("color") .long("color")
.help("When to colorize the output") .help("When to colorize the output")
@ -139,7 +169,7 @@ It's also possible to specify the preference using environment variables. See <h
"#) "#)
.display_order(HELP_ORDER_LOW) .display_order(HELP_ORDER_LOW)
.value_name("WHEN") .value_name("WHEN")
.possible_values(&["auto", "always", "never"]) .value_parser(value_parser!(ColorWhen))
.default_value("auto") .default_value("auto")
.global(true)); .global(true));
@ -153,7 +183,7 @@ It's also possible to specify the preference using environment variables. See <h
.index(1) .index(1)
.value_parser(value_parser!(Shell)) .value_parser(value_parser!(Shell))
.required(true) .required(true)
.takes_value(true), .num_args(1),
), ),
); );
@ -187,7 +217,7 @@ pub async fn run() {
let mut app = build_cli(true); let mut app = build_cli(true);
let matches = app.clone().get_matches(); let matches = app.clone().get_matches();
set_color_pref(matches.value_of("color").unwrap()); set_color_pref(matches.get_one("color").unwrap());
init_logging(); init_logging();
handle_command!(apply, matches); handle_command!(apply, matches);
@ -221,9 +251,9 @@ fn gen_completions(args: &ArgMatches) {
clap_complete::generate(shell, &mut app, "colmena", &mut std::io::stdout()); clap_complete::generate(shell, &mut app, "colmena", &mut std::io::stdout());
} }
fn set_color_pref(cli: &str) { fn set_color_pref(when: &ColorWhen) {
if cli != "auto" { if when != &ColorWhen::Auto {
clicolors_control::set_colors_enabled(cli == "always"); clicolors_control::set_colors_enabled(when == &ColorWhen::Always);
} }
} }
@ -247,3 +277,13 @@ fn init_logging() {
.write_style(style) .write_style(style)
.init(); .init();
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cli_debug_assert() {
build_cli(true).debug_assert()
}
}

View file

@ -1,11 +1,14 @@
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use clap::{Arg, ArgMatches, Command as ClapCommand}; use clap::{
builder::{ArgPredicate, PossibleValuesParser, ValueParser},
value_parser, Arg, ArgMatches, Command as ClapCommand,
};
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::nix::deployment::{ use crate::nix::deployment::{
Deployment, EvaluationNodeLimit, Evaluator, Goal, Options, ParallelismLimit, Deployment, EvaluationNodeLimit, EvaluatorType, Goal, Options, ParallelismLimit,
}; };
use crate::nix::NodeFilter; use crate::nix::NodeFilter;
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
@ -24,17 +27,18 @@ The evaluation process is RAM-intensive. The default behavior is to limit the ma
Set to 0 to disable the limit. Set to 0 to disable the limit.
"#) "#)
.default_value("auto") .default_value("auto")
.takes_value(true) .num_args(1)
.validator(|s| { .value_parser(ValueParser::new(|s: &str| -> Result<EvaluationNodeLimit, String> {
if s == "auto" { if s == "auto" {
return Ok(()); return Ok(EvaluationNodeLimit::Heuristic);
} }
match s.parse::<usize>() { match s.parse::<usize>() {
Ok(_) => Ok(()), Ok(0) => Ok(EvaluationNodeLimit::None),
Ok(n) => Ok(EvaluationNodeLimit::Manual(n)),
Err(_) => Err(String::from("The value must be a valid number")), Err(_) => Err(String::from("The value must be a valid number")),
} }
})) })))
.arg(Arg::new("parallel") .arg(Arg::new("parallel")
.short('p') .short('p')
.long("parallel") .long("parallel")
@ -45,13 +49,8 @@ Set to 0 to disable the limit.
Set to 0 to disable parallemism limit. Set to 0 to disable parallemism limit.
"#) "#)
.default_value("10") .default_value("10")
.takes_value(true) .num_args(1)
.validator(|s| { .value_parser(value_parser!(usize)))
match s.parse::<usize>() {
Ok(_) => Ok(()),
Err(_) => Err(String::from("The value must be a valid number")),
}
}))
.arg(Arg::new("keep-result") .arg(Arg::new("keep-result")
.long("keep-result") .long("keep-result")
.help("Create GC roots for built profiles") .help("Create GC roots for built profiles")
@ -60,13 +59,13 @@ Set to 0 to disable parallemism limit.
The built system profiles will be added as GC roots so that they will not be removed by the garbage collector. 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. The links will be created under .gcroots in the directory the Hive configuration is located.
"#) "#)
.takes_value(false)) .num_args(0))
.arg(Arg::new("verbose") .arg(Arg::new("verbose")
.short('v') .short('v')
.long("verbose") .long("verbose")
.help("Be verbose") .help("Be verbose")
.long_help("Deactivates the progress spinner and prints every line of output.") .long_help("Deactivates the progress spinner and prints every line of output.")
.takes_value(false)) .num_args(0))
.arg(Arg::new("no-keys") .arg(Arg::new("no-keys")
.long("no-keys") .long("no-keys")
.help("Do not upload keys") .help("Do not upload keys")
@ -75,23 +74,23 @@ The links will be created under .gcroots in the directory the Hive configuration
By default, Colmena will upload keys set in `deployment.keys` before deploying the new profile on a node. 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`. To upload keys without building or deploying the rest of the configuration, use `colmena upload-keys`.
"#) "#)
.takes_value(false)) .num_args(0))
.arg(Arg::new("reboot") .arg(Arg::new("reboot")
.long("reboot") .long("reboot")
.help("Reboot nodes after activation") .help("Reboot nodes after activation")
.long_help("Reboots nodes after activation and waits for them to come back up.") .long_help("Reboots nodes after activation and waits for them to come back up.")
.takes_value(false)) .num_args(0))
.arg(Arg::new("no-substitute") .arg(Arg::new("no-substitute")
.long("no-substitute") .long("no-substitute")
.alias("no-substitutes") .alias("no-substitutes")
.help("Do not use substitutes") .help("Do not use substitutes")
.long_help("Disables the use of substituters when copying closures to the remote host.") .long_help("Disables the use of substituters when copying closures to the remote host.")
.takes_value(false)) .num_args(0))
.arg(Arg::new("no-gzip") .arg(Arg::new("no-gzip")
.long("no-gzip") .long("no-gzip")
.help("Do not use gzip") .help("Do not use gzip")
.long_help("Disables the use of gzip when copying closures to the remote host.") .long_help("Disables the use of gzip when copying closures to the remote host.")
.takes_value(false)) .num_args(0))
.arg(Arg::new("build-on-target") .arg(Arg::new("build-on-target")
.long("build-on-target") .long("build-on-target")
.help("Build the system profiles on the target nodes") .help("Build the system profiles on the target nodes")
@ -101,17 +100,17 @@ If enabled, the system profiles will be built on the target nodes themselves, no
This overrides per-node perferences set in `deployment.buildOnTarget`. This overrides per-node perferences set in `deployment.buildOnTarget`.
To temporarily disable remote build on all nodes, use `--no-build-on-target`. To temporarily disable remote build on all nodes, use `--no-build-on-target`.
"#) "#)
.takes_value(false)) .num_args(0))
.arg(Arg::new("no-build-on-target") .arg(Arg::new("no-build-on-target")
.long("no-build-on-target") .long("no-build-on-target")
.hide(true) .hide(true)
.takes_value(false)) .num_args(0))
.arg(Arg::new("force-replace-unknown-profiles") .arg(Arg::new("force-replace-unknown-profiles")
.long("force-replace-unknown-profiles") .long("force-replace-unknown-profiles")
.help("Ignore all targeted nodes deployment.replaceUnknownProfiles setting") .help("Ignore all targeted nodes deployment.replaceUnknownProfiles setting")
.long_help(r#"If `deployment.replaceUnknownProfiles` is set for a target, using this switch .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."#) will treat deployment.replaceUnknownProfiles as though it was set true and perform unknown profile replacement."#)
.takes_value(false)) .num_args(0))
.arg(Arg::new("evaluator") .arg(Arg::new("evaluator")
.long("evaluator") .long("evaluator")
.help("The evaluator to use (experimental)") .help("The evaluator to use (experimental)")
@ -119,10 +118,10 @@ will treat deployment.replaceUnknownProfiles as though it was set true and perfo
This is an experimental feature."#) This is an experimental feature."#)
.default_value("chunked") .default_value("chunked")
.possible_values(Evaluator::possible_values())) .value_parser(value_parser!(EvaluatorType)))
} }
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
let command = ClapCommand::new("apply") let command = ClapCommand::new("apply")
.about("Apply configurations on remote machines") .about("Apply configurations on remote machines")
.arg( .arg(
@ -141,10 +140,10 @@ Same as the targets for switch-to-configuration, with the following extra pseudo
"#, "#,
) )
.default_value("switch") .default_value("switch")
.default_value_if("reboot", None, Some("boot")) .default_value_if("reboot", ArgPredicate::IsPresent, Some("boot"))
.default_value("switch") .default_value("switch")
.index(1) .index(1)
.possible_values(&[ .value_parser(PossibleValuesParser::new([
"build", "build",
"push", "push",
"switch", "switch",
@ -152,7 +151,7 @@ Same as the targets for switch-to-configuration, with the following extra pseudo
"test", "test",
"dry-activate", "dry-activate",
"keys", "keys",
]), ])),
); );
let command = register_deploy_args(command); let command = register_deploy_args(command);
@ -164,10 +163,15 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from);
let goal_arg = local_args.value_of("goal").unwrap(); // FIXME: Just get_one::<Goal>
let goal_arg = local_args.get_one::<String>("goal").unwrap();
let goal = Goal::from_str(goal_arg).unwrap(); let goal = Goal::from_str(goal_arg).unwrap();
let filter = local_args.value_of("on").map(NodeFilter::new).transpose()?; // FIXME: Just get_one::<NodeFilter>
let filter = local_args
.get_one::<String>("on")
.map(NodeFilter::new)
.transpose()?;
if filter.is_none() && goal != Goal::Build { if filter.is_none() && goal != Goal::Build {
// User did not specify node, we should check meta and see rules // User did not specify node, we should check meta and see rules
@ -184,7 +188,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
.await?; .await?;
let n_targets = targets.len(); let n_targets = targets.len();
let verbose = local_args.is_present("verbose") || goal == Goal::DryActivate; let verbose = local_args.get_flag("verbose") || goal == Goal::DryActivate;
let mut output = SimpleProgressOutput::new(verbose); let mut output = SimpleProgressOutput::new(verbose);
let progress = output.get_sender(); let progress = output.get_sender();
@ -193,22 +197,27 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
// FIXME: Configure limits // FIXME: Configure limits
let options = { let options = {
let mut options = Options::default(); let mut options = Options::default();
options.set_substituters_push(!local_args.is_present("no-substitute")); options.set_substituters_push(!local_args.get_flag("no-substitute"));
options.set_gzip(!local_args.is_present("no-gzip")); options.set_gzip(!local_args.get_flag("no-gzip"));
options.set_upload_keys(!local_args.is_present("no-keys")); options.set_upload_keys(!local_args.get_flag("no-keys"));
options.set_reboot(local_args.is_present("reboot")); options.set_reboot(local_args.get_flag("reboot"));
options.set_force_replace_unknown_profiles( options.set_force_replace_unknown_profiles(
local_args.is_present("force-replace-unknown-profiles"), local_args.get_flag("force-replace-unknown-profiles"),
);
options.set_evaluator(
local_args
.get_one::<EvaluatorType>("evaluator")
.unwrap()
.to_owned(),
); );
options.set_evaluator(local_args.value_of_t("evaluator").unwrap());
if local_args.is_present("keep-result") { if local_args.get_flag("keep-result") {
options.set_create_gc_roots(true); options.set_create_gc_roots(true);
} }
if local_args.is_present("no-build-on-target") { if local_args.get_flag("no-build-on-target") {
options.set_force_build_on_target(false); options.set_force_build_on_target(false);
} else if local_args.is_present("build-on-target") { } else if local_args.get_flag("build-on-target") {
options.set_force_build_on_target(true); options.set_force_build_on_target(true);
} }
@ -217,7 +226,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
deployment.set_options(options); deployment.set_options(options);
if local_args.is_present("no-keys") && goal == Goal::UploadKeys { if local_args.get_flag("no-keys") && goal == Goal::UploadKeys {
log::error!("--no-keys cannot be used when the goal is to upload keys"); log::error!("--no-keys cannot be used when the goal is to upload keys");
quit::with_code(1); quit::with_code(1);
} }
@ -225,11 +234,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let parallelism_limit = { let parallelism_limit = {
let mut limit = ParallelismLimit::default(); let mut limit = ParallelismLimit::default();
limit.set_apply_limit({ limit.set_apply_limit({
let limit = local_args let limit = local_args.get_one::<usize>("parallel").unwrap().to_owned();
.value_of("parallel")
.unwrap()
.parse::<usize>()
.unwrap();
if limit == 0 { if limit == 0 {
n_targets n_targets
} else { } else {
@ -239,17 +244,10 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
limit limit
}; };
let evaluation_node_limit = match local_args.value_of("eval-node-limit").unwrap() { let evaluation_node_limit = local_args
"auto" => EvaluationNodeLimit::Heuristic, .get_one::<EvaluationNodeLimit>("eval-node-limit")
number => { .unwrap()
let number = number.parse::<usize>().unwrap(); .to_owned();
if number == 0 {
EvaluationNodeLimit::None
} else {
EvaluationNodeLimit::Manual(number)
}
}
};
deployment.set_parallelism_limit(parallelism_limit); deployment.set_parallelism_limit(parallelism_limit);
deployment.set_evaluation_node_limit(evaluation_node_limit); deployment.set_evaluation_node_limit(evaluation_node_limit);

View file

@ -1,7 +1,7 @@
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use clap::{Arg, ArgMatches, Command as ClapCommand}; use clap::{builder::PossibleValuesParser, Arg, ArgMatches, Command as ClapCommand};
use tokio::fs; use tokio::fs;
use crate::error::ColmenaError; use crate::error::ColmenaError;
@ -10,7 +10,7 @@ use crate::nix::{host::Local as LocalHost, NodeName};
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
use crate::util; use crate::util;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
ClapCommand::new("apply-local") ClapCommand::new("apply-local")
.about("Apply configurations on the local machine") .about("Apply configurations on the local machine")
.arg(Arg::new("goal") .arg(Arg::new("goal")
@ -18,16 +18,24 @@ pub fn subcommand() -> ClapCommand<'static> {
.long_help("Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local.") .long_help("Same as the targets for switch-to-configuration.\n\"push\" is noop in apply-local.")
.default_value("switch") .default_value("switch")
.index(1) .index(1)
.possible_values(&["push", "switch", "boot", "test", "dry-activate", "keys"])) .value_parser(PossibleValuesParser::new([
"push",
"switch",
"boot",
"test",
"dry-activate",
"keys",
])))
.arg(Arg::new("sudo") .arg(Arg::new("sudo")
.long("sudo") .long("sudo")
.help("Attempt to escalate privileges if not run as root")) .help("Attempt to escalate privileges if not run as root")
.num_args(0))
.arg(Arg::new("verbose") .arg(Arg::new("verbose")
.short('v') .short('v')
.long("verbose") .long("verbose")
.help("Be verbose") .help("Be verbose")
.long_help("Deactivates the progress spinner and prints every line of output.") .long_help("Deactivates the progress spinner and prints every line of output.")
.takes_value(false)) .num_args(0))
.arg(Arg::new("no-keys") .arg(Arg::new("no-keys")
.long("no-keys") .long("no-keys")
.help("Do not deploy keys") .help("Do not deploy keys")
@ -35,11 +43,11 @@ pub fn subcommand() -> ClapCommand<'static> {
By default, Colmena will deploy keys set in `deployment.keys` before activating the profile on this host. By default, Colmena will deploy keys set in `deployment.keys` before activating the profile on this host.
"#) "#)
.takes_value(false)) .num_args(0))
.arg(Arg::new("node") .arg(Arg::new("node")
.long("node") .long("node")
.help("Override the node name to use") .help("Override the node name to use")
.takes_value(true)) .num_args(1))
// Removed // Removed
.arg(Arg::new("sudo-command") .arg(Arg::new("sudo-command")
@ -47,11 +55,11 @@ By default, Colmena will deploy keys set in `deployment.keys` before activating
.value_name("COMMAND") .value_name("COMMAND")
.help("Removed: Configure deployment.privilegeEscalationCommand in node configuration") .help("Removed: Configure deployment.privilegeEscalationCommand in node configuration")
.hide(true) .hide(true)
.takes_value(true)) .num_args(1))
} }
pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> {
if local_args.occurrences_of("sudo-command") > 0 { if local_args.contains_id("sudo-command") {
log::error!("--sudo-command has been removed. Please configure it in deployment.privilegeEscalationCommand in the node configuration."); log::error!("--sudo-command has been removed. Please configure it in deployment.privilegeEscalationCommand in the node configuration.");
quit::with_code(1); quit::with_code(1);
} }
@ -68,8 +76,8 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
quit::with_code(5); quit::with_code(5);
} }
let escalate_privileges = local_args.is_present("sudo"); let escalate_privileges = local_args.get_flag("sudo");
let verbose = local_args.is_present("verbose") || escalate_privileges; // cannot use spinners with interactive sudo let verbose = local_args.get_flag("verbose") || escalate_privileges; // cannot use spinners with interactive sudo
{ {
let euid: u32 = unsafe { libc::geteuid() }; let euid: u32 = unsafe { libc::geteuid() };
@ -81,8 +89,8 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let hive = util::hive_from_args(local_args).await.unwrap(); let hive = util::hive_from_args(local_args).await.unwrap();
let hostname = { let hostname = {
let s = if local_args.is_present("node") { let s = if local_args.contains_id("node") {
local_args.value_of("node").unwrap().to_owned() local_args.get_one::<String>("node").unwrap().to_owned()
} else { } else {
hostname::get() hostname::get()
.expect("Could not get hostname") .expect("Could not get hostname")
@ -92,7 +100,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
NodeName::new(s)? NodeName::new(s)?
}; };
let goal = Goal::from_str(local_args.value_of("goal").unwrap()).unwrap(); let goal = Goal::from_str(local_args.get_one::<String>("goal").unwrap()).unwrap();
let target = { let target = {
if let Some(info) = hive.deployment_info_single(&hostname).await.unwrap() { if let Some(info) = hive.deployment_info_single(&hostname).await.unwrap() {
@ -131,7 +139,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let options = { let options = {
let mut options = Options::default(); let mut options = Options::default();
options.set_upload_keys(!local_args.is_present("no-keys")); options.set_upload_keys(!local_args.get_flag("no-keys"));
options options
}; };

View file

@ -1,11 +1,11 @@
use clap::{Arg, Command as ClapCommand}; use clap::{builder::PossibleValuesParser, Arg, Command as ClapCommand};
use crate::util; use crate::util;
use super::apply; use super::apply;
pub use super::apply::run; pub use super::apply::run;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
let command = ClapCommand::new("build") let command = ClapCommand::new("build")
.about("Build configurations but not push to remote machines") .about("Build configurations but not push to remote machines")
.long_about( .long_about(
@ -17,8 +17,8 @@ This subcommand behaves as if you invoked `apply` with the `build` goal."#,
Arg::new("goal") Arg::new("goal")
.hide(true) .hide(true)
.default_value("build") .default_value("build")
.possible_values(&["build"]) .value_parser(PossibleValuesParser::new(["build"]))
.takes_value(true), .num_args(1),
); );
let command = apply::register_deploy_args(command); let command = apply::register_deploy_args(command);

View file

@ -5,15 +5,15 @@ use clap::{Arg, ArgMatches, Command as ClapCommand};
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::util; use crate::util;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
subcommand_gen("eval") subcommand_gen("eval")
} }
pub fn deprecated_alias() -> ClapCommand<'static> { pub fn deprecated_alias() -> ClapCommand {
subcommand_gen("introspect").hide(true) subcommand_gen("introspect").hide(true)
} }
fn subcommand_gen(name: &str) -> ClapCommand<'static> { fn subcommand_gen(name: &'static str) -> ClapCommand {
ClapCommand::new(name) ClapCommand::new(name)
.about("Evaluate an expression using the complete configuration") .about("Evaluate an expression using the complete configuration")
.long_about(r#"Evaluate an expression using the complete configuration .long_about(r#"Evaluate an expression using the complete configuration
@ -28,16 +28,16 @@ For example, to retrieve the configuration of one node, you may write something
.index(1) .index(1)
.value_name("FILE") .value_name("FILE")
.help("The .nix file containing the expression") .help("The .nix file containing the expression")
.takes_value(true)) .num_args(1))
.arg(Arg::new("expression") .arg(Arg::new("expression")
.short('E') .short('E')
.value_name("EXPRESSION") .value_name("EXPRESSION")
.help("The Nix expression") .help("The Nix expression")
.takes_value(true)) .num_args(1))
.arg(Arg::new("instantiate") .arg(Arg::new("instantiate")
.long("instantiate") .long("instantiate")
.help("Actually instantiate the expression") .help("Actually instantiate the expression")
.takes_value(false)) .num_args(0))
} }
pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> {
@ -49,15 +49,21 @@ pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<()
let hive = util::hive_from_args(local_args).await?; let hive = util::hive_from_args(local_args).await?;
if !(local_args.is_present("expression") ^ local_args.is_present("expression_file")) { if !(local_args.contains_id("expression") ^ local_args.contains_id("expression_file")) {
log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both."); log::error!("Either an expression (-E) or a .nix file containing an expression should be specified, not both.");
quit::with_code(1); quit::with_code(1);
} }
let expression = if local_args.is_present("expression") { let expression = if local_args.contains_id("expression") {
local_args.value_of("expression").unwrap().to_string() local_args
.get_one::<String>("expression")
.unwrap()
.to_owned()
} else { } else {
let path: PathBuf = local_args.value_of("expression_file").unwrap().into(); let path = local_args
.get_one::<PathBuf>("expression_file")
.unwrap()
.to_owned();
format!( format!(
"import {}", "import {}",
path.canonicalize() path.canonicalize()
@ -67,7 +73,7 @@ pub async fn run(global_args: &ArgMatches, local_args: &ArgMatches) -> Result<()
) )
}; };
let instantiate = local_args.is_present("instantiate"); let instantiate = local_args.get_flag("instantiate");
let result = hive.introspect(expression, instantiate).await?; let result = hive.introspect(expression, instantiate).await?;
if instantiate { if instantiate {

View file

@ -2,7 +2,7 @@ use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use clap::{Arg, ArgMatches, Command as ClapCommand}; use clap::{value_parser, Arg, ArgMatches, Command as ClapCommand};
use futures::future::join_all; use futures::future::join_all;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
@ -12,10 +12,9 @@ use crate::nix::NodeFilter;
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
use crate::util; use crate::util;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
let command = ClapCommand::new("exec") let command = ClapCommand::new("exec")
.about("Run a command on remote machines") .about("Run a command on remote machines")
.trailing_var_arg(true)
.arg( .arg(
Arg::new("parallel") Arg::new("parallel")
.short('p') .short('p')
@ -29,11 +28,8 @@ In `colmena exec`, the parallelism limit is disabled (0) by default.
"#, "#,
) )
.default_value("0") .default_value("0")
.takes_value(true) .num_args(1)
.validator(|s| match s.parse::<usize>() { .value_parser(value_parser!(usize)),
Ok(_) => Ok(()),
Err(_) => Err(String::from("The value must be a valid number")),
}),
) )
.arg( .arg(
Arg::new("verbose") Arg::new("verbose")
@ -41,15 +37,15 @@ In `colmena exec`, the parallelism limit is disabled (0) by default.
.long("verbose") .long("verbose")
.help("Be verbose") .help("Be verbose")
.long_help("Deactivates the progress spinner and prints every line of output.") .long_help("Deactivates the progress spinner and prints every line of output.")
.takes_value(false), .num_args(0),
) )
.arg( .arg(
Arg::new("command") Arg::new("command")
.value_name("COMMAND") .value_name("COMMAND")
.last(true) .trailing_var_arg(true)
.help("Command") .help("Command")
.required(true) .required(true)
.multiple_occurrences(true) .num_args(1..)
.long_help( .long_help(
r#"Command to run r#"Command to run
@ -67,16 +63,16 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let hive = util::hive_from_args(local_args).await?; let hive = util::hive_from_args(local_args).await?;
let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from); let ssh_config = env::var("SSH_CONFIG_FILE").ok().map(PathBuf::from);
let filter = local_args.value_of("on").map(NodeFilter::new).transpose()?; // FIXME: Just get_one::<NodeFilter>
let filter = local_args
.get_one::<String>("on")
.map(NodeFilter::new)
.transpose()?;
let mut targets = hive.select_nodes(filter, ssh_config, true).await?; let mut targets = hive.select_nodes(filter, ssh_config, true).await?;
let parallel_sp = Arc::new({ let parallel_sp = Arc::new({
let limit = local_args let limit = local_args.get_one::<usize>("parallel").unwrap().to_owned();
.value_of("parallel")
.unwrap()
.parse::<usize>()
.unwrap();
if limit > 0 { if limit > 0 {
Some(Semaphore::new(limit)) Some(Semaphore::new(limit))
@ -87,13 +83,13 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let command: Arc<Vec<String>> = Arc::new( let command: Arc<Vec<String>> = Arc::new(
local_args local_args
.values_of("command") .get_many::<String>("command")
.unwrap() .unwrap()
.map(|s| s.to_string()) .cloned()
.collect(), .collect(),
); );
let mut output = SimpleProgressOutput::new(local_args.is_present("verbose")); let mut output = SimpleProgressOutput::new(local_args.get_flag("verbose"));
let (mut monitor, meta) = JobMonitor::new(output.get_sender()); let (mut monitor, meta) = JobMonitor::new(output.get_sender());

View file

@ -4,7 +4,7 @@ use crate::error::ColmenaError;
use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs; use crate::nix::evaluator::nix_eval_jobs::get_pinned_nix_eval_jobs;
use crate::nix::NixCheck; use crate::nix::NixCheck;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
ClapCommand::new("nix-info").about("Show information about the current Nix installation") ClapCommand::new("nix-info").about("Show information about the current Nix installation")
} }

View file

@ -8,7 +8,7 @@ use crate::error::ColmenaError;
use crate::nix::info::NixCheck; use crate::nix::info::NixCheck;
use crate::util; use crate::util;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
ClapCommand::new("repl") ClapCommand::new("repl")
.about("Start an interactive REPL with the complete configuration") .about("Start an interactive REPL with the complete configuration")
.long_about( .long_about(

View file

@ -14,7 +14,7 @@ macro_rules! node {
}; };
} }
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
ClapCommand::new("test-progress") ClapCommand::new("test-progress")
.about("Run progress spinner tests") .about("Run progress spinner tests")
.hide(true) .hide(true)

View file

@ -1,11 +1,11 @@
use clap::{Arg, Command as ClapCommand}; use clap::{builder::PossibleValuesParser, Arg, Command as ClapCommand};
use crate::util; use crate::util;
use super::apply; use super::apply;
pub use super::apply::run; pub use super::apply::run;
pub fn subcommand() -> ClapCommand<'static> { pub fn subcommand() -> ClapCommand {
let command = ClapCommand::new("upload-keys") let command = ClapCommand::new("upload-keys")
.about("Upload keys to remote hosts") .about("Upload keys to remote hosts")
.long_about( .long_about(
@ -17,8 +17,8 @@ This subcommand behaves as if you invoked `apply` with the pseudo `keys` goal."#
Arg::new("goal") Arg::new("goal")
.hide(true) .hide(true)
.default_value("keys") .default_value("keys")
.possible_values(&["keys"]) .value_parser(PossibleValuesParser::new(["keys"]))
.takes_value(true), .num_args(1),
); );
let command = apply::register_deploy_args(command); let command = apply::register_deploy_args(command);

View file

@ -8,7 +8,7 @@ pub mod limits;
pub use limits::{EvaluationNodeLimit, ParallelismLimit}; pub use limits::{EvaluationNodeLimit, ParallelismLimit};
pub mod options; pub mod options;
pub use options::{Evaluator, Options}; pub use options::{EvaluatorType, Options};
use std::collections::HashMap; use std::collections::HashMap;
use std::mem; use std::mem;
@ -162,10 +162,10 @@ impl Deployment {
let deployment = DeploymentHandle::new(self); let deployment = DeploymentHandle::new(self);
let meta_future = meta.run(|meta| async move { let meta_future = meta.run(|meta| async move {
match deployment.options.evaluator { match deployment.options.evaluator {
Evaluator::Chunked => { EvaluatorType::Chunked => {
deployment.execute_chunked(meta.clone(), targets).await?; deployment.execute_chunked(meta.clone(), targets).await?;
} }
Evaluator::Streaming => { EvaluatorType::Streaming => {
log::warn!("Streaming evaluation is an experimental feature"); log::warn!("Streaming evaluation is an experimental feature");
deployment.execute_streaming(meta.clone(), targets).await?; deployment.execute_streaming(meta.clone(), targets).await?;
} }

View file

@ -1,8 +1,7 @@
//! Deployment options. //! Deployment options.
use std::str::FromStr; use clap::{builder::PossibleValue, ValueEnum};
use crate::error::{ColmenaError, ColmenaResult};
use crate::nix::CopyOptions; use crate::nix::CopyOptions;
/// Options for a deployment. /// Options for a deployment.
@ -33,12 +32,12 @@ pub struct Options {
pub(super) force_replace_unknown_profiles: bool, pub(super) force_replace_unknown_profiles: bool,
/// Which evaluator to use (experimental). /// Which evaluator to use (experimental).
pub(super) evaluator: Evaluator, pub(super) evaluator: EvaluatorType,
} }
/// Which evaluator to use. /// Which evaluator to use.
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum Evaluator { pub enum EvaluatorType {
Chunked, Chunked,
Streaming, Streaming,
} }
@ -72,7 +71,7 @@ impl Options {
self.force_replace_unknown_profiles = enable; self.force_replace_unknown_profiles = enable;
} }
pub fn set_evaluator(&mut self, evaluator: Evaluator) { pub fn set_evaluator(&mut self, evaluator: EvaluatorType) {
self.evaluator = evaluator; self.evaluator = evaluator;
} }
@ -95,27 +94,20 @@ impl Default for Options {
create_gc_roots: false, create_gc_roots: false,
force_build_on_target: None, force_build_on_target: None,
force_replace_unknown_profiles: false, force_replace_unknown_profiles: false,
evaluator: Evaluator::Chunked, evaluator: EvaluatorType::Chunked,
} }
} }
} }
impl FromStr for Evaluator { impl ValueEnum for EvaluatorType {
type Err = ColmenaError; fn value_variants<'a>() -> &'a [Self] {
&[Self::Chunked, Self::Streaming]
}
fn from_str(s: &str) -> ColmenaResult<Self> { fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
match s { match self {
"chunked" => Ok(Self::Chunked), Self::Chunked => Some(PossibleValue::new("chunked")),
"streaming" => Ok(Self::Streaming), Self::Streaming => Some(PossibleValue::new("streaming")),
_ => Err(ColmenaError::Unknown {
message: "Valid values are 'chunked' and 'streaming'".to_string(),
}),
} }
} }
} }
impl Evaluator {
pub fn possible_values() -> &'static [&'static str] {
&["chunked", "streaming"]
}
}

View file

@ -5,7 +5,7 @@
use std::env; use std::env;
use std::future::Future; use std::future::Future;
use clap::ArgMatches; use clap::{parser::ValueSource as ClapValueSource, ArgMatches};
use crate::error::ColmenaError; use crate::error::ColmenaError;
@ -48,7 +48,7 @@ fn troubleshoot(
// in their Colmena checkout, and encounter NoFlakesSupport // in their Colmena checkout, and encounter NoFlakesSupport
// because Colmena always prefers flake.nix when it exists. // because Colmena always prefers flake.nix when it exists.
if global_args.occurrences_of("config") == 0 { if let Some(ClapValueSource::DefaultValue) = global_args.value_source("config") {
let cwd = env::current_dir()?; let cwd = env::current_dir()?;
if cwd.join("flake.nix").is_file() && cwd.join("hive.nix").is_file() { if cwd.join("flake.nix").is_file() && cwd.join("hive.nix").is_file() {
eprintln!( eprintln!(

View file

@ -3,7 +3,7 @@ use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use async_trait::async_trait; use async_trait::async_trait;
use clap::{Arg, ArgMatches, Command as ClapCommand}; use clap::{parser::ValueSource as ClapValueSource, Arg, ArgMatches, Command as ClapCommand};
use futures::future::join3; use futures::future::join3;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader}; use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
@ -193,8 +193,8 @@ impl CommandExt for CommandExecution {
} }
pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> { pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> {
let path = match args.occurrences_of("config") { let path = match args.value_source("config").unwrap() {
0 => { ClapValueSource::DefaultValue => {
// traverse upwards until we find hive.nix // traverse upwards until we find hive.nix
let mut cur = std::env::current_dir()?; let mut cur = std::env::current_dir()?;
let mut file_path = None; let mut file_path = None;
@ -231,9 +231,9 @@ pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> {
file_path.unwrap() file_path.unwrap()
} }
_ => { ClapValueSource::CommandLine => {
let path = args let path = args
.value_of("config") .get_one::<String>("config")
.expect("The config arg should exist") .expect("The config arg should exist")
.to_owned(); .to_owned();
let fpath = PathBuf::from(&path); let fpath = PathBuf::from(&path);
@ -246,11 +246,11 @@ pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> {
let hive_path = HivePath::Flake(flake); let hive_path = HivePath::Flake(flake);
let mut hive = Hive::new(hive_path).await?; let mut hive = Hive::new(hive_path).await?;
if args.is_present("show-trace") { if args.get_flag("show-trace") {
hive.set_show_trace(true); hive.set_show_trace(true);
} }
if args.is_present("impure") { if args.get_flag("impure") {
hive.set_impure(true); hive.set_impure(true);
} }
@ -259,6 +259,7 @@ pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> {
fpath fpath
} }
x => panic!("Unexpected value source for config: {:?}", x),
}; };
let hive_path = HivePath::from_path(path).await?; let hive_path = HivePath::from_path(path).await?;
@ -273,11 +274,11 @@ pub async fn hive_from_args(args: &ArgMatches) -> ColmenaResult<Hive> {
let mut hive = Hive::new(hive_path).await?; let mut hive = Hive::new(hive_path).await?;
if args.is_present("show-trace") { if args.get_flag("show-trace") {
hive.set_show_trace(true); hive.set_show_trace(true);
} }
if args.is_present("impure") { if args.get_flag("impure") {
hive.set_impure(true); hive.set_impure(true);
} }
@ -298,7 +299,7 @@ The list is comma-separated and globs are supported. To match tags, prepend the
- edge-* - edge-*
- edge-*,core-* - edge-*,core-*
- @a-tag,@tags-can-have-*"#) - @a-tag,@tags-can-have-*"#)
.takes_value(true)) .num_args(1))
} }
pub async fn capture_stream<R>( pub async fn capture_stream<R>(