diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f3e055..8689e00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `--print-build-logs` / `-L` argument. - `nix copy` & `nix build` operations are now manually implemented through a native - rust nix daemon client. + rust nix daemon client if `--experimental-nix-client` is passed. - Node `target` liveliness is now determined directly by initiating a nix daemon - handshake. + handshake if `--experimental-nix-client` is passed. ### Changed diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 73e6a9d..223c47e 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -59,6 +59,13 @@ pub struct Cli { #[arg(long, global = true, default_value_t = false)] pub show_trace: bool, + /// Use the experimental native Nix Daemon Client instead of the nix CLI commands. + /// + /// This enables native protocol-level optimisations but may not be compatible with all Nix versions + /// and is still a work in progress cause issues. + #[arg(long, global = true, default_value_t = false)] + pub experimental_nix_client: bool, + #[cfg(debug_assertions)] #[arg(long, hide = true, global = true)] pub markdown_help: bool, @@ -334,6 +341,7 @@ impl ToSubCommandModifiers for Cli { SubCommandModifiers { show_trace: self.show_trace, non_interactive: self.non_interactive, + experimental_nix_client: self.experimental_nix_client, ssh_accept_host: match &self.command { Commands::Apply(args) if args.ssh_accept_host => { wire_core::StrictHostKeyChecking::No diff --git a/crates/core/src/commands/common.rs b/crates/core/src/commands/common.rs index f4f380e..87e2aba 100644 --- a/crates/core/src/commands/common.rs +++ b/crates/core/src/commands/common.rs @@ -1,17 +1,89 @@ // SPDX-License-Identifier: AGPL-3.0-or-later // Copyright 2024-2025 wire Contributors +use std::collections::HashMap; + use tracing::instrument; use crate::{ EvalGoal, SubCommandModifiers, commands::{ CommandArguments, Either, WireCommandChip, builder::CommandStringBuilder, run_command, + run_command_with_env, }, errors::{CommandError, HiveInitialisationError, HiveLibError}, - hive::HiveLocation, + hive::{ + HiveLocation, + node::{Context, Push, SharedTarget}, + }, }; +fn get_common_copy_path_help(error: &CommandError) -> Option { + if let CommandError::CommandFailed { logs, .. } = error + && (logs.contains("error: unexpected end-of-file")) + { + Some("wire requires the deploying user or wire binary cache is trusted on the remote server. if you're attempting to make that change, skip keys with --no-keys. please read https://wire.forall.systems/guides/keys for more information".to_string()) + } else { + None + } +} + +/// Pushes the path with the regular nix commands. +pub async fn push( + context: &Context, + target: &SharedTarget, + push: Push<'_>, + substitute_on_destination: bool, +) -> Result<(), HiveLibError> { + let target = target.0.read().await; + + let mut command_string = CommandStringBuilder::nix(); + + command_string.args(&["--extra-experimental-features", "nix-command", "copy"]); + command_string.opt_arg(substitute_on_destination, "--substitute-on-destination"); + command_string.arg("--to"); + command_string.args(&[ + format!( + "ssh://{user}@{host}", + user = target.user, + host = target.get_preferred_host()?, + ), + match push { + Push::Derivation(drv) => format!("{}^* --derivation", drv.to_absolute_path()), + Push::Path(path) => path.to_absolute_path(), + }, + ]); + + let child = run_command_with_env( + &CommandArguments::new(command_string, context.modifiers) + .mode(crate::commands::ChildOutputMode::Nix), + HashMap::from([( + "NIX_SSHOPTS".into(), + target.create_ssh_opts(context.modifiers)?, + )]), + ) + .await?; + + let status = child.wait_till_success().await; + + let help = if let Err(ref error) = status { + get_common_copy_path_help(error).map(Box::new) + } else { + None + }; + + status.map_err(|error| HiveLibError::NixCopyCliError { + name: context.name.clone(), + path: match push { + Push::Derivation(path) | Push::Path(path) => path.clone(), + }, + error: Box::new(error), + help, + })?; + + Ok(()) +} + fn get_common_command_help(error: &CommandError) -> Option { if let CommandError::CommandFailed { logs, .. } = error // marshmallow: your using this repo as a hive you idiot diff --git a/crates/core/src/errors.rs b/crates/core/src/errors.rs index 3d52432..c8758b1 100644 --- a/crates/core/src/errors.rs +++ b/crates/core/src/errors.rs @@ -242,17 +242,6 @@ pub enum HiveLibError { #[diagnostic(transparent)] StorePath(StorePathError), - #[diagnostic(code(wire::CopyPath))] - #[error("failed to copy path {} to node {name}", path.to_absolute_path())] - NixCopyError { - name: Name, - path: SafeStorePath, - #[source] - error: Box, - #[help] - help: Option, - }, - #[error("Failed to apply key {}", .0)] KeyError( String, @@ -261,7 +250,8 @@ pub enum HiveLibError { KeyError, ), - #[diagnostic(code(wire::BuildNode))] + /// Regular nix build error + #[diagnostic(code(wire::BuildNodeDaemon))] #[error("failed to build node {name}")] NixBuildError { name: Name, @@ -269,6 +259,39 @@ pub enum HiveLibError { source: NixDaemonClientError, }, + /// Nix daemon nix build error + #[diagnostic(code(wire::BuildNodeCli))] + #[error("failed to build node {name}")] + NixBuildCliError { + name: Name, + #[source] + source: CommandError, + }, + + /// Regular nix copy error + #[diagnostic(code(wire::CopyPathDaemon))] + #[error("failed to copy path {} to node {name}", path.to_absolute_path())] + NixCopyError { + name: Name, + path: SafeStorePath, + #[source] + error: Box, + #[help] + help: Option, + }, + + /// Experimental nix daemon client copy error + #[diagnostic(code(wire::CopyPathCli))] + #[error("failed to copy path {} to node {name}", path.to_absolute_path())] + NixCopyCliError { + name: Name, + path: SafeStorePath, + #[source] + error: Box, + #[help] + help: Option>, + }, + #[diagnostic(code(wire::Evaluate))] #[error("failed to evaluate `{attribute}` from the context of a hive.")] NixEvalError { @@ -289,3 +312,9 @@ pub enum HiveLibError { #[error("SIGINT received, shut down")] Sigint, } + +impl From for HiveLibError { + fn from(e: StorePathError) -> Self { + HiveLibError::StorePath(e) + } +} diff --git a/crates/core/src/hive/steps/build.rs b/crates/core/src/hive/steps/build.rs index 5e4dee5..91b7294 100644 --- a/crates/core/src/hive/steps/build.rs +++ b/crates/core/src/hive/steps/build.rs @@ -7,8 +7,11 @@ use tracing::{debug, info, instrument}; use wire_nix_client::{DerivedPath, DerivedPathOutput, NixClient, NixDaemonClientError}; use crate::{ - HiveLibError, - commands::{Either, trace_nix_log_message}, + HiveLibError, SafeStorePath, + commands::{ + CommandArguments, Either, WireCommandChip, builder::CommandStringBuilder, + run_command_with_env, trace_nix_log_message, + }, hive::node::{Context, ExecuteStep, SharedTarget}, open_remote_client, }; @@ -32,73 +35,117 @@ impl ExecuteStep for Build { async fn execute(&self, ctx: &mut Context) -> Result<(), HiveLibError> { let top_level = ctx.state.evaluation.as_ref().unwrap(); - let mut connection = if let Some(ref target) = self.target { - let target = target.0.read().await; - - Either::Left( - open_remote_client( - &target, - ctx.modifiers, - trace_nix_log_message, - ctx.should_quit.clone(), + if ctx.modifiers.experimental_nix_client { + // use experimental nix daemon client + let mut connection = if let Some(ref target) = self.target { + let target = target.0.read().await; + + Either::Left( + open_remote_client( + &target, + ctx.modifiers, + trace_nix_log_message, + ctx.should_quit.clone(), + ) + .await? + .0, ) - .await? - .0, - ) - } else { - Either::Right( - NixClient::open_local( - trace_nix_log_message, - ctx.should_quit.clone(), - ctx.modifiers.print_build_logs, + } else { + Either::Right( + NixClient::open_local( + trace_nix_log_message, + ctx.should_quit.clone(), + ctx.modifiers.print_build_logs, + ) + .await + .map_err(HiveLibError::NixDaemonClientError)?, ) - .await - .map_err(HiveLibError::NixDaemonClientError)?, + }; + + let mut output_map = match connection { + Either::Left(ref mut conn) => conn.query_derivation_output_map(top_level).await, + Either::Right(ref mut conn) => conn.query_derivation_output_map(top_level).await, + } + .map_err(|err| HiveLibError::NixBuildError { + name: ctx.name.clone(), + source: err, + })?; + + debug!(output_map = ?output_map, "got output map"); + + let output_path = + output_map + .remove(SYSTEM_OUTPUT) + .flatten() + .ok_or(HiveLibError::NixBuildError { + name: ctx.name.clone(), + source: NixDaemonClientError::NixDaemonInvalidResponse(format!( + "Derivation {top_level:?} did not have output {SYSTEM_OUTPUT:?}" + )), + })?; + + let derived_path = DerivedPath { + store_path: top_level, + outputs: DerivedPathOutput::OutputNames(&[SYSTEM_OUTPUT]), + }; + + match connection { + Either::Left(mut conn) => conn.build(&vec![derived_path]).await, + Either::Right(mut conn) => conn.build(&vec![derived_path]).await, + } + .map_err(|source| HiveLibError::NixBuildError { + name: ctx.name.clone(), + source, + })?; + + info!("Built output: {output_path:?}"); + + // print built path to stdout + println!("{}", output_path.to_absolute_path()); + + ctx.state.build = Some(output_path); + } else { + // use regular nix build command + let mut command_string = CommandStringBuilder::nix(); + command_string.args(&[ + "--extra-experimental-features", + "nix-command", + "build", + "--no-link", + "--print-out-paths", + ]); + command_string.opt_arg(ctx.modifiers.print_build_logs, "--print-build-logs"); + command_string.arg(format!("{}^*", top_level.to_absolute_path())); + + let status = run_command_with_env( + &CommandArguments::new(command_string, ctx.modifiers) + // build remotely if asked for AND we isnt applying locally + .execute_on_remote(self.target.clone()) + .mode(crate::commands::ChildOutputMode::Nix) + .log_stdout(), + std::collections::HashMap::new(), ) - }; - - let mut output_map = match connection { - Either::Left(ref mut conn) => conn.query_derivation_output_map(top_level).await, - Either::Right(ref mut conn) => conn.query_derivation_output_map(top_level).await, + .await? + .wait_till_success() + .await + .map_err(|source| HiveLibError::NixBuildCliError { + name: ctx.name.clone(), + source, + })?; + + let stdout = match status { + Either::Left((_, stdout)) | Either::Right((_, stdout)) => stdout, + }; + + info!("Built output: {stdout:?}"); + + // print built path to stdout + println!("{stdout}"); + + ctx.state.build = Some(SafeStorePath::::from_absolute_path( + stdout.as_bytes(), + )?); } - .map_err(|err| HiveLibError::NixBuildError { - name: ctx.name.clone(), - source: err, - })?; - - debug!(output_map = ?output_map, "got output map"); - - let output_path = - output_map - .remove(SYSTEM_OUTPUT) - .flatten() - .ok_or(HiveLibError::NixBuildError { - name: ctx.name.clone(), - source: NixDaemonClientError::NixDaemonInvalidResponse(format!( - "Derivation {top_level:?} did not have output {SYSTEM_OUTPUT:?}" - )), - })?; - - let derived_path = DerivedPath { - store_path: top_level, - outputs: DerivedPathOutput::OutputNames(&[SYSTEM_OUTPUT]), - }; - - match connection { - Either::Left(mut conn) => conn.build(&vec![derived_path]).await, - Either::Right(mut conn) => conn.build(&vec![derived_path]).await, - } - .map_err(|source| HiveLibError::NixBuildError { - name: ctx.name.clone(), - source, - })?; - - info!("Built output: {output_path:?}"); - - // print built path to stdout - println!("{}", output_path.to_absolute_path()); - - ctx.state.build = Some(output_path); Ok(()) } diff --git a/crates/core/src/hive/steps/keys.rs b/crates/core/src/hive/steps/keys.rs index 01681f0..3b9ab7d 100644 --- a/crates/core/src/hive/steps/keys.rs +++ b/crates/core/src/hive/steps/keys.rs @@ -30,7 +30,7 @@ use crate::commands::builder::CommandStringBuilder; use crate::commands::{CommandArguments, WireCommandChip, run_command}; use crate::errors::KeyError; use crate::hive::node::{Context, ExecuteStep, Push, SharedTarget}; -use crate::{HiveLibError, SafeStorePath, push}; +use crate::{HiveLibError, SafeStorePath, push_with_daemon}; #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash)] #[serde(tag = "t", content = "c")] @@ -327,7 +327,7 @@ impl ExecuteStep for PushKeyAgent { .map_err(HiveLibError::StorePath)?; if let Some(ref target) = self.target { - push( + push_with_daemon( ctx, target, Push::Path(&agent_store_path), diff --git a/crates/core/src/hive/steps/push.rs b/crates/core/src/hive/steps/push.rs index e88cad3..be1f884 100644 --- a/crates/core/src/hive/steps/push.rs +++ b/crates/core/src/hive/steps/push.rs @@ -8,7 +8,6 @@ use tracing::instrument; use crate::{ HiveLibError, hive::node::{Context, ExecuteStep, SharedTarget}, - push, }; #[derive(Debug)] @@ -42,13 +41,23 @@ impl ExecuteStep for PushEvaluatedOutput { async fn execute(&self, ctx: &mut Context) -> Result<(), HiveLibError> { let top_level = ctx.state.evaluation.as_ref().unwrap(); - push( - ctx, - &self.target, - crate::hive::node::Push::Derivation(top_level), - self.substitute_on_destination, - ) - .await?; + if ctx.modifiers.experimental_nix_client { + crate::push_with_daemon( + ctx, + &self.target, + crate::hive::node::Push::Derivation(top_level), + self.substitute_on_destination, + ) + .await?; + } else { + crate::commands::common::push( + ctx, + &self.target, + crate::hive::node::Push::Derivation(top_level), + self.substitute_on_destination, + ) + .await?; + } Ok(()) } @@ -59,13 +68,23 @@ impl ExecuteStep for PushBuildOutput { async fn execute(&self, ctx: &mut Context) -> Result<(), HiveLibError> { let built_path = ctx.state.build.as_ref().unwrap(); - push( - ctx, - &self.target, - crate::hive::node::Push::Path(built_path), - self.substitute_on_destination, - ) - .await?; + if ctx.modifiers.experimental_nix_client { + crate::push_with_daemon( + ctx, + &self.target, + crate::hive::node::Push::Path(built_path), + self.substitute_on_destination, + ) + .await?; + } else { + crate::commands::common::push( + ctx, + &self.target, + crate::hive::node::Push::Path(built_path), + self.substitute_on_destination, + ) + .await?; + } Ok(()) } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 9764251..66003a5 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -18,7 +18,7 @@ use tokio::{ process::{ChildStdin, ChildStdout, Command}, sync::{AcquireError, Semaphore, SemaphorePermit, mpsc::UnboundedSender, oneshot}, }; -use tracing::{debug, info, instrument, trace}; +use tracing::{info, instrument, trace}; use wire_nix_client::{ NixClient, NixDaemonClientError, WireAddToStoreNarRequest, store_path::SafeStorePath, }; @@ -54,12 +54,14 @@ pub enum StrictHostKeyChecking { } #[derive(Debug, Clone, Copy)] +#[allow(clippy::struct_excessive_bools)] pub struct SubCommandModifiers { pub show_trace: bool, pub non_interactive: bool, pub ssh_accept_host: StrictHostKeyChecking, pub ssh_verbosity: usize, pub print_build_logs: bool, + pub experimental_nix_client: bool, } impl Default for SubCommandModifiers { @@ -70,6 +72,7 @@ impl Default for SubCommandModifiers { ssh_accept_host: StrictHostKeyChecking::default(), ssh_verbosity: 0, print_build_logs: false, + experimental_nix_client: false, } } } @@ -168,7 +171,8 @@ fn get_common_copy_path_help(error: &NixDaemonClientError) -> Option { } } -pub async fn push( +/// Pushes the path with a native daemon. +pub async fn push_with_daemon( context: &Context, target: &SharedTarget, push: Push<'_>,