Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The last associated nix log is now shown next to the node name, including a
possible build name when `--print-build-logs` is passed.
- Fix outputting coloured text, disrespecting NO_COLOR, in some case.
- `--option` argument for `wire apply` and `wire build`.

### Changed

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ signal-hook = "0.4.0"
clap-markdown = "0.1.5"
clap_mangen = "0.3.0"
konst = { workspace = true }
regex = "1.11"
31 changes: 19 additions & 12 deletions crates/cli/src/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,10 @@ where

fn select_names(
args: &CommonVerbArgs,
mut modifiers: SubCommandModifiers,
modifiers: &mut SubCommandModifiers,
hive: &Hive,
) -> Result<Vec<Name>> {
let (tags, names) = resolve_targets(&args.on, &mut modifiers)?;
let (tags, names) = resolve_targets(&args.on, modifiers)?;

let selected_names: Vec<_> = hive
.nodes
Expand Down Expand Up @@ -145,6 +145,18 @@ fn statusbar_clear_names() {
}
}

/// parition a vector of names and results into a list of successful names and errors
fn partition_results(
result: Vec<(Name, Result<(), HiveLibError>)>,
) -> (Vec<Name>, Vec<(Name, HiveLibError)>) {
result
.into_iter()
.partition_map(|(name, result)| match result {
Ok(..) => Either::Left(name),
Err(err) => Either::Right((name, err)),
})
}

#[allow(clippy::missing_errors_doc)]
pub async fn run_goal<F>(hive: &mut Hive, make_goal: F, arguments: RunGoalArguments) -> Result<()>
where
Expand All @@ -155,12 +167,13 @@ where
location,
args,
partition,
modifiers,
mut modifiers,
cache,
} = arguments;

let location = Arc::new(location);
let selected_names = select_names(&args, modifiers, hive)?;
let selected_names = select_names(&args, &mut modifiers, hive)?;
let modifiers = Arc::new(modifiers);
let num_selected = selected_names.len();
let partitioned_names = partition_slice(&selected_names, &partition).to_vec();

Expand Down Expand Up @@ -232,19 +245,13 @@ where
}

let futures = futures::stream::iter(set).buffer_unordered(args.parallel);
let result = futures.collect::<Vec<_>>().await;
let results = futures.collect::<Vec<_>>().await;

for task in evaluation_cache_tasks {
let _ = task.await;
}

let (successful, errors): (Vec<_>, Vec<_>) =
result
.into_iter()
.partition_map(|(name, result)| match result {
Ok(..) => Either::Left(name),
Err(err) => Either::Right((name, err)),
});
let (successful, errors) = partition_results(results);

if !successful.is_empty() {
info!(
Expand Down
97 changes: 94 additions & 3 deletions crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// Copyright 2024-2025 wire Contributors

use clap::builder::PossibleValue;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
use clap::error::ErrorKind;
use clap::{ArgAction, Args, FromArgMatches, Parser, Subcommand, ValueEnum};
use clap::{ValueHint, crate_version};
use clap_complete::CompletionCandidate;
use clap_complete::engine::ArgValueCompleter;
Expand All @@ -11,6 +12,7 @@ use clap_verbosity_flag::InfoLevel;
use konst::result::unwrap;
use konst::string::split_once;
use konst::{option, result, try_};
use regex::Regex;
use tokio::runtime::Handle;
use wire_core::SubCommandModifiers;
use wire_core::commands::common::get_hive_node_names;
Expand All @@ -22,13 +24,17 @@ use wire_core::hive::{SCHEMA_VERSION_SEMVER, get_hive_location};
use std::io::IsTerminal;
#[cfg(debug_assertions)]
use std::path::PathBuf;
use std::sync::LazyLock;
use std::{
fmt::{self, Display, Formatter},
sync::Arc,
};

const NIX_OPTIONS_HELP_HEADING: &'static str = "Nix Options";

const NIX_OPTION_NAME_REGEX: &str = r"^[a-zA-Z][a-zA-Z0-9_-]*$";
const NIX_OPTION_VALUE_REGEX: &str = r"^[a-zA-Z0-9_+ ./=-]*$";

#[allow(clippy::struct_excessive_bools)]
#[derive(Parser)]
#[command(
Expand Down Expand Up @@ -182,6 +188,79 @@ impl From<HandleUnreachableArg> for HandleUnreachable {
}
}

fn validate_nix_option_pair(name: &str, value: &str) -> Result<(String, String), String> {
static NAME_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(NIX_OPTION_NAME_REGEX).unwrap());
static VALUE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(NIX_OPTION_VALUE_REGEX).unwrap());

if !NAME_REGEX.is_match(name) {
return Err(format!(
"invalid nix option name '{name}': must match {NIX_OPTION_NAME_REGEX}"
));
}

if !VALUE_REGEX.is_match(value) {
return Err(format!(
"invalid nix option value '{value}': must match {NIX_OPTION_VALUE_REGEX}"
));
}

Ok((name.to_string(), value.to_string()))
}

#[derive(Clone, Default)]
pub struct NixOptions(pub Vec<(String, String)>);

impl FromArgMatches for NixOptions {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
let mut value = Self::default();
value.update_from_arg_matches(matches)?;

Ok(value)
}

fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
let raw: Vec<String> = matches
.get_many::<String>("option")
.unwrap_or_default()
.cloned()
.collect();

for chunk in raw.as_chunks::<2>().0 {
let validated = validate_nix_option_pair(&chunk[0], &chunk[1])
.map_err(|e| clap::Error::raw(ErrorKind::ValueValidation, e))?;

self.0.push(validated);
}

Ok(())
}
}

impl Args for NixOptions {
fn augment_args(cmd: clap::Command) -> clap::Command {
cmd.arg(
clap::Arg::new("option")
.long("option")
.value_names(["NAME", "VALUE"])
.num_args(2)
.action(ArgAction::Append)
.help("Set nix option name to value, overriding nix.conf.")
.help_heading(NIX_OPTIONS_HELP_HEADING)
.long_help(
"Set nix option name to value, overriding nix.conf.\n\n\
This option does not apply to the experimental nix client.\n\
Passing nix options disables wire's evaluation cache.",
),
)
}

fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
Self::augment_args(cmd)
}
}

#[derive(Args)]
pub struct CommonVerbArgs {
/// List of literal node names, a literal `-`, or `@` prefixed tags.
Expand All @@ -202,6 +281,13 @@ pub struct CommonVerbArgs {
help_heading = NIX_OPTIONS_HELP_HEADING
)]
pub print_build_logs: bool,

/// Set nix option name to value, overriding nix.conf.
///
/// This option does not apply to the experimental nix client.
/// Passing nix options disables wire's evaluation cache.
#[command(flatten)]
pub option: NixOptions,
}

#[allow(clippy::struct_excessive_bools)]
Expand Down Expand Up @@ -368,14 +454,19 @@ impl ToSubCommandModifiers for Cli {
Commands::Apply(args) => args.ssh_verbose.into(),
_ => 0,
},
options: match &self.command {
Commands::Apply(args) => Arc::new(args.common.option.0.clone()),
Commands::Build(args) => Arc::new(args.common.option.0.clone()),
Commands::Inspect { .. } => Arc::new(Vec::new()),
},
}
}
}

fn node_names_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
tokio::task::block_in_place(|| {
let handle = Handle::current();
let modifiers = SubCommandModifiers::default();
let modifiers = Arc::new(SubCommandModifiers::default());
let mut completions = vec![];

if current.is_empty() || current == "-" {
Expand All @@ -390,7 +481,7 @@ fn node_names_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {

let Ok(hive_location) = handle.block_on(get_hive_location(
current_dir.display().to_string(),
modifiers,
modifiers.clone(),
)) else {
return completions;
};
Expand Down
15 changes: 9 additions & 6 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async fn main() -> Result<()> {
let should_shutdown = Arc::new(AtomicBool::new(false));
let signals_task = tokio::spawn(handle_signals(signals, should_shutdown.clone()));

let location = get_hive_location(args.path, modifiers).await?;
let location = get_hive_location(args.path, modifiers.clone().into()).await?;
let cache = Arc::new(InspectionCache::new().await);

dispatch_command(
Expand Down Expand Up @@ -135,7 +135,8 @@ async fn dispatch_command(
) -> Result<()> {
match args {
cli::Commands::Apply(apply_args) => {
let mut hive = Hive::new_from_path(&location, cache.clone(), modifiers).await?;
let mut hive =
Hive::new_from_path(&location, cache.clone(), modifiers.clone().into()).await?;
let goal = apply_args.goal.clone().try_into().unwrap();

// Respect user's --always-build-local arg
Expand All @@ -162,14 +163,15 @@ async fn dispatch_command(
location,
args: apply_args.common,
partition: Partitions::default(),
modifiers,
modifiers: modifiers.clone(),
cache: cache.clone(),
},
)
.await?;
}
cli::Commands::Build(build_args) => {
let mut hive = Hive::new_from_path(&location, cache.clone(), modifiers).await?;
let mut hive =
Hive::new_from_path(&location, cache.clone(), modifiers.clone().into()).await?;

apply::run_goal(
&mut hive,
Expand All @@ -188,7 +190,8 @@ async fn dispatch_command(
cli::Commands::Inspect { json, selection } => println!("{}", {
match selection {
cli::Inspection::Full => {
let hive = Hive::new_from_path(&location, cache.clone(), modifiers).await?;
let hive =
Hive::new_from_path(&location, cache.clone(), modifiers.into()).await?;
if json {
serde_json::to_string(&hive).into_diagnostic()?
} else {
Expand All @@ -197,7 +200,7 @@ async fn dispatch_command(
}
}
cli::Inspection::Names => {
let names = get_hive_node_names(&location, modifiers).await?;
let names = get_hive_node_names(&location, modifiers.into()).await?;

if json {
serde_json::to_string(&names).into_diagnostic()?
Expand Down
42 changes: 39 additions & 3 deletions crates/core/src/commands/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@

use std::fmt;

use itertools::Itertools;

pub struct CommandStringBuilder {
command: String,
}

impl CommandStringBuilder {
pub(crate) fn nix() -> Self {
Self {
pub(crate) fn nix(options: &[(String, String)]) -> Self {
let mut value = Self {
command: "nix".to_string(),
}
};

value.args(
&options
.iter()
.map(|(name, value)| {
let value = value.replace('\\', "\\\\").replace('"', "\\\"");
format!("--option \"{name}\" \"{value}\"")
})
.collect_vec(),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

value
}

pub(crate) fn new<S: AsRef<str>>(s: S) -> Self {
Expand Down Expand Up @@ -71,4 +85,26 @@ mod tests {
);
assert_eq!(builder.to_string(), "a b c d e g");
}

#[test]
fn nix_options_with_spaces() {
let options = vec![
("trusted-public-keys".to_string(), "key1 key2".to_string()),
(
"experimental-features".to_string(),
"nix-command flakes".to_string(),
),
];
let builder = CommandStringBuilder::nix(&options);
let output = builder.to_string();

assert!(
output.contains("\"trusted-public-keys\" \"key1 key2\""),
"expected quotes around value with spaces, got: {output}"
);
assert!(
output.contains("\"experimental-features\" \"nix-command flakes\""),
"expected quotes around value with spaces, got: {output}"
);
}
}
Loading
Loading