git.delta.rocks / jrsonnet / refs/commits / 94ece5cae749

difftreelog

build update to nixos release-25.05

luvuxwnmYaroslav Bolyukin2025-06-28parent: #1470de8.patch.diff
in: trunk

34 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "abort-on-drop"
@@ -377,29 +377,6 @@
 ]
 
 [[package]]
-name = "bindgen"
-version = "0.69.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
-dependencies = [
- "bitflags",
- "cexpr",
- "clang-sys",
- "itertools 0.12.1",
- "lazy_static",
- "lazycell",
- "log",
- "prettyplease",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash",
- "shlex",
- "syn",
- "which",
-]
-
-[[package]]
 name = "bitflags"
 version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -488,15 +465,6 @@
 ]
 
 [[package]]
-name = "cexpr"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
-dependencies = [
- "nom 7.1.3",
-]
-
-[[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -556,17 +524,6 @@
  "crypto-common",
  "inout",
  "zeroize",
-]
-
-[[package]]
-name = "clang-sys"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
-dependencies = [
- "glob",
- "libc",
- "libloading",
 ]
 
 [[package]]
@@ -1242,12 +1199,6 @@
 version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
-[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "h2"
@@ -1332,15 +1283,6 @@
 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
  "digest",
-]
-
-[[package]]
-name = "home"
-version = "0.5.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
-dependencies = [
- "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1648,15 +1590,6 @@
 version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
-
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
 
 [[package]]
 name = "itertools"
@@ -1701,28 +1634,12 @@
 ]
 
 [[package]]
-name = "lazycell"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
-
-[[package]]
 name = "libc"
 version = "0.2.174"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
 
 [[package]]
-name = "libloading"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
-dependencies = [
- "cfg-if",
- "windows-targets",
-]
-
-[[package]]
 name = "libm"
 version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1887,14 +1804,6 @@
  "tokio-util",
  "tracing",
  "unindent",
-]
-
-[[package]]
-name = "nix-native-eval"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "nixrs",
 ]
 
 [[package]]
@@ -1912,25 +1821,6 @@
 ]
 
 [[package]]
-name = "nixrs"
-version = "0.1.0"
-source = "git+https://github.com/Anillc/nixrs#740fcf4048cc5b6de8c54d18254f12d53909a867"
-dependencies = [
- "libc",
- "nixrs-sys",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "nixrs-sys"
-version = "0.1.0"
-source = "git+https://github.com/Anillc/nixrs#740fcf4048cc5b6de8c54d18254f12d53909a867"
-dependencies = [
- "bindgen",
- "pkg-config",
-]
-
-[[package]]
 name = "nom"
 version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2224,12 +2114,6 @@
  "der",
  "spki",
 ]
-
-[[package]]
-name = "pkg-config"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
 
 [[package]]
 name = "poly1305"
@@ -3752,18 +3636,6 @@
 dependencies = [
  "js-sys",
  "wasm-bindgen",
-]
-
-[[package]]
-name = "which"
-version = "4.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
-dependencies = [
- "either",
- "home",
- "once_cell",
- "rustix 0.38.40",
 ]
 
 [[package]]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,8 +2,8 @@
 members = ["crates/*", "cmds/*"]
 resolver = "2"
 package.version = "0.1.0"
-package.edition = "2021"
-package.rust-version = "1.82.0"
+package.edition = "2024"
+package.rust-version = "1.86.0"
 
 [workspace.dependencies]
 better-command = { path = "./crates/better-command" }
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,15 +1,15 @@
 use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
 
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 use clap::Parser;
 use fleet_base::{
-	deploy::{deploy_task, upload_task, DeployAction},
+	deploy::{DeployAction, deploy_task, upload_task},
 	host::{Config, DeployKind, GenerationStorage},
 	opts::FleetOpts,
 };
-use nix_eval::{nix_go, NixBuildBatch};
+use nix_eval::{NixBuildBatch, nix_go};
 use tokio::task::LocalSet;
-use tracing::{error, field, info, info_span, warn, Instrument};
+use tracing::{Instrument, error, field, info, info_span, warn};
 
 #[derive(Parser)]
 pub struct Deploy {
@@ -167,11 +167,12 @@
 						self.action,
 						&host,
 						remote_path,
-						if let Ok(v) = opts.action_attr(&host, "specialisation").await {
-							v
-						} else {
-							error!("unreachable? failed to get specialization");
-							return;
+						match opts.action_attr(&host, "specialisation").await {
+							Ok(v) => v,
+							_ => {
+								error!("unreachable? failed to get specialization");
+								return;
+							}
 						},
 						disable_rollback,
 					)
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,6 +1,6 @@
 use std::collections::BTreeSet;
 
-use anyhow::{ensure, Result};
+use anyhow::{Result, ensure};
 use clap::Parser;
 use fleet_base::host::Config;
 use nix_eval::nix_go_json;
modifiedcmds/fleet/src/cmds/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/mod.rs
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -1,6 +1,6 @@
 pub mod build_systems;
 pub mod complete;
 pub mod info;
+pub mod rollback;
 pub mod secrets;
 pub mod tf;
-pub mod rollback;
\ No newline at end of file
modifiedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -1,9 +1,9 @@
 use std::collections::HashSet;
 
-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
 use clap::Parser;
 use fleet_base::{
-	deploy::{deploy_task, upload_task, DeployAction},
+	deploy::{DeployAction, deploy_task, upload_task},
 	host::{Config, ConfigHost, Generation, GenerationStorage},
 	opts::FleetOpts,
 };
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,25 +1,25 @@
 use std::{
 	collections::{BTreeMap, BTreeSet, HashSet},
-	io::{self, stdin, stdout, Read, Write},
+	io::{self, Read, Write, stdin, stdout},
 	path::PathBuf,
 };
 
 use age::Recipient;
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use chrono::{DateTime, Utc};
 use clap::Parser;
 use fleet_base::{
-	fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},
+	fleetdata::{FleetSecret, FleetSecretPart, FleetSharedSecret, encrypt_secret_data},
 	host::Config,
 	opts::FleetOpts,
 };
 use fleet_shared::SecretData;
-use nix_eval::{nix_go, nix_go_json, NixBuildBatch, Value};
+use nix_eval::{NixBuildBatch, Value, nix_go, nix_go_json};
 use owo_colors::OwoColorize;
 use serde::Deserialize;
 use tabled::{Table, Tabled};
 use tokio::fs::read;
-use tracing::{error, info, info_span, warn, Instrument};
+use tracing::{Instrument, error, info, info_span, warn};
 
 #[derive(Parser)]
 pub enum Secret {
@@ -187,7 +187,9 @@
 		true
 	} else if set.difference(&expected_set).next().is_some() {
 		// TODO: Remove this warning for revokable secrets.
-		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");
+		warn!(
+			"host was removed from secret owners, but until this host rebuild, the secret will still be stored on it."
+		);
 		nix_go_json!(field.regenerateOnOwnerRemoved)
 	} else if expected_set.difference(&set).next().is_some() {
 		nix_go_json!(field.regenerateOnOwnerAdded)
@@ -296,8 +298,8 @@
 	let out_parent = host.mktemp_dir().await?;
 	let out = format!("{out_parent}/out");
 
-	let mut gen = host.cmd(generator).await?;
-	gen.env("out", &out);
+	let mut r#gen = host.cmd(generator).await?;
+	r#gen.env("out", &out);
 	if on.is_none() {
 		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.
 		let project_path: String = config
@@ -306,9 +308,9 @@
 			.into_os_string()
 			.into_string()
 			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;
-		gen.env("FLEET_PROJECT", project_path);
+		r#gen.env("FLEET_PROJECT", project_path);
 	}
-	gen.run().await.context("impure generator")?;
+	r#gen.run().await.context("impure generator")?;
 
 	{
 		let marker = host.read_file_text(format!("{out}/marker")).await?;
@@ -510,7 +512,9 @@
 	if !remove_machines.is_empty() {
 		// TODO: maybe force secret regeneration?
 		// Not that useful without revokation.
-		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
+		warn!(
+			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"
+		);
 	}
 	Ok(target_machines)
 }
@@ -596,7 +600,9 @@
 				part: part_name,
 			} => {
 				if config.has_secret(&machine, &name) && !replace && !merge {
-					bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");
+					bail!(
+						"secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret"
+					);
 				}
 
 				let mut out = if merge && !replace {
modifiedcmds/fleet/src/extra_args.rsdiffbeforeafterboth
--- a/cmds/fleet/src/extra_args.rs
+++ b/cmds/fleet/src/extra_args.rs
@@ -1,6 +1,6 @@
 use std::ffi::{OsStr, OsString};
 
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 
 pub fn parse_os(os: &OsStr) -> Result<Vec<OsString>> {
 	Ok(shlex::bytes::split(os.as_encoded_bytes())
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -6,27 +6,27 @@
 
 use std::{ffi::OsString, process::ExitCode};
 
-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
 use clap::{CommandFactory, Parser};
 use cmds::{
 	build_systems::{BuildSystems, Deploy},
-	rollback::RollbackSingle,
 	complete::Complete,
 	info::Info,
+	rollback::RollbackSingle,
 	secrets::Secret,
 	tf::Tf,
 };
 use fleet_base::{host::Config, opts::FleetOpts};
-use futures::{future::LocalBoxFuture, stream::FuturesUnordered, TryStreamExt};
+use futures::{TryStreamExt, future::LocalBoxFuture, stream::FuturesUnordered};
 // use host::Config;
 #[cfg(feature = "indicatif")]
 use human_repr::HumanCount;
 #[cfg(feature = "indicatif")]
 use indicatif::{ProgressState, ProgressStyle};
-use tracing::{error, info, info_span, Instrument};
+use tracing::{Instrument, error, info, info_span};
 #[cfg(feature = "indicatif")]
 use tracing_indicatif::IndicatifLayer;
-use tracing_subscriber::{prelude::*, EnvFilter};
+use tracing_subscriber::{EnvFilter, prelude::*};
 
 #[derive(Parser)]
 struct Prefetch {}
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -1,21 +1,22 @@
 use std::{
 	env,
 	fs::{File, OpenOptions},
-	io::{self, copy, stdin, stdout, Read, Write},
+	io::{self, Read, Write, copy, stdin, stdout},
 	str::FromStr,
 };
 
 use age::{
+	Encryptor, Recipient,
 	ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
-	Encryptor, Recipient,
 };
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use clap::{Parser, ValueEnum};
 use ed25519_dalek::SecretKey;
 use fleet_shared::SecretData;
 use rand::{
+	RngCore,
 	distr::{Alphanumeric, Distribution, SampleString, Uniform},
-	rng, RngCore,
+	rng,
 };
 
 fn write_output_file(out: &str) -> Result<File> {
@@ -78,7 +79,9 @@
 	let list = match list {
 		Ok(v) => v,
 		Err(env::VarError::NotPresent) => {
-			bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities");
+			bail!(
+				"gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities"
+			);
 		}
 		Err(e) => bail!("somehow, identities list is not utf-8: {e}"),
 	};
@@ -254,13 +257,7 @@
 					write_private(
 						&recipients,
 						&private,
-						&key[..{
-							if no_embed_public {
-								32
-							} else {
-								64
-							}
-						}],
+						&key[..{ if no_embed_public { 32 } else { 64 } }],
 						encoding,
 					)?;
 				}
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -5,20 +5,20 @@
 	iter,
 	os::unix::prelude::PermissionsExt,
 	path::{Path, PathBuf},
-	str::{from_utf8, FromStr},
+	str::{FromStr, from_utf8},
 };
 
 use age::{
-	ssh::{Identity as SshIdentity, Recipient as SshRecipient},
 	Decryptor, Encryptor, Identity, Recipient,
+	ssh::{Identity as SshIdentity, Recipient as SshRecipient},
 };
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use clap::Parser;
 use fleet_shared::SecretData;
-use nix::unistd::{chown, Group, User};
+use nix::unistd::{Group, User, chown};
 use serde::Deserialize;
 use tracing::{error, info, info_span};
-use tracing_subscriber::{filter::LevelFilter, EnvFilter};
+use tracing_subscriber::{EnvFilter, filter::LevelFilter};
 
 #[derive(Parser)]
 #[clap(author)]
modifiedcmds/terraform-provider-fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/terraform-provider-fleet/src/main.rs
+++ b/cmds/terraform-provider-fleet/src/main.rs
@@ -1,3 +1,5 @@
 fn main() {
-	panic!("this is a stub, real provider is in development, I just don't want to keep it in different branch.")
+	panic!(
+		"this is a stub, real provider is in development, I just don't want to keep it in different branch."
+	)
 }
modifiedcrates/better-command/src/handler.rsdiffbeforeafterboth
--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -7,7 +7,7 @@
 
 use regex::Regex;
 use serde::Deserialize;
-use tracing::{info, info_span, warn, Span};
+use tracing::{Span, info, info_span, warn};
 #[cfg(feature = "indicatif")]
 use tracing_indicatif::span_ext::IndicatifSpanExt as _;
 
@@ -112,9 +112,13 @@
 			match log {
 				NixLog::Msg { msg, raw_msg, .. } => {
 					#[allow(clippy::nonminimal_bool)]
-					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty"))
-					&& !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake")
-					&& msg != "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy" {
+					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '")
+						&& msg.ends_with("' is dirty"))
+						&& !msg.starts_with(
+							"\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake",
+						) && msg
+						!= "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy"
+					{
 						if let Some(raw_msg) = raw_msg {
 							if !msg.is_empty() {
 								info!(target: "nix", "{}\n{}", raw_msg.trim_end(), msg.trim_end())
@@ -156,8 +160,12 @@
 					id,
 					..
 				} if typ == 100 && fields.len() >= 3 => {
-					if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] =
-						&fields[..]
+					if let [
+						LogField::String(drv),
+						LogField::String(from),
+						LogField::String(to),
+						..,
+					] = &fields[..]
 					{
 						let mut drv = drv.as_str();
 
@@ -289,8 +297,12 @@
 				}
 				NixLog::Result { fields, id, typ } if typ == 105 && fields.len() >= 4 => {
 					if let Some(span) = self.spans.get(&id) {
-						if let [LogField::Num(done), LogField::Num(expected), LogField::Num(_running), LogField::Num(_failed)] =
-							&fields[..4]
+						if let [
+							LogField::Num(done),
+							LogField::Num(expected),
+							LogField::Num(_running),
+							LogField::Num(_failed),
+						] = &fields[..4]
 						{
 							#[cfg(feature = "indicatif")]
 							{
modifiedcrates/fleet-base/src/command.rsdiffbeforeafterboth
before · crates/fleet-base/src/command.rs
1use std::{ffi::OsStr, pin, process::Stdio, sync::Arc, task::Poll};23use anyhow::{anyhow, Result};4use better_command::{Handler, NixHandler, PlainHandler};5use futures::StreamExt;6use itertools::Either;7use openssh::{OverSsh, OwningCommand, Session};8use serde::de::DeserializeOwned;9use tokio::{io::AsyncRead, process::Command, select};10use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};11use tracing::debug;1213use crate::host::EscalationStrategy;1415fn escape_bash(input: &str, out: &mut String) {16	const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";17	if input.chars().all(|c| !TO_ESCAPE.contains(c)) {18		out.push_str(input);19		return;20	}21	out.push('\'');22	for (i, v) in input.split('\'').enumerate() {23		if i != 0 {24			out.push_str("'\"'\"'");25		}26		out.push_str(v);27	}28	out.push('\'');29}30fn ostoutf8(os: impl AsRef<OsStr>) -> String {31	os.as_ref().to_str().expect("non-utf8 data").to_owned()32}3334#[derive(Clone, Debug)]35pub struct MyCommand {36	command: String,37	args: Vec<String>,38	env: Vec<(String, String)>,39	ssh_session: Option<Arc<Session>>,40	escalation: EscalationStrategy,41	escalate: bool,42}43impl MyCommand {44	pub fn new_on(45		escalation: EscalationStrategy,46		cmd: impl AsRef<OsStr>,47		session: Arc<Session>,48	) -> Self {49		assert!(!cmd.as_ref().is_empty());50		Self {51			command: ostoutf8(cmd),52			args: vec![],53			env: vec![],54			ssh_session: Some(session),55			escalation,56			escalate: false,57		}58	}59	pub fn new(escalation: EscalationStrategy, cmd: impl AsRef<OsStr>) -> Self {60		assert!(!cmd.as_ref().is_empty());61		Self {62			command: ostoutf8(cmd),63			args: vec![],64			env: vec![],65			ssh_session: None,66			escalation,67			escalate: false,68		}69	}70	fn new_here(&self, cmd: impl AsRef<OsStr>) -> Self {71		if let Some(ssh_session) = self.ssh_session.clone() {72			Self::new_on(self.escalation, cmd, ssh_session)73		} else {74			Self::new(self.escalation, cmd)75		}76	}7778	fn into_args(self) -> Vec<String> {79		let mut out = Vec::new();80		if !self.env.is_empty() {81			out.push("env".to_owned());82			for (k, v) in self.env {83				assert!(!k.contains('='));84				out.push(format!("{k}={v}"));85			}86		}87		out.push(self.command);88		out.extend(self.args);89		out90	}9192	/// Translates environment variables into env command execution.93	/// Required for ssh, as ssh don't allow to send environment variables (at least by default).94	///95	/// FIXME: Insecure, as arguments might be seen by other users on the same machine.96	/// Figure out some way to transfer environment using stdio?97	fn translate_env_into_env(self) -> Self {98		if self.env.is_empty() {99			return self;100		}101		let mut out = self.new_here("env");102		for (k, v) in self.env {103			assert!(!k.contains('='));104			out.arg(format!("{k}={v}"));105		}106		out.arg(self.command);107		out.args(self.args);108109		out110	}111	fn into_string(self) -> String {112		let mut out = String::new();113		if !self.env.is_empty() {114			out.push_str("env");115			for (k, v) in self.env {116				out.push(' ');117				assert!(!k.contains('='));118				escape_bash(&k, &mut out);119				out.push('=');120				escape_bash(&v, &mut out);121			}122		}123		if !out.is_empty() {124			out.push(' ');125		}126		escape_bash(&self.command, &mut out);127		for arg in self.args {128			out.push(' ');129			escape_bash(&arg, &mut out);130		}131		out132	}133	fn into_command_unchecked_local(self) -> Command {134		let mut out = Command::new(self.command);135		out.args(self.args);136		for (k, v) in self.env {137			out.env(k, v);138		}139		out140	}141	fn into_command(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {142		Ok(if let Some(session) = self.ssh_session.clone() {143			let cmd = self.translate_env_into_env().into_command_unchecked_local();144			Either::Right(145				cmd.over_ssh(session)146					.map_err(|e| anyhow!("ssh error: {e}"))?,147			)148		} else {149			let cmd = self.into_command_unchecked_local();150			Either::Left(cmd)151		})152	}153	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {154		let arg = arg.as_ref();155		self.args.push(ostoutf8(arg));156		self157	}158	pub fn eqarg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {159		let arg = arg.as_ref();160		let value = value.as_ref();161		let arg = ostoutf8(arg);162		let value = ostoutf8(value);163		self.arg(format!("{arg}={value}"));164		self165	}166	pub fn comparg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {167		self.arg(arg);168		self.arg(value);169		self170	}171	pub fn env(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {172		self.env173			.push((name.as_ref().to_owned(), value.as_ref().to_owned()));174		self175	}176	pub fn args<V: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {177		for arg in args.into_iter() {178			let arg = arg.as_ref();179			self.args.push(ostoutf8(arg));180		}181		self182	}183	pub fn sudo(mut self) -> Self {184		self.escalate = true;185		self186	}187	fn wrap_sudo_if_needed(self) -> Self {188		if !self.escalate {189			return self;190		}191		match self.escalation {192			EscalationStrategy::Su => {193				let mut out = self.new_here("su");194				out.arg("-c").arg(self.into_string());195				out196			}197			EscalationStrategy::Sudo => {198				let mut out = self.new_here("sudo");199				out.args(self.into_args());200				out201			}202			EscalationStrategy::Run0 => {203				// run0 wants interactive authentication by default.204				let mut run0 = self.new_here("run0");205				let mut out = self.new_here("script");206207				// Red backgrounds messes with fleet formatting208				run0.arg("--background=");209				run0.args(self.into_args());210211				out.arg("-q");212				out.arg("/dev/null");213				out.arg("-c");214				out.arg(run0.into_string());215				dbg!(&out);216				out217			}218		}219	}220221	pub async fn run(self) -> Result<()> {222		let str = self.clone().into_string();223		let cmd = self.wrap_sudo_if_needed().into_command()?;224		match cmd {225			Either::Left(cmd) => run_nix_inner(str, cmd, &mut PlainHandler).await?,226			Either::Right(cmd) => run_nix_inner_ssh(str, cmd, &mut PlainHandler).await?,227		};228		Ok(())229	}230	pub async fn run_string(self) -> Result<String> {231		let bytes = self.run_bytes().await?;232		Ok(String::from_utf8(bytes)?)233	}234	pub async fn run_value<T: DeserializeOwned>(self) -> Result<T> {235		let v = self.run_string().await?;236		Ok(serde_json::from_str(&v)?)237	}238	pub async fn run_bytes(self) -> Result<Vec<u8>> {239		let str = self.clone().into_string();240		let cmd = self.wrap_sudo_if_needed().into_command()?;241		let v = match cmd {242			Either::Left(cmd) => run_nix_inner_stdout(str, cmd, &mut PlainHandler).await?,243			Either::Right(cmd) => run_nix_inner_stdout_ssh(str, cmd, &mut PlainHandler).await?,244		};245		Ok(v)246	}247248	pub async fn run_nix_string(mut self) -> Result<String> {249		let str = self.clone().into_string();250		self.arg("--log-format").arg("internal-json");251		let cmd = self.wrap_sudo_if_needed().into_command()?;252		let bytes = match cmd {253			Either::Left(cmd) => run_nix_inner_stdout(str, cmd, &mut NixHandler::default()).await?,254			Either::Right(cmd) => {255				run_nix_inner_stdout_ssh(str, cmd, &mut NixHandler::default()).await?256			}257		};258		Ok(String::from_utf8(bytes)?)259	}260	pub async fn run_nix(mut self) -> Result<()> {261		let str = self.clone().into_string();262		self.arg("--log-format").arg("internal-json");263		let cmd = self.wrap_sudo_if_needed().into_command()?;264		match cmd {265			Either::Left(mut cmd) => {266				cmd.stdout(Stdio::inherit());267				run_nix_inner(str, cmd, &mut NixHandler::default()).await268			}269			Either::Right(mut cmd) => {270				cmd.stdout(openssh::Stdio::inherit());271				run_nix_inner_ssh(str, cmd, &mut NixHandler::default()).await272			}273		}274	}275}276277struct EmptyAsyncRead;278impl AsyncRead for EmptyAsyncRead {279	fn poll_read(280		self: std::pin::Pin<&mut Self>,281		_cx: &mut std::task::Context<'_>,282		_buf: &mut tokio::io::ReadBuf<'_>,283	) -> Poll<std::io::Result<()>> {284		Poll::Pending285	}286}287288async fn run_nix_inner_stdout(289	str: String,290	cmd: Command,291	handler: &mut dyn Handler,292) -> Result<Vec<u8>> {293	Ok(run_nix_inner_raw(str, cmd, true, handler, None)294		.await?295		.expect("has out"))296}297async fn run_nix_inner(str: String, cmd: Command, handler: &mut dyn Handler) -> Result<()> {298	let v = run_nix_inner_raw(str, cmd, false, handler, None).await?;299	assert!(v.is_none());300	Ok(())301}302async fn run_nix_inner_stdout_ssh(303	str: String,304	cmd: OwningCommand<Arc<Session>>,305	handler: &mut dyn Handler,306) -> Result<Vec<u8>> {307	Ok(run_nix_inner_raw_ssh(str, cmd, true, handler, None)308		.await?309		.expect("has out"))310}311async fn run_nix_inner_ssh(312	str: String,313	cmd: OwningCommand<Arc<Session>>,314	handler: &mut dyn Handler,315) -> Result<()> {316	let v = run_nix_inner_raw_ssh(str, cmd, false, handler, None).await?;317	assert!(v.is_none());318	Ok(())319}320321async fn run_nix_inner_raw(322	str: String,323	mut cmd: Command,324	want_stdout: bool,325	err_handler: &mut dyn Handler,326	mut out_handler: Option<&mut dyn Handler>,327) -> Result<Option<Vec<u8>>> {328	cmd.stderr(Stdio::piped());329	cmd.stdout(Stdio::piped());330	debug!("running command {str:?} on local");331	let mut child = cmd.spawn()?;332	let mut stderr = child.stderr.take().unwrap();333	let stdout = child.stdout.take().unwrap();334	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());335	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));336	let mut ob = want_stdout337		.then(|| out.take().unwrap())338		.unwrap_or_else(|| Box::new(EmptyAsyncRead));339	let mut ol = (!want_stdout)340		.then(|| out.take().unwrap())341		.unwrap_or_else(|| Box::new(EmptyAsyncRead));342	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());343	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());344345	// while let Some(line) = read.next().await? {}346347	let mut out_buf = if want_stdout { Some(vec![]) } else { None };348	loop {349		select! {350			e = err.next() => {351				if let Some(e) = e {352					let e = e?;353					err_handler.handle_line(&e);354				}355			},356			o = ob.next() => {357				if let Some(o) = o {358					out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);359				}360			},361			o = ol.next() => {362				if let Some(o) = o {363					let o = o?;364					if let Some(out) = out_handler.as_mut() {365						out.handle_line(&o)366					} else {367						err_handler.handle_line(&o)368					}369					// out_handler.handle_info(&o);370				}371			},372			code = child.wait() => {373				let code = code?;374				if !code.success() {375					anyhow::bail!("command '{str}' failed with status {}", code);376				}377				break;378			}379		}380	}381382	Ok(out_buf)383}384async fn run_nix_inner_raw_ssh(385	str: String,386	mut cmd: OwningCommand<Arc<Session>>,387	want_stdout: bool,388	err_handler: &mut dyn Handler,389	mut out_handler: Option<&mut dyn Handler>,390) -> Result<Option<Vec<u8>>> {391	debug!("running command {str:?} over ssh");392	cmd.stderr(openssh::Stdio::piped());393	cmd.stdout(openssh::Stdio::piped());394	let mut child = cmd.spawn().await?;395	let mut stderr = child.stderr().take().unwrap();396	let stdout = child.stdout().take().unwrap();397	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());398	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));399	let mut ob = want_stdout400		.then(|| out.take().unwrap())401		.unwrap_or_else(|| Box::new(EmptyAsyncRead));402	let mut ol = (!want_stdout)403		.then(|| out.take().unwrap())404		.unwrap_or_else(|| Box::new(EmptyAsyncRead));405	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());406	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());407408	// while let Some(line) = read.next().await? {}409410	let mut out_buf = if want_stdout { Some(vec![]) } else { None };411412	let mut wait_future = pin::pin!(child.wait());413	loop {414		select! {415			e = err.next() => {416				if let Some(e) = e {417					let e = e?;418					err_handler.handle_line(&e);419				}420			},421			o = ob.next() => {422				if let Some(o) = o {423					out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);424				}425			},426			o = ol.next() => {427				if let Some(o) = o {428					let o = o?;429					if let Some(out) = out_handler.as_mut() {430						out.handle_line(&o)431					} else {432						err_handler.handle_line(&o)433					}434					// out_handler.handle_info(&o);435				}436			},437			code = &mut wait_future => {438				let code = code?;439				if !code.success() {440					anyhow::bail!("command '{str}' failed with status {}", code);441				}442				break;443			}444		}445	}446447	Ok(out_buf)448}
after · crates/fleet-base/src/command.rs
1use std::{ffi::OsStr, pin, process::Stdio, sync::Arc, task::Poll};23use anyhow::{Result, anyhow};4use better_command::{Handler, NixHandler, PlainHandler};5use futures::StreamExt;6use itertools::Either;7use openssh::{OverSsh, OwningCommand, Session};8use serde::de::DeserializeOwned;9use tokio::{io::AsyncRead, process::Command, select};10use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};11use tracing::debug;1213use crate::host::EscalationStrategy;1415fn escape_bash(input: &str, out: &mut String) {16	const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";17	if input.chars().all(|c| !TO_ESCAPE.contains(c)) {18		out.push_str(input);19		return;20	}21	out.push('\'');22	for (i, v) in input.split('\'').enumerate() {23		if i != 0 {24			out.push_str("'\"'\"'");25		}26		out.push_str(v);27	}28	out.push('\'');29}30fn ostoutf8(os: impl AsRef<OsStr>) -> String {31	os.as_ref().to_str().expect("non-utf8 data").to_owned()32}3334#[derive(Clone, Debug)]35pub struct MyCommand {36	command: String,37	args: Vec<String>,38	env: Vec<(String, String)>,39	ssh_session: Option<Arc<Session>>,40	escalation: EscalationStrategy,41	escalate: bool,42}43impl MyCommand {44	pub fn new_on(45		escalation: EscalationStrategy,46		cmd: impl AsRef<OsStr>,47		session: Arc<Session>,48	) -> Self {49		assert!(!cmd.as_ref().is_empty());50		Self {51			command: ostoutf8(cmd),52			args: vec![],53			env: vec![],54			ssh_session: Some(session),55			escalation,56			escalate: false,57		}58	}59	pub fn new(escalation: EscalationStrategy, cmd: impl AsRef<OsStr>) -> Self {60		assert!(!cmd.as_ref().is_empty());61		Self {62			command: ostoutf8(cmd),63			args: vec![],64			env: vec![],65			ssh_session: None,66			escalation,67			escalate: false,68		}69	}70	fn new_here(&self, cmd: impl AsRef<OsStr>) -> Self {71		match self.ssh_session.clone() {72			Some(ssh_session) => Self::new_on(self.escalation, cmd, ssh_session),73			_ => Self::new(self.escalation, cmd),74		}75	}7677	fn into_args(self) -> Vec<String> {78		let mut out = Vec::new();79		if !self.env.is_empty() {80			out.push("env".to_owned());81			for (k, v) in self.env {82				assert!(!k.contains('='));83				out.push(format!("{k}={v}"));84			}85		}86		out.push(self.command);87		out.extend(self.args);88		out89	}9091	/// Translates environment variables into env command execution.92	/// Required for ssh, as ssh don't allow to send environment variables (at least by default).93	///94	/// FIXME: Insecure, as arguments might be seen by other users on the same machine.95	/// Figure out some way to transfer environment using stdio?96	fn translate_env_into_env(self) -> Self {97		if self.env.is_empty() {98			return self;99		}100		let mut out = self.new_here("env");101		for (k, v) in self.env {102			assert!(!k.contains('='));103			out.arg(format!("{k}={v}"));104		}105		out.arg(self.command);106		out.args(self.args);107108		out109	}110	fn into_string(self) -> String {111		let mut out = String::new();112		if !self.env.is_empty() {113			out.push_str("env");114			for (k, v) in self.env {115				out.push(' ');116				assert!(!k.contains('='));117				escape_bash(&k, &mut out);118				out.push('=');119				escape_bash(&v, &mut out);120			}121		}122		if !out.is_empty() {123			out.push(' ');124		}125		escape_bash(&self.command, &mut out);126		for arg in self.args {127			out.push(' ');128			escape_bash(&arg, &mut out);129		}130		out131	}132	fn into_command_unchecked_local(self) -> Command {133		let mut out = Command::new(self.command);134		out.args(self.args);135		for (k, v) in self.env {136			out.env(k, v);137		}138		out139	}140	fn into_command(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {141		Ok(match self.ssh_session.clone() {142			Some(session) => {143				let cmd = self.translate_env_into_env().into_command_unchecked_local();144				Either::Right(145					cmd.over_ssh(session)146						.map_err(|e| anyhow!("ssh error: {e}"))?,147				)148			}149			_ => {150				let cmd = self.into_command_unchecked_local();151				Either::Left(cmd)152			}153		})154	}155	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {156		let arg = arg.as_ref();157		self.args.push(ostoutf8(arg));158		self159	}160	pub fn eqarg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {161		let arg = arg.as_ref();162		let value = value.as_ref();163		let arg = ostoutf8(arg);164		let value = ostoutf8(value);165		self.arg(format!("{arg}={value}"));166		self167	}168	pub fn comparg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {169		self.arg(arg);170		self.arg(value);171		self172	}173	pub fn env(&mut self, name: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {174		self.env175			.push((name.as_ref().to_owned(), value.as_ref().to_owned()));176		self177	}178	pub fn args<V: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {179		for arg in args.into_iter() {180			let arg = arg.as_ref();181			self.args.push(ostoutf8(arg));182		}183		self184	}185	pub fn sudo(mut self) -> Self {186		self.escalate = true;187		self188	}189	fn wrap_sudo_if_needed(self) -> Self {190		if !self.escalate {191			return self;192		}193		match self.escalation {194			EscalationStrategy::Su => {195				let mut out = self.new_here("su");196				out.arg("-c").arg(self.into_string());197				out198			}199			EscalationStrategy::Sudo => {200				let mut out = self.new_here("sudo");201				out.args(self.into_args());202				out203			}204			EscalationStrategy::Run0 => {205				// run0 wants interactive authentication by default.206				let mut run0 = self.new_here("run0");207				let mut out = self.new_here("script");208209				// Red backgrounds messes with fleet formatting210				run0.arg("--background=");211				run0.args(self.into_args());212213				out.arg("-q");214				out.arg("/dev/null");215				out.arg("-c");216				out.arg(run0.into_string());217				dbg!(&out);218				out219			}220		}221	}222223	pub async fn run(self) -> Result<()> {224		let str = self.clone().into_string();225		let cmd = self.wrap_sudo_if_needed().into_command()?;226		match cmd {227			Either::Left(cmd) => run_nix_inner(str, cmd, &mut PlainHandler).await?,228			Either::Right(cmd) => run_nix_inner_ssh(str, cmd, &mut PlainHandler).await?,229		};230		Ok(())231	}232	pub async fn run_string(self) -> Result<String> {233		let bytes = self.run_bytes().await?;234		Ok(String::from_utf8(bytes)?)235	}236	pub async fn run_value<T: DeserializeOwned>(self) -> Result<T> {237		let v = self.run_string().await?;238		Ok(serde_json::from_str(&v)?)239	}240	pub async fn run_bytes(self) -> Result<Vec<u8>> {241		let str = self.clone().into_string();242		let cmd = self.wrap_sudo_if_needed().into_command()?;243		let v = match cmd {244			Either::Left(cmd) => run_nix_inner_stdout(str, cmd, &mut PlainHandler).await?,245			Either::Right(cmd) => run_nix_inner_stdout_ssh(str, cmd, &mut PlainHandler).await?,246		};247		Ok(v)248	}249250	pub async fn run_nix_string(mut self) -> Result<String> {251		let str = self.clone().into_string();252		self.arg("--log-format").arg("internal-json");253		let cmd = self.wrap_sudo_if_needed().into_command()?;254		let bytes = match cmd {255			Either::Left(cmd) => run_nix_inner_stdout(str, cmd, &mut NixHandler::default()).await?,256			Either::Right(cmd) => {257				run_nix_inner_stdout_ssh(str, cmd, &mut NixHandler::default()).await?258			}259		};260		Ok(String::from_utf8(bytes)?)261	}262	pub async fn run_nix(mut self) -> Result<()> {263		let str = self.clone().into_string();264		self.arg("--log-format").arg("internal-json");265		let cmd = self.wrap_sudo_if_needed().into_command()?;266		match cmd {267			Either::Left(mut cmd) => {268				cmd.stdout(Stdio::inherit());269				run_nix_inner(str, cmd, &mut NixHandler::default()).await270			}271			Either::Right(mut cmd) => {272				cmd.stdout(openssh::Stdio::inherit());273				run_nix_inner_ssh(str, cmd, &mut NixHandler::default()).await274			}275		}276	}277}278279struct EmptyAsyncRead;280impl AsyncRead for EmptyAsyncRead {281	fn poll_read(282		self: std::pin::Pin<&mut Self>,283		_cx: &mut std::task::Context<'_>,284		_buf: &mut tokio::io::ReadBuf<'_>,285	) -> Poll<std::io::Result<()>> {286		Poll::Pending287	}288}289290async fn run_nix_inner_stdout(291	str: String,292	cmd: Command,293	handler: &mut dyn Handler,294) -> Result<Vec<u8>> {295	Ok(run_nix_inner_raw(str, cmd, true, handler, None)296		.await?297		.expect("has out"))298}299async fn run_nix_inner(str: String, cmd: Command, handler: &mut dyn Handler) -> Result<()> {300	let v = run_nix_inner_raw(str, cmd, false, handler, None).await?;301	assert!(v.is_none());302	Ok(())303}304async fn run_nix_inner_stdout_ssh(305	str: String,306	cmd: OwningCommand<Arc<Session>>,307	handler: &mut dyn Handler,308) -> Result<Vec<u8>> {309	Ok(run_nix_inner_raw_ssh(str, cmd, true, handler, None)310		.await?311		.expect("has out"))312}313async fn run_nix_inner_ssh(314	str: String,315	cmd: OwningCommand<Arc<Session>>,316	handler: &mut dyn Handler,317) -> Result<()> {318	let v = run_nix_inner_raw_ssh(str, cmd, false, handler, None).await?;319	assert!(v.is_none());320	Ok(())321}322323async fn run_nix_inner_raw(324	str: String,325	mut cmd: Command,326	want_stdout: bool,327	err_handler: &mut dyn Handler,328	mut out_handler: Option<&mut dyn Handler>,329) -> Result<Option<Vec<u8>>> {330	cmd.stderr(Stdio::piped());331	cmd.stdout(Stdio::piped());332	debug!("running command {str:?} on local");333	let mut child = cmd.spawn()?;334	let mut stderr = child.stderr.take().unwrap();335	let stdout = child.stdout.take().unwrap();336	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());337	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));338	let mut ob = want_stdout339		.then(|| out.take().unwrap())340		.unwrap_or_else(|| Box::new(EmptyAsyncRead));341	let mut ol = (!want_stdout)342		.then(|| out.take().unwrap())343		.unwrap_or_else(|| Box::new(EmptyAsyncRead));344	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());345	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());346347	// while let Some(line) = read.next().await? {}348349	let mut out_buf = if want_stdout { Some(vec![]) } else { None };350	loop {351		select! {352			e = err.next() => {353				if let Some(e) = e {354					let e = e?;355					err_handler.handle_line(&e);356				}357			},358			o = ob.next() => {359				if let Some(o) = o {360					out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);361				}362			},363			o = ol.next() => {364				if let Some(o) = o {365					let o = o?;366					if let Some(out) = out_handler.as_mut() {367						out.handle_line(&o)368					} else {369						err_handler.handle_line(&o)370					}371					// out_handler.handle_info(&o);372				}373			},374			code = child.wait() => {375				let code = code?;376				if !code.success() {377					anyhow::bail!("command '{str}' failed with status {}", code);378				}379				break;380			}381		}382	}383384	Ok(out_buf)385}386async fn run_nix_inner_raw_ssh(387	str: String,388	mut cmd: OwningCommand<Arc<Session>>,389	want_stdout: bool,390	err_handler: &mut dyn Handler,391	mut out_handler: Option<&mut dyn Handler>,392) -> Result<Option<Vec<u8>>> {393	debug!("running command {str:?} over ssh");394	cmd.stderr(openssh::Stdio::piped());395	cmd.stdout(openssh::Stdio::piped());396	let mut child = cmd.spawn().await?;397	let mut stderr = child.stderr().take().unwrap();398	let stdout = child.stdout().take().unwrap();399	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());400	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));401	let mut ob = want_stdout402		.then(|| out.take().unwrap())403		.unwrap_or_else(|| Box::new(EmptyAsyncRead));404	let mut ol = (!want_stdout)405		.then(|| out.take().unwrap())406		.unwrap_or_else(|| Box::new(EmptyAsyncRead));407	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());408	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());409410	// while let Some(line) = read.next().await? {}411412	let mut out_buf = if want_stdout { Some(vec![]) } else { None };413414	let mut wait_future = pin::pin!(child.wait());415	loop {416		select! {417			e = err.next() => {418				if let Some(e) = e {419					let e = e?;420					err_handler.handle_line(&e);421				}422			},423			o = ob.next() => {424				if let Some(o) = o {425					out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);426				}427			},428			o = ol.next() => {429				if let Some(o) = o {430					let o = o?;431					if let Some(out) = out_handler.as_mut() {432						out.handle_line(&o)433					} else {434						err_handler.handle_line(&o)435					}436					// out_handler.handle_info(&o);437				}438			},439			code = &mut wait_future => {440				let code = code?;441				if !code.success() {442					anyhow::bail!("command '{str}' failed with status {}", code);443				}444				break;445			}446		}447	}448449	Ok(out_buf)450}
modifiedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -1,10 +1,10 @@
 use std::{path::PathBuf, time::Duration};
 
-use anyhow::{anyhow, bail, Context as _, Result};
+use anyhow::{Context as _, Result, anyhow, bail};
 use clap::ValueEnum;
 use itertools::Itertools;
 use tokio::time::sleep;
-use tracing::{error, info, info_span, warn, Instrument as _};
+use tracing::{Instrument as _, error, info, info_span, warn};
 
 use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};
 
@@ -221,7 +221,9 @@
 					.in_current_span()
 					.await
 				{
-					error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
+					error!(
+						"failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}"
+					)
 				}
 			}
 			info!("disarming watchdog, just in case");
@@ -233,12 +235,17 @@
 					error!("failed to disarm rollback run: {e}");
 				}
 			}
-		} else if let Err(_e) = host
-			.rm_file("/etc/fleet_rollback_marker", true)
-			.in_current_span()
-			.await
-		{
-			// Marker might not exist, yet better try to remove it.
+		} else {
+			match host
+				.rm_file("/etc/fleet_rollback_marker", true)
+				.in_current_span()
+				.await
+			{
+				Err(_e) => {
+					// Marker might not exist, yet better try to remove it.
+				}
+				_ => {}
+			}
 		}
 	}
 	Ok(())
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -10,7 +10,7 @@
 	distr::{Alphanumeric, SampleString as _},
 	rng,
 };
-use serde::{de::Error, Deserialize, Serialize};
+use serde::{Deserialize, Serialize, de::Error};
 use serde_json::Value;
 
 #[derive(Serialize, Deserialize, Default)]
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -10,14 +10,14 @@
 	sync::{Arc, Mutex, MutexGuard, OnceLock},
 };
 
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use fleet_shared::SecretData;
-use nix_eval::{nix_go, nix_go_json, util::assert_warn, NixSession, Value};
+use nix_eval::{NixSession, Value, nix_go, nix_go_json, util::assert_warn};
 use openssh::SessionBuilder;
 use serde::de::DeserializeOwned;
 use tabled::Tabled;
 use tempfile::NamedTempFile;
-use time::{format_description, UtcDateTime};
+use time::{UtcDateTime, format_description};
 use tracing::warn;
 
 use crate::{
@@ -87,7 +87,9 @@
 			"fleet" => Ok(Self::Fleet),
 			"nixos-install" => Ok(Self::NixosInstall),
 			"nixos-lustrate" => Ok(Self::NixosLustrate),
-			v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""),
+			v => bail!(
+				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""
+			),
 		}
 	}
 }
@@ -189,11 +191,11 @@
 			.map(|e| e.trim())
 			.filter(|&l| !l.is_empty())
 			.filter_map(|g| {
-				let gen = parse_generation_line(g);
-				if gen.is_none() {
+				let generation = parse_generation_line(g);
+				if generation.is_none() {
 					warn!("bad generation: {g}");
 				};
-				gen
+				generation
 			})
 			.collect::<Vec<_>>();
 		for ele in generations.iter_mut() {
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -1,7 +1,7 @@
 use std::str::FromStr as _;
 
 use age::Recipient;
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 use futures::{StreamExt as _, TryStreamExt as _};
 use itertools::Itertools as _;
 use tracing::warn;
@@ -39,12 +39,12 @@
 		}
 	}
 	/// Insecure, requires root
-	pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient> {
+	pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient + use<>> {
 		let key = self.key(host).await?;
 		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
 	}
 
-	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {
+	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient + use<>>> {
 		let hosts = self.expand_owner_set(hosts).await?;
 		futures::stream::iter(hosts.iter())
 			.then(|m| self.recipient(m.as_ref()))
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -1,6 +1,6 @@
 pub mod command;
+pub mod deploy;
 pub mod fleetdata;
 pub mod host;
 mod keys;
 pub mod opts;
-pub mod deploy;
\ No newline at end of file
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -6,15 +6,15 @@
 	sync::{Arc, Mutex},
 };
 
-use anyhow::{bail, Context, Result};
-use nix_eval::{nix_go, util::assert_warn, NixSessionPool, Value};
+use anyhow::{Context, Result, bail};
+use nix_eval::{NixSessionPool, Value, nix_go, util::assert_warn};
 use nom::{
+	Parser,
 	bytes::complete::take_while1,
 	character::complete::char,
 	combinator::{map, opt},
 	multi::separated_list1,
 	sequence::{preceded, separated_pair},
-	Parser,
 };
 
 use crate::{
@@ -44,7 +44,8 @@
 	let (input, name) = map(
 		take_while1(|v| v != ',' && v != '?' && v != '@'),
 		str::to_owned,
-	).parse_complete(input)
+	)
+	.parse_complete(input)
 	.map_err(err_to_string)?;
 
 	let kw_item = separated_pair(
modifiedcrates/fleet-shared/src/encoding.rsdiffbeforeafterboth
--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -3,8 +3,8 @@
 	str::FromStr,
 };
 
-use base64::engine::{general_purpose::STANDARD_NO_PAD, Engine};
-use serde::{de::Error, Deserialize, Deserializer, Serialize};
+use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};
+use serde::{Deserialize, Deserializer, Serialize, de::Error};
 use unicode_categories::UnicodeCategories;
 
 #[derive(Debug, PartialEq, Clone)]
modifiedcrates/nix-eval/src/pool.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/pool.rs
+++ b/crates/nix-eval/src/pool.rs
@@ -5,7 +5,7 @@
 
 use r2d2::Pool;
 
-use crate::{session::NixSessionInner, Error, NixSession, Result};
+use crate::{Error, NixSession, Result, session::NixSessionInner};
 
 pub struct NixSessionPool(Pool<NixSessionPoolInner>);
 impl NixSessionPool {
modifiedcrates/nix-eval/src/session.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/session.rs
+++ b/crates/nix-eval/src/session.rs
@@ -3,16 +3,16 @@
 use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
 use futures::StreamExt;
 use itertools::Itertools as _;
-use serde::{de::DeserializeOwned, Deserialize};
+use serde::{Deserialize, de::DeserializeOwned};
 use thiserror::Error;
 use tokio::{
 	io::AsyncWriteExt,
 	process::{ChildStderr, ChildStdin, ChildStdout, Command},
 	select,
-	sync::{mpsc, oneshot, Mutex},
+	sync::{Mutex, mpsc, oneshot},
 };
 use tokio_util::codec::{FramedRead, LinesCodec};
-use tracing::{debug, error, warn, Level};
+use tracing::{Level, debug, error, warn};
 
 #[derive(Error, Debug, Clone)]
 pub enum Error {
modifiedcrates/nix-eval/src/util.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -3,7 +3,7 @@
 use anyhow::bail;
 use tracing::{debug, warn};
 
-use crate::{nix_go_json, Value};
+use crate::{Value, nix_go_json};
 
 #[tracing::instrument(level = "info", skip(val))]
 pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
modifiedcrates/nix-eval/src/value.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/value.rs
+++ b/crates/nix-eval/src/value.rs
@@ -1,9 +1,9 @@
 use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
 
 use better_command::NixHandler;
-use serde::{de::DeserializeOwned, Serialize};
+use serde::{Serialize, de::DeserializeOwned};
 
-use crate::{macros::NixExprBuilder, nix_go, Error, NixBuildBatch, NixSession, Result};
+use crate::{Error, NixBuildBatch, NixSession, Result, macros::NixExprBuilder, nix_go};
 
 #[derive(Clone)]
 pub enum Index {
deletedcrates/nix-native-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-native-eval/Cargo.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-[package]
-name = "nix-native-eval"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-
-nixrs = { git = "https://github.com/Anillc/nixrs", version = "0.1.0" }
deletedcrates/nix-native-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-native-eval/src/lib.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use anyhow::Result;
-use nixrs::{State, Store};
-
-pub fn init() -> Result<()> {
-	nixrs::init()?;
-	let store = Store::new("daemon")?;
-	let state = State::new(store)?;
-	let _ = state;
-
-	Ok(())
-}
modifiedcrates/nixlike/fuzz/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/fuzz/Cargo.toml
+++ b/crates/nixlike/fuzz/Cargo.toml
@@ -4,7 +4,7 @@
 version = "0.0.0"
 authors = ["Automatically generated"]
 publish = false
-edition = "2021"
+edition = "2024"
 
 [package.metadata]
 cargo-fuzz = true
modifiedcrates/nixlike/src/de_impl.rsdiffbeforeafterboth
--- a/crates/nixlike/src/de_impl.rs
+++ b/crates/nixlike/src/de_impl.rs
@@ -2,8 +2,8 @@
 
 use linked_hash_map::LinkedHashMap;
 use serde::{
+	Deserializer,
 	de::{self, MapAccess, SeqAccess},
-	Deserializer,
 };
 
 use crate::{Error, Value};
@@ -28,11 +28,12 @@
 	where
 		K: de::DeserializeSeed<'de>,
 	{
-		if let Some((k, v)) = self.iter.next() {
-			let _ = self.value.insert(v);
-			Ok(Some(seed.deserialize(Value::String(k))?))
-		} else {
-			Ok(None)
+		match self.iter.next() {
+			Some((k, v)) => {
+				let _ = self.value.insert(v);
+				Ok(Some(seed.deserialize(Value::String(k))?))
+			}
+			_ => Ok(None),
 		}
 	}
 
@@ -62,10 +63,9 @@
 	where
 		T: de::DeserializeSeed<'de>,
 	{
-		if let Some(v) = self.iter.next() {
-			Ok(Some(seed.deserialize(v)?))
-		} else {
-			Ok(None)
+		match self.iter.next() {
+			Some(v) => Ok(Some(seed.deserialize(v)?)),
+			_ => Ok(None),
 		}
 	}
 }
modifiedcrates/nixlike/src/se_impl.rsdiffbeforeafterboth
--- a/crates/nixlike/src/se_impl.rs
+++ b/crates/nixlike/src/se_impl.rs
@@ -2,11 +2,11 @@
 
 use linked_hash_map::LinkedHashMap;
 use serde::{
+	Serializer,
 	ser::{
 		self, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
 		SerializeTupleStruct, SerializeTupleVariant,
 	},
-	Serializer,
 };
 
 use crate::{Error, Value};
@@ -90,9 +90,7 @@
 
 	fn end(self) -> Result<Self::Ok, Self::Error> {
 		Ok(Value::Object(
-			vec![(self.0, Value::Array(self.1 .0))]
-				.into_iter()
-				.collect(),
+			vec![(self.0, Value::Array(self.1.0))].into_iter().collect(),
 		))
 	}
 }
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1739936662,
-        "narHash": "sha256-x4syUjNUuRblR07nDPeLDP7DpphaBVbUaSoeZkFbGSk=",
+        "lastModified": 1750266157,
+        "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "19de14aaeb869287647d9461cbd389187d8ecdb7",
+        "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
         "type": "github"
       },
       "original": {
@@ -22,11 +22,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1738453229,
-        "narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
+        "lastModified": 1749398372,
+        "narHash": "sha256-tYBdgS56eXYaWVW3fsnPQ/nFlgWi/Z2Ymhyu21zVM98=",
         "owner": "hercules-ci",
         "repo": "flake-parts",
-        "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
+        "rev": "9305fe4e5c2a6fcf5ba6a3ff155720fbe4076569",
         "type": "github"
       },
       "original": {
@@ -37,16 +37,16 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1740339700,
-        "narHash": "sha256-cbrw7EgQhcdFnu6iS3vane53bEagZQy/xyIkDWpCgVE=",
+        "lastModified": 1750895632,
+        "narHash": "sha256-EPZWiRmaSTxoBArK5dQyRlSNVLXiBt2hmsYIPgMf3zk=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "04ef94c4c1582fd485bbfdb8c4a8ba250e359195",
+        "rev": "6ac57ce7fee0d80226095a57ccb7519855ad7c5e",
         "type": "github"
       },
       "original": {
         "owner": "nixos",
-        "ref": "release-24.11",
+        "ref": "release-25.05",
         "repo": "nixpkgs",
         "type": "github"
       }
@@ -68,11 +68,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1740277845,
-        "narHash": "sha256-NNU0CdiaSbAeZ8tpDG4aFi9qtcdlItRvk8Xns9oBrVU=",
+        "lastModified": 1750819193,
+        "narHash": "sha256-XvkupGPZqD54HuKhN/2WhbKjAHeTl1UEnWspzUzRFfA=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "f933070c29f9c1c5457447a51903f27f76ebb519",
+        "rev": "1ba3b9c59b68a4b00156827ad46393127b51b808",
         "type": "github"
       },
       "original": {
@@ -103,11 +103,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1744961264,
-        "narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=",
+        "lastModified": 1749194973,
+        "narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=",
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "8d404a69efe76146368885110f29a2ca3700bee6",
+        "rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
   description = "NixOS cluster configuration management";
 
   inputs = {
-    nixpkgs.url = "github:nixos/nixpkgs/release-24.11";
+    nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
     rust-overlay = {
       url = "github:oxalica/rust-overlay";
       inputs.nixpkgs.follows = "nixpkgs";
@@ -156,7 +156,7 @@
                 bacon
                 nil
                 rustPlatform.bindgenHook
-                nixVersions.nix_2_22
+                # nixVersions.nix_2_22
               ];
               environment.PROTOC = "${pkgs.protobuf}/bin/protoc";
             };
modifiedrust-toolchain.tomldiffbeforeafterboth
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
 [toolchain]
-channel = "1.85.0"
+channel = "1.86.0"
 components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]
modifiedrustfmt.tomldiffbeforeafterboth
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,3 @@
 hard_tabs = true
-imports_granularity = "Crate"
-group_imports = "StdExternalCrate"
+# imports_granularity = "Crate"
+# group_imports = "StdExternalCrate"