From 661e960a19ca472425cdf52a757a08315368ec54 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:26:11 -0700 Subject: [PATCH 01/69] Add build flag to resolve command --- flake.nix | 2 +- src/cli/cmd/convert.rs | 10 +++------- src/cli/cmd/mod.rs | 10 ++++++++++ src/cli/cmd/resolve.rs | 17 +++++++++++++---- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/flake.nix b/flake.nix index f4af3676..79c0b768 100644 --- a/flake.nix +++ b/flake.nix @@ -103,7 +103,7 @@ default = pkgs.mkShell { packages = with pkgs; [ (fenixToolchain system) - cargo-watch + bacon rust-analyzer nixpkgs-fmt diff --git a/src/cli/cmd/convert.rs b/src/cli/cmd/convert.rs index 185a8e03..41c154b3 100644 --- a/src/cli/cmd/convert.rs +++ b/src/cli/cmd/convert.rs @@ -6,7 +6,7 @@ use clap::Parser; use once_cell::sync::Lazy; use tracing::{span, Level}; -use super::CommandExecute; +use super::{nix_command, CommandExecute}; // match {nixos,nixpkgs,release}-YY.MM branches static RELEASE_BRANCH_REGEX: Lazy = Lazy::new(|| { @@ -81,12 +81,8 @@ impl CommandExecute for ConvertSubcommand { println!("{new_flake_contents}"); } else { tokio::fs::write(self.flake_path, new_flake_contents).await?; - tokio::process::Command::new("nix") - .args(["--extra-experimental-features", "nix-command flakes"]) - .arg("flake") - .arg("lock") - .status() - .await?; + + nix_command(&["flake", "lock"]).await?; } Ok(ExitCode::SUCCESS) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 38daee5b..00b08e1f 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -367,6 +367,16 @@ macro_rules! flakehub_url { }}; } +async fn nix_command(args: &[&str]) -> Result<(), FhError> { + tokio::process::Command::new("nix") + .args(["--extra-experimental-features", "nix-command flakes"]) + .args(args) + .status() + .await?; + + Ok(()) +} + #[cfg(test)] mod tests { #[test] diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 3aefd6e0..83173fcd 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -3,7 +3,7 @@ use std::process::ExitCode; use clap::Parser; use serde::{Deserialize, Serialize}; -use super::{print_json, CommandExecute, FlakeHubClient}; +use super::{nix_command, print_json, CommandExecute, FlakeHubClient}; /// Resolves a FlakeHub flake reference into a store path. #[derive(Debug, Parser)] @@ -16,6 +16,10 @@ pub(crate) struct ResolveSubcommand { #[arg(long)] json: bool, + /// Build the resolved path with Nix. + #[arg(short, long, default_value_t = false)] + build: bool, + #[clap(from_global)] api_addr: url::Url, } @@ -38,12 +42,17 @@ impl CommandExecute for ResolveSubcommand { .strip_prefix("https://flakehub.com/f/") .unwrap_or(&self.flake_ref); - let value = FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; + let resolved_path = + FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; + + if self.build { + nix_command(&["build", &resolved_path.store_path]).await?; + } if self.json { - print_json(value)?; + print_json(resolved_path)?; } else { - println!("{}", value.store_path); + println!("{}", resolved_path.store_path); } Ok(ExitCode::SUCCESS) From 83408c5cd3fd6c558b0f14cbd1f297c3c0464b17 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:29:16 -0700 Subject: [PATCH 02/69] Check for Nix command --- src/cli/cmd/init/mod.rs | 2 +- src/cli/cmd/mod.rs | 5 +++++ src/cli/error.rs | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli/cmd/init/mod.rs b/src/cli/cmd/init/mod.rs index fe279976..e2cec63a 100644 --- a/src/cli/cmd/init/mod.rs +++ b/src/cli/cmd/init/mod.rs @@ -280,7 +280,7 @@ impl CommandExecute for InitSubcommand { } } -fn command_exists(cmd: &str) -> bool { +pub(super) fn command_exists(cmd: &str) -> bool { Command::new(cmd).output().is_ok() } diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 00b08e1f..8de613bc 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -23,6 +23,7 @@ use tabled::settings::{ use url::Url; use self::{ + init::command_exists, list::{Flake, Org, Release, Version}, resolve::ResolvedPath, search::SearchResult, @@ -368,6 +369,10 @@ macro_rules! flakehub_url { } async fn nix_command(args: &[&str]) -> Result<(), FhError> { + if !command_exists("nix") { + return Err(FhError::MissingExecutable(String::from("nix"))); + } + tokio::process::Command::new("nix") .args(["--extra-experimental-features", "nix-command flakes"]) .args(args) diff --git a/src/cli/error.rs b/src/cli/error.rs index 78259efd..c8fb5572 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -24,6 +24,9 @@ pub(crate) enum FhError { #[error("malformed flake reference")] MalformedFlakeOutputRef, + #[error("{0} is not installed or not on the PATH")] + MissingExecutable(String), + #[error("missing from flake output reference: {0}")] MissingFromOutputRef(String), From 670f6d369da24d35401a50a9fa1425f04a6bd14c Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:33:56 -0700 Subject: [PATCH 03/69] Set max jobs and print logs --- src/cli/cmd/resolve.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 83173fcd..6af903bc 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -46,7 +46,14 @@ impl CommandExecute for ResolveSubcommand { FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; if self.build { - nix_command(&["build", &resolved_path.store_path]).await?; + nix_command(&[ + "build", + "--max-jobs", + "0", + "--print-build-logs", + &resolved_path.store_path, + ]) + .await?; } if self.json { From 60903763e70714a544b17710302812117ed78f88 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:41:47 -0700 Subject: [PATCH 04/69] Add error wrappers to Nix commands --- src/cli/cmd/convert.rs | 5 ++++- src/cli/cmd/resolve.rs | 4 +++- src/cli/error.rs | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/convert.rs b/src/cli/cmd/convert.rs index 41c154b3..9497480b 100644 --- a/src/cli/cmd/convert.rs +++ b/src/cli/cmd/convert.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use std::process::{ExitCode, Stdio}; use clap::Parser; +use color_eyre::eyre::Context; use once_cell::sync::Lazy; use tracing::{span, Level}; @@ -82,7 +83,9 @@ impl CommandExecute for ConvertSubcommand { } else { tokio::fs::write(self.flake_path, new_flake_contents).await?; - nix_command(&["flake", "lock"]).await?; + nix_command(&["flake", "lock"]) + .await + .wrap_err("failed to create missing lock file entries")?; } Ok(ExitCode::SUCCESS) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 6af903bc..a6d19e8f 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -1,6 +1,7 @@ use std::process::ExitCode; use clap::Parser; +use color_eyre::eyre::Context; use serde::{Deserialize, Serialize}; use super::{nix_command, print_json, CommandExecute, FlakeHubClient}; @@ -53,7 +54,8 @@ impl CommandExecute for ResolveSubcommand { "--print-build-logs", &resolved_path.store_path, ]) - .await?; + .await + .wrap_err("failed to build resolved store path with Nix")?; } if self.json { diff --git a/src/cli/error.rs b/src/cli/error.rs index c8fb5572..90801dc6 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -36,6 +36,9 @@ pub(crate) enum FhError { #[error("template error: {0}")] Render(#[from] handlebars::RenderError), + #[error(transparent)] + Report(#[from] color_eyre::Report), + #[error("template error: {0}")] Template(#[from] Box), From 321e4cb62b28b996868c6f922ce91eca08e9fde5 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:44:14 -0700 Subject: [PATCH 05/69] s/build/fetch --- src/cli/cmd/resolve.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index a6d19e8f..08bc11c8 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -17,9 +17,9 @@ pub(crate) struct ResolveSubcommand { #[arg(long)] json: bool, - /// Build the resolved path with Nix. + /// Fetch the resolved path with Nix. #[arg(short, long, default_value_t = false)] - build: bool, + fetch: bool, #[clap(from_global)] api_addr: url::Url, @@ -46,16 +46,10 @@ impl CommandExecute for ResolveSubcommand { let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; - if self.build { - nix_command(&[ - "build", - "--max-jobs", - "0", - "--print-build-logs", - &resolved_path.store_path, - ]) - .await - .wrap_err("failed to build resolved store path with Nix")?; + if self.fetch { + nix_command(&["build", "--print-build-logs", &resolved_path.store_path]) + .await + .wrap_err("failed to build resolved store path with Nix")?; } if self.json { From 537849922187134b06b5de30bbdfc3af1d9a0473 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:47:19 -0700 Subject: [PATCH 06/69] Make command_exists return a Result --- src/cli/cmd/init/mod.rs | 12 ++++++++---- src/cli/cmd/mod.rs | 4 +--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/cli/cmd/init/mod.rs b/src/cli/cmd/init/mod.rs index e2cec63a..725f6666 100644 --- a/src/cli/cmd/init/mod.rs +++ b/src/cli/cmd/init/mod.rs @@ -240,7 +240,7 @@ impl CommandExecute for InitSubcommand { write(self.output, flake_string)?; if project.has_directory(".git") - && command_exists("git") + && command_exists("git")? && Prompt::bool(&format!( "Would you like to add your new Nix {} to Git?", if use_flake_compat { "files" } else { "file" } @@ -263,7 +263,7 @@ impl CommandExecute for InitSubcommand { write(PathBuf::from(".envrc"), String::from("use flake"))?; if Prompt::bool("You'll need to run `direnv allow` to activate direnv in this project. Would you like to do that now?") { - if command_exists("direnv") { + if command_exists("direnv")? { Command::new("direnv").arg("allow").output()?; } else { println!("It looks like direnv isn't installed."); @@ -280,8 +280,12 @@ impl CommandExecute for InitSubcommand { } } -pub(super) fn command_exists(cmd: &str) -> bool { - Command::new(cmd).output().is_ok() +pub(super) fn command_exists(cmd: &str) -> Result { + if !Command::new(cmd).output().is_ok() { + return Err(FhError::MissingExecutable(String::from(cmd))); + } + + Ok(true) } async fn select_nixpkgs(api_addr: &str) -> Result { diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 8de613bc..0f4ac378 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -369,9 +369,7 @@ macro_rules! flakehub_url { } async fn nix_command(args: &[&str]) -> Result<(), FhError> { - if !command_exists("nix") { - return Err(FhError::MissingExecutable(String::from("nix"))); - } + command_exists("nix")?; tokio::process::Command::new("nix") .args(["--extra-experimental-features", "nix-command flakes"]) From 42151d734d86918571ba0353ee0064d91e2c03c0 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:49:50 -0700 Subject: [PATCH 07/69] Fix clippy warning --- src/cli/cmd/init/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/init/mod.rs b/src/cli/cmd/init/mod.rs index 725f6666..6381ae7f 100644 --- a/src/cli/cmd/init/mod.rs +++ b/src/cli/cmd/init/mod.rs @@ -281,7 +281,7 @@ impl CommandExecute for InitSubcommand { } pub(super) fn command_exists(cmd: &str) -> Result { - if !Command::new(cmd).output().is_ok() { + if Command::new(cmd).output().is_err() { return Err(FhError::MissingExecutable(String::from(cmd))); } From 51091c063b5b12b7c5e7405af541a4f5347b05f1 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 14:50:37 -0700 Subject: [PATCH 08/69] Reinstate max jobs flag --- src/cli/cmd/resolve.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 08bc11c8..d7cbc666 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -47,9 +47,15 @@ impl CommandExecute for ResolveSubcommand { FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; if self.fetch { - nix_command(&["build", "--print-build-logs", &resolved_path.store_path]) - .await - .wrap_err("failed to build resolved store path with Nix")?; + nix_command(&[ + "build", + "--print-build-logs", + "--max-jobs", + "0", + &resolved_path.store_path, + ]) + .await + .wrap_err("failed to build resolved store path with Nix")?; } if self.json { From 1687da6e1e5a18202ea724d15c20ef8c059a3d1f Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 15:17:25 -0700 Subject: [PATCH 09/69] Add debug statements when running Nix commands --- src/cli/cmd/convert.rs | 2 ++ src/cli/cmd/resolve.rs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/cli/cmd/convert.rs b/src/cli/cmd/convert.rs index 9497480b..3aad457e 100644 --- a/src/cli/cmd/convert.rs +++ b/src/cli/cmd/convert.rs @@ -83,6 +83,8 @@ impl CommandExecute for ConvertSubcommand { } else { tokio::fs::write(self.flake_path, new_flake_contents).await?; + tracing::debug!("Running: nix flake lock"); + nix_command(&["flake", "lock"]) .await .wrap_err("failed to create missing lock file entries")?; diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index d7cbc666..9a8e90e0 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -47,6 +47,11 @@ impl CommandExecute for ResolveSubcommand { FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; if self.fetch { + tracing::debug!( + "Running: nix build --print-build-logs --max-jobs 0 {}", + &resolved_path.store_path, + ); + nix_command(&[ "build", "--print-build-logs", From 47fc57eaa9142d8cbdb6674f08aad858d59fde50 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 16 Jul 2024 15:24:20 -0700 Subject: [PATCH 10/69] Bump patch version to 0.1.14 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 271d5594..e4c3c29d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ dependencies = [ [[package]] name = "fh" -version = "0.1.13" +version = "0.1.14" dependencies = [ "async-trait", "axum", diff --git a/Cargo.toml b/Cargo.toml index d658eff3..f56af00b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fh" -version = "0.1.13" +version = "0.1.14" authors = ["Determinate Systems "] edition = "2021" license = "Apache 2.0" From dfdc36ae1d17869b996ab08c3b6efc7fdc10a7ac Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 17 Jul 2024 08:34:02 -0700 Subject: [PATCH 11/69] Add profile flag --- flake.nix | 1 + src/cli/cmd/resolve.rs | 32 +++++++++++++++++++++++++------- src/cli/error.rs | 3 +++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/flake.nix b/flake.nix index 79c0b768..8c405f22 100644 --- a/flake.nix +++ b/flake.nix @@ -104,6 +104,7 @@ packages = with pkgs; [ (fenixToolchain system) bacon + cargo-watch rust-analyzer nixpkgs-fmt diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 9a8e90e0..eddb7702 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -3,6 +3,9 @@ use std::process::ExitCode; use clap::Parser; use color_eyre::eyre::Context; use serde::{Deserialize, Serialize}; +use tokio::fs::metadata; + +use crate::cli::error::FhError; use super::{nix_command, print_json, CommandExecute, FlakeHubClient}; @@ -17,9 +20,9 @@ pub(crate) struct ResolveSubcommand { #[arg(long)] json: bool, - /// Fetch the resolved path with Nix. - #[arg(short, long, default_value_t = false)] - fetch: bool, + /// TODO + #[arg(short, long)] + profile: Option, #[clap(from_global)] api_addr: url::Url, @@ -46,24 +49,39 @@ impl CommandExecute for ResolveSubcommand { let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; - if self.fetch { + if let Some(profile) = self.profile { tracing::debug!( "Running: nix build --print-build-logs --max-jobs 0 {}", &resolved_path.store_path, ); + let profile = if profile.starts_with("/nix/var/nix/profiles") { + profile + } else { + format!("/nix/var/nix/profiles/{profile}") + }; + + // Ensure that the profile exists + if let Ok(path) = metadata(&profile).await { + if !path.is_dir() { + return Err(FhError::MissingProfile(profile).into()); + } + } else { + return Err(FhError::MissingProfile(profile).into()); + } + nix_command(&[ "build", "--print-build-logs", "--max-jobs", "0", + "--profile", + &profile, &resolved_path.store_path, ]) .await .wrap_err("failed to build resolved store path with Nix")?; - } - - if self.json { + } else if self.json { print_json(resolved_path)?; } else { println!("{}", resolved_path.store_path); diff --git a/src/cli/error.rs b/src/cli/error.rs index 90801dc6..af333144 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -30,6 +30,9 @@ pub(crate) enum FhError { #[error("missing from flake output reference: {0}")] MissingFromOutputRef(String), + #[error("profile {0} not found")] + MissingProfile(String), + #[error("the flake has no inputs")] NoInputs, From 2721d7a430aaef7409b04c37ccc0f57cc0f726f0 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 17 Jul 2024 10:59:04 -0700 Subject: [PATCH 12/69] Add missing doc string --- src/cli/cmd/resolve.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index eddb7702..20d403d1 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -17,11 +17,11 @@ pub(crate) struct ResolveSubcommand { flake_ref: String, /// Output the result as JSON displaying the store path plus the original attribute path. - #[arg(long)] + #[arg(long, env = "FH_RESOLVE_JSON_OUTPUT")] json: bool, - /// TODO - #[arg(short, long)] + /// Update a specific Nix profile with the resolved path. + #[arg(short, long, env = "FH_RESOLVE_PROFILE")] profile: Option, #[clap(from_global)] From 0b9443e327a3274dce7cf8b15520122c2619f7fe Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 18 Jul 2024 09:40:52 -0700 Subject: [PATCH 13/69] Create separate apply command --- src/cli/cmd/apply.rs | 75 ++++++++++++++++++++++++++++++++++++++++++ src/cli/cmd/list.rs | 2 +- src/cli/cmd/mod.rs | 27 +++++++++++---- src/cli/cmd/resolve.rs | 57 ++++---------------------------- src/cli/cmd/search.rs | 2 +- src/cli/error.rs | 2 +- src/main.rs | 11 ++++--- 7 files changed, 111 insertions(+), 65 deletions(-) create mode 100644 src/cli/cmd/apply.rs diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs new file mode 100644 index 00000000..ac187d55 --- /dev/null +++ b/src/cli/cmd/apply.rs @@ -0,0 +1,75 @@ +use std::process::ExitCode; + +use clap::Parser; +use color_eyre::eyre::Context; + +use crate::cli::{ + cmd::{nix_command, parse_output_ref}, + error::FhError, +}; + +use super::{CommandExecute, FlakeHubClient}; + +/// Apply a FlakeHub store path to the specified Nix profile. +#[derive(Parser)] +pub(crate) struct ApplySubcommand { + /// Update a specific Nix profile with the resolved path. + #[arg(env = "FH_RESOLVE_PROFILE")] + profile: String, + + /// The FlakeHub output reference to apply to the profile. + /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} + output_ref: String, + + /// Output the result as JSON displaying the store path plus the original attribute path. + #[arg(long, env = "FH_JSON_OUTPUT")] + json: bool, + + #[clap(from_global)] + api_addr: url::Url, +} + +#[async_trait::async_trait] +impl CommandExecute for ApplySubcommand { + async fn execute(self) -> color_eyre::Result { + let output_ref = parse_output_ref(self.output_ref)?; + + let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), output_ref).await?; + + let profile = if self.profile.starts_with("/nix/var/nix/profiles") { + self.profile + } else { + format!("/nix/var/nix/profiles/{}", self.profile) + }; + + if let Ok(path) = tokio::fs::metadata(&profile).await { + if !path.is_dir() { + return Err(FhError::MissingProfile(profile).into()); + } + } else { + return Err(FhError::MissingProfile(profile).into()); + } + + tracing::debug!( + "Running: nix build --print-build-logs --max-jobs 0 --profile {} {}", + &profile, + &resolved_path.store_path, + ); + + nix_command(&[ + "build", + "--print-build-logs", + "--max-jobs", + "0", + "--profile", + &profile, + &resolved_path.store_path, + ]) + .await + .wrap_err("failed to build resolved store path with Nix")?; + + tracing::info!("Successfully applied resolved path to profile {profile}"); + + Ok(ExitCode::SUCCESS) + } +} diff --git a/src/cli/cmd/list.rs b/src/cli/cmd/list.rs index ee8d8a11..8cf91534 100644 --- a/src/cli/cmd/list.rs +++ b/src/cli/cmd/list.rs @@ -27,7 +27,7 @@ pub(crate) struct ListSubcommand { cmd: Subcommands, /// Output results as JSON. - #[arg(long, global = true)] + #[arg(long, global = true, env = "FH_JSON_OUTPUT")] json: bool, #[arg(from_global)] diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 0f4ac378..f7d581f5 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod add; +pub(crate) mod apply; pub(crate) mod completion; pub(crate) mod convert; pub(crate) mod eject; @@ -62,15 +63,16 @@ pub trait CommandExecute { #[derive(clap::Subcommand)] pub(crate) enum FhSubcommands { Add(add::AddSubcommand), + Apply(apply::ApplySubcommand), Completion(completion::CompletionSubcommand), + Convert(convert::ConvertSubcommand), + Eject(eject::EjectSubcommand), Init(init::InitSubcommand), List(list::ListSubcommand), - Search(search::SearchSubcommand), - Convert(convert::ConvertSubcommand), Login(login::LoginSubcommand), - Status(status::StatusSubcommand), - Eject(eject::EjectSubcommand), Resolve(resolve::ResolveSubcommand), + Search(search::SearchSubcommand), + Status(status::StatusSubcommand), } #[derive(Debug, Deserialize)] @@ -153,13 +155,13 @@ impl FlakeHubClient { Ok(res) } - async fn resolve(api_addr: &str, flake_ref: String) -> Result { + async fn resolve(api_addr: &str, output_ref: FlakeOutputRef) -> Result { let FlakeOutputRef { ref org, ref flake, ref version_constraint, ref attr_path, - } = flake_ref.try_into()?; + } = output_ref; let url = flakehub_url!( api_addr, @@ -380,6 +382,19 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { Ok(()) } +fn parse_output_ref(output_ref: String) -> Result { + // Ensures that users can use both forms: + // 1. https://flakehub/f/{org}/{project}/{version_req}#{output} + // 2. {org}/{project}/{version_req}#{output} + let output_ref = String::from( + output_ref + .strip_prefix("https://flakehub.com/f/") + .unwrap_or(&output_ref), + ); + + output_ref.try_into() +} + #[cfg(test)] mod tests { #[test] diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 20d403d1..5953ebe6 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -1,13 +1,9 @@ use std::process::ExitCode; use clap::Parser; -use color_eyre::eyre::Context; use serde::{Deserialize, Serialize}; -use tokio::fs::metadata; -use crate::cli::error::FhError; - -use super::{nix_command, print_json, CommandExecute, FlakeHubClient}; +use super::{parse_output_ref, print_json, CommandExecute, FlakeHubClient}; /// Resolves a FlakeHub flake reference into a store path. #[derive(Debug, Parser)] @@ -17,13 +13,9 @@ pub(crate) struct ResolveSubcommand { flake_ref: String, /// Output the result as JSON displaying the store path plus the original attribute path. - #[arg(long, env = "FH_RESOLVE_JSON_OUTPUT")] + #[arg(long, env = "FH_JSON_OUTPUT")] json: bool, - /// Update a specific Nix profile with the resolved path. - #[arg(short, long, env = "FH_RESOLVE_PROFILE")] - profile: Option, - #[clap(from_global)] api_addr: url::Url, } @@ -33,55 +25,18 @@ pub(crate) struct ResolvedPath { // The original attribute path, i.e. attr_path in {org}/{flake}/{version}#{attr_path} attribute_path: String, // The resolved store path - store_path: String, + pub(crate) store_path: String, } #[async_trait::async_trait] impl CommandExecute for ResolveSubcommand { #[tracing::instrument(skip_all)] async fn execute(self) -> color_eyre::Result { - // Ensures that users can use otherwise-valid flake refs - let flake_ref = self - .flake_ref - .strip_prefix("https://flakehub.com/f/") - .unwrap_or(&self.flake_ref); - - let resolved_path = - FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref.to_string()).await?; - - if let Some(profile) = self.profile { - tracing::debug!( - "Running: nix build --print-build-logs --max-jobs 0 {}", - &resolved_path.store_path, - ); - - let profile = if profile.starts_with("/nix/var/nix/profiles") { - profile - } else { - format!("/nix/var/nix/profiles/{profile}") - }; + let flake_ref = parse_output_ref(self.flake_ref)?; - // Ensure that the profile exists - if let Ok(path) = metadata(&profile).await { - if !path.is_dir() { - return Err(FhError::MissingProfile(profile).into()); - } - } else { - return Err(FhError::MissingProfile(profile).into()); - } + let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref).await?; - nix_command(&[ - "build", - "--print-build-logs", - "--max-jobs", - "0", - "--profile", - &profile, - &resolved_path.store_path, - ]) - .await - .wrap_err("failed to build resolved store path with Nix")?; - } else if self.json { + if self.json { print_json(resolved_path)?; } else { println!("{}", resolved_path.store_path); diff --git a/src/cli/cmd/search.rs b/src/cli/cmd/search.rs index 2b3126e8..9c398db2 100644 --- a/src/cli/cmd/search.rs +++ b/src/cli/cmd/search.rs @@ -20,7 +20,7 @@ pub(crate) struct SearchSubcommand { max_results: usize, /// Output results as JSON. - #[clap(long)] + #[clap(long, env = "FH_JSON_OUTPUT")] json: bool, #[clap(from_global)] diff --git a/src/cli/error.rs b/src/cli/error.rs index af333144..7849d3eb 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -30,7 +30,7 @@ pub(crate) enum FhError { #[error("missing from flake output reference: {0}")] MissingFromOutputRef(String), - #[error("profile {0} not found")] + #[error("Nix profile {0} not found")] MissingProfile(String), #[error("the flake has no inputs")] diff --git a/src/main.rs b/src/main.rs index 04f64274..29261e57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,14 +30,15 @@ async fn main() -> color_eyre::Result { match cli.subcommand { FhSubcommands::Add(add) => add.execute().await, - FhSubcommands::Init(init) => init.execute().await, - FhSubcommands::List(list) => list.execute().await, - FhSubcommands::Search(search) => search.execute().await, + FhSubcommands::Apply(apply) => apply.execute().await, FhSubcommands::Completion(completion) => completion.execute().await, FhSubcommands::Convert(convert) => convert.execute().await, - FhSubcommands::Login(login) => login.execute().await, - FhSubcommands::Status(status) => status.execute().await, FhSubcommands::Eject(eject) => eject.execute().await, + FhSubcommands::Init(init) => init.execute().await, + FhSubcommands::List(list) => list.execute().await, + FhSubcommands::Login(login) => login.execute().await, FhSubcommands::Resolve(resolve) => resolve.execute().await, + FhSubcommands::Search(search) => search.execute().await, + FhSubcommands::Status(status) => status.execute().await, } } From 9505f866ac00cdefceec100da94a582f1c65f699 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 18 Jul 2024 09:58:45 -0700 Subject: [PATCH 14/69] Add some more debug traces --- src/cli/cmd/apply.rs | 17 +++++++++++++++-- src/cli/cmd/mod.rs | 20 ++++++++++++++++---- src/cli/cmd/resolve.rs | 10 ++++++++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index ac187d55..223cdb46 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -34,7 +34,13 @@ impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { let output_ref = parse_output_ref(self.output_ref)?; - let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), output_ref).await?; + let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; + + tracing::debug!( + "Successfully resolved reference {} to path {}", + &output_ref, + &resolved_path.store_path + ); let profile = if self.profile.starts_with("/nix/var/nix/profiles") { self.profile @@ -42,7 +48,11 @@ impl CommandExecute for ApplySubcommand { format!("/nix/var/nix/profiles/{}", self.profile) }; + tracing::debug!("Successfully located Nix profile {profile}"); + if let Ok(path) = tokio::fs::metadata(&profile).await { + tracing::debug!("Profile path {path:?} exists but isn't a directory"); + if !path.is_dir() { return Err(FhError::MissingProfile(profile).into()); } @@ -68,7 +78,10 @@ impl CommandExecute for ApplySubcommand { .await .wrap_err("failed to build resolved store path with Nix")?; - tracing::info!("Successfully applied resolved path to profile {profile}"); + tracing::info!( + "Successfully applied resolved path {} to profile {profile}", + &resolved_path.store_path + ); Ok(ExitCode::SUCCESS) } diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index f7d581f5..46d75cb1 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -10,6 +10,8 @@ pub(crate) mod resolve; pub(crate) mod search; pub(crate) mod status; +use std::fmt::Display; + use color_eyre::eyre::WrapErr; use once_cell::sync::Lazy; use reqwest::{ @@ -155,10 +157,10 @@ impl FlakeHubClient { Ok(res) } - async fn resolve(api_addr: &str, output_ref: FlakeOutputRef) -> Result { + async fn resolve(api_addr: &str, output_ref: &FlakeOutputRef) -> Result { let FlakeOutputRef { ref org, - ref flake, + project: ref flake, ref version_constraint, ref attr_path, } = output_ref; @@ -260,11 +262,21 @@ pub(crate) fn print_json(value: T) -> Result<(), FhError> { // https://api.flakehub.com/f/{org}/{flake}/{version_constraint}/output/{attr_path} struct FlakeOutputRef { org: String, - flake: String, + project: String, version_constraint: String, attr_path: String, } +impl Display for FlakeOutputRef { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}/{}/{}#{}", + self.org, self.project, self.version_constraint, self.attr_path + ) + } +} + impl TryFrom for FlakeOutputRef { type Error = FhError; @@ -302,7 +314,7 @@ impl TryFrom for FlakeOutputRef { Ok(FlakeOutputRef { org: org.to_string(), - flake: flake.to_string(), + project: flake.to_string(), version_constraint: version.to_string(), attr_path: attr_path.to_string(), }) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index 5953ebe6..f414f0fc 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -32,9 +32,15 @@ pub(crate) struct ResolvedPath { impl CommandExecute for ResolveSubcommand { #[tracing::instrument(skip_all)] async fn execute(self) -> color_eyre::Result { - let flake_ref = parse_output_ref(self.flake_ref)?; + let output_ref = parse_output_ref(self.flake_ref)?; - let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), flake_ref).await?; + let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; + + tracing::debug!( + "Successfully resolved reference {} to path {}", + &output_ref, + &resolved_path.store_path + ); if self.json { print_json(resolved_path)?; From c082a05fa4e49496fbab84e8f7a5eb6a6502d0de Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 18 Jul 2024 10:43:32 -0700 Subject: [PATCH 15/69] Fix doc string explanations --- src/cli/cmd/apply.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 223cdb46..dfc51146 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -10,10 +10,10 @@ use crate::cli::{ use super::{CommandExecute, FlakeHubClient}; -/// Apply a FlakeHub store path to the specified Nix profile. +/// Update the specified Nix profile with the path resolved from a flake output reference. #[derive(Parser)] pub(crate) struct ApplySubcommand { - /// Update a specific Nix profile with the resolved path. + /// The Nix profile to which you want to apply the resolved store path. #[arg(env = "FH_RESOLVE_PROFILE")] profile: String, From f65f980f3b013461276bba83fd7cf5276e6e707a Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 18 Jul 2024 12:31:30 -0700 Subject: [PATCH 16/69] Make Nix failure more informative --- src/cli/cmd/mod.rs | 26 ++++++++++++++++++++++---- src/cli/error.rs | 3 +++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 46d75cb1..07f25c64 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -10,7 +10,7 @@ pub(crate) mod resolve; pub(crate) mod search; pub(crate) mod status; -use std::fmt::Display; +use std::{fmt::Display, process::Stdio}; use color_eyre::eyre::WrapErr; use once_cell::sync::Lazy; @@ -385,13 +385,31 @@ macro_rules! flakehub_url { async fn nix_command(args: &[&str]) -> Result<(), FhError> { command_exists("nix")?; - tokio::process::Command::new("nix") + let output = tokio::process::Command::new("nix") .args(["--extra-experimental-features", "nix-command flakes"]) .args(args) - .status() + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() .await?; - Ok(()) + if output.status.success() { + Ok(()) + } else { + let mut s = String::new(); + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + if !stdout.is_empty() { + s.push_str(&stdout.trim()); + } + + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + if !stderr.is_empty() { + s.push_str(&stderr.trim()); + } + + Err(FhError::FailedCommand(s)) + } } fn parse_output_ref(output_ref: String) -> Result { diff --git a/src/cli/error.rs b/src/cli/error.rs index 7849d3eb..1c818c1c 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -1,5 +1,8 @@ #[derive(Debug, thiserror::Error)] pub(crate) enum FhError { + #[error("failed command: {0}")] + FailedCommand(String), + #[error("file error: {0}")] Filesystem(#[from] std::io::Error), From cb70381a21efff8e724223e36aa8509c794df0be Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 18 Jul 2024 13:53:04 -0700 Subject: [PATCH 17/69] Fix clippy issues --- src/cli/cmd/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 07f25c64..c101a345 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -400,12 +400,12 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { let stdout = String::from_utf8_lossy(&output.stdout).to_string(); if !stdout.is_empty() { - s.push_str(&stdout.trim()); + s.push_str(stdout.trim()); } let stderr = String::from_utf8_lossy(&output.stderr).to_string(); if !stderr.is_empty() { - s.push_str(&stderr.trim()); + s.push_str(stderr.trim()); } Err(FhError::FailedCommand(s)) From 22bdf2a202bd8e3b0f4bd86feb22f95265499fcf Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 10:49:42 -0700 Subject: [PATCH 18/69] Check for switch-to-configuration --- src/cli/cmd/apply.rs | 73 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index dfc51146..8fe48852 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -1,4 +1,4 @@ -use std::process::ExitCode; +use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; use clap::Parser; use color_eyre::eyre::Context; @@ -25,6 +25,10 @@ pub(crate) struct ApplySubcommand { #[arg(long, env = "FH_JSON_OUTPUT")] json: bool, + /// TODO + #[arg(short, long, env = "FH_RESOLVE_SWITCH")] + switch: bool, + #[clap(from_global)] api_addr: url::Url, } @@ -42,27 +46,28 @@ impl CommandExecute for ApplySubcommand { &resolved_path.store_path ); - let profile = if self.profile.starts_with("/nix/var/nix/profiles") { + let profile_path = if self.profile.starts_with("/nix/var/nix/profiles") { self.profile } else { format!("/nix/var/nix/profiles/{}", self.profile) }; - tracing::debug!("Successfully located Nix profile {profile}"); - - if let Ok(path) = tokio::fs::metadata(&profile).await { - tracing::debug!("Profile path {path:?} exists but isn't a directory"); + tracing::debug!("Successfully located Nix profile {profile_path}"); + if let Ok(path) = tokio::fs::metadata(&profile_path).await { if !path.is_dir() { - return Err(FhError::MissingProfile(profile).into()); + tracing::debug!( + "Profile path {path:?} exists but isn't a directory; this should never happen" + ); + return Err(FhError::MissingProfile(profile_path).into()); } } else { - return Err(FhError::MissingProfile(profile).into()); + return Err(FhError::MissingProfile(profile_path).into()); } tracing::debug!( "Running: nix build --print-build-logs --max-jobs 0 --profile {} {}", - &profile, + &profile_path, &resolved_path.store_path, ); @@ -72,17 +77,59 @@ impl CommandExecute for ApplySubcommand { "--max-jobs", "0", "--profile", - &profile, + &profile_path, &resolved_path.store_path, ]) .await .wrap_err("failed to build resolved store path with Nix")?; - tracing::info!( - "Successfully applied resolved path {} to profile {profile}", - &resolved_path.store_path + let switch_bin_path = { + let mut path = PathBuf::from(&resolved_path.store_path); + path.push("bin"); + path.push("switch-to-configuration"); + path + }; + + tracing::debug!( + "Checking for switch-to-configuration executable at {}", + &switch_bin_path.display().to_string(), ); + if switch_bin_path.exists() && switch_bin_path.is_file() { + if let Ok(switch_bin_path_metadata) = tokio::fs::metadata(&switch_bin_path).await { + let permissions = switch_bin_path_metadata.permissions(); + if permissions.mode() & 0o111 != 0 { + if self.switch { + tracing::debug!( + "Running {} as an executable", + &switch_bin_path.display().to_string(), + ); + + let output = tokio::process::Command::new(&switch_bin_path) + .output() + .await + .wrap_err("failed to run switch-to-configuration executable")?; + + println!("{}", String::from_utf8_lossy(&output.stdout)); + } else { + tracing::info!( + "Successfully resolved path {} to profile {}", + &resolved_path.store_path, + profile_path + ); + + println!("To update your machine, run:\n\n {profile_path}/bin/switch-to-configuration switch\n"); + println!("Or for more information:\n\n {profile_path}/bin/switch-to-configuration --help"); + } + } + } + } else { + tracing::info!( + "Successfully applied resolved path {} to profile at {profile_path}", + &resolved_path.store_path + ); + } + Ok(ExitCode::SUCCESS) } } From 743a42343d03906bc0ed828cd2f05bd46769fbd9 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 11:21:29 -0700 Subject: [PATCH 19/69] Fix construction of bin path --- src/cli/cmd/apply.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 8fe48852..55a0eed0 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -25,7 +25,7 @@ pub(crate) struct ApplySubcommand { #[arg(long, env = "FH_JSON_OUTPUT")] json: bool, - /// TODO + /// Run the specified profile's bin/switch-to-configuration if present #[arg(short, long, env = "FH_RESOLVE_SWITCH")] switch: bool, @@ -83,8 +83,13 @@ impl CommandExecute for ApplySubcommand { .await .wrap_err("failed to build resolved store path with Nix")?; + tracing::info!( + "Successfully applied resolved path {} to profile at {profile_path}", + &resolved_path.store_path + ); + let switch_bin_path = { - let mut path = PathBuf::from(&resolved_path.store_path); + let mut path = PathBuf::from(&profile_path); path.push("bin"); path.push("switch-to-configuration"); path @@ -96,12 +101,17 @@ impl CommandExecute for ApplySubcommand { ); if switch_bin_path.exists() && switch_bin_path.is_file() { + tracing::debug!( + "Found switch-to-configuration executable at {}", + &switch_bin_path.display().to_string(), + ); + if let Ok(switch_bin_path_metadata) = tokio::fs::metadata(&switch_bin_path).await { let permissions = switch_bin_path_metadata.permissions(); if permissions.mode() & 0o111 != 0 { if self.switch { - tracing::debug!( - "Running {} as an executable", + tracing::info!( + "Switching configuration by running {}", &switch_bin_path.display().to_string(), ); @@ -119,14 +129,19 @@ impl CommandExecute for ApplySubcommand { ); println!("To update your machine, run:\n\n {profile_path}/bin/switch-to-configuration switch\n"); - println!("Or for more information:\n\n {profile_path}/bin/switch-to-configuration --help"); + println!("For more information:\n\n {profile_path}/bin/switch-to-configuration --help"); } + } else { + tracing::debug!( + "switch-to-configuration executable at {} isn't executable; skipping", + &switch_bin_path.display().to_string() + ); } } } else { - tracing::info!( - "Successfully applied resolved path {} to profile at {profile_path}", - &resolved_path.store_path + tracing::debug!( + "No switch-to-configuration executable found at {}", + &switch_bin_path.display().to_string(), ); } From b0641cda1bafc1e826c41300c3081645b78a88d2 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 11:25:18 -0700 Subject: [PATCH 20/69] Inherit stdout and stderr when running Nix commands --- src/cli/cmd/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index c101a345..83edbc55 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -388,8 +388,8 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { let output = tokio::process::Command::new("nix") .args(["--extra-experimental-features", "nix-command flakes"]) .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) .output() .await?; From 31591d264e3786849ecd43568c430c77f46792b8 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 12:14:00 -0700 Subject: [PATCH 21/69] Add verb option --- src/cli/cmd/apply.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 55a0eed0..3d7f1622 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -25,9 +25,10 @@ pub(crate) struct ApplySubcommand { #[arg(long, env = "FH_JSON_OUTPUT")] json: bool, - /// Run the specified profile's bin/switch-to-configuration if present - #[arg(short, long, env = "FH_RESOLVE_SWITCH")] - switch: bool, + /// The command to run with the profile: bin/switch-to-configuration + /// For NixOS, this would be `switch`: bin/switch-to-configuration switch + #[arg(long, env = "FH_RESOLVE_VERB")] + verb: Option, #[clap(from_global)] api_addr: url::Url, @@ -109,16 +110,18 @@ impl CommandExecute for ApplySubcommand { if let Ok(switch_bin_path_metadata) = tokio::fs::metadata(&switch_bin_path).await { let permissions = switch_bin_path_metadata.permissions(); if permissions.mode() & 0o111 != 0 { - if self.switch { + if let Some(verb) = self.verb { tracing::info!( - "Switching configuration by running {}", + "Switching configuration by running {} {}", &switch_bin_path.display().to_string(), + verb, ); let output = tokio::process::Command::new(&switch_bin_path) + .args([&verb]) .output() .await - .wrap_err("failed to run switch-to-configuration executable")?; + .wrap_err("failed to run switch-to-configuration")?; println!("{}", String::from_utf8_lossy(&output.stdout)); } else { From f1da9230486855fccaf8a1a6850fee05b0fe958d Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 12:32:51 -0700 Subject: [PATCH 22/69] Provide enum for apply verb --- src/cli/cmd/apply.rs | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 3d7f1622..81b4cab1 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -1,6 +1,6 @@ -use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; +use std::{fmt::Display, os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; -use clap::Parser; +use clap::{Parser, ValueEnum}; use color_eyre::eyre::Context; use crate::cli::{ @@ -28,12 +28,35 @@ pub(crate) struct ApplySubcommand { /// The command to run with the profile: bin/switch-to-configuration /// For NixOS, this would be `switch`: bin/switch-to-configuration switch #[arg(long, env = "FH_RESOLVE_VERB")] - verb: Option, + verb: Option, #[clap(from_global)] api_addr: url::Url, } +#[derive(Clone, Debug, ValueEnum)] +pub enum Verb { + Switch, + Boot, + Test, + DryActivate, +} + +impl Display for Verb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Switch => "switch", + Self::Boot => "boot", + Self::Test => "test", + Self::DryActivate => "dry-activate", + } + ) + } +} + #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { @@ -114,11 +137,11 @@ impl CommandExecute for ApplySubcommand { tracing::info!( "Switching configuration by running {} {}", &switch_bin_path.display().to_string(), - verb, + verb.to_string(), ); let output = tokio::process::Command::new(&switch_bin_path) - .args([&verb]) + .args([&verb.to_string()]) .output() .await .wrap_err("failed to run switch-to-configuration")?; @@ -131,8 +154,7 @@ impl CommandExecute for ApplySubcommand { profile_path ); - println!("To update your machine, run:\n\n {profile_path}/bin/switch-to-configuration switch\n"); - println!("For more information:\n\n {profile_path}/bin/switch-to-configuration --help"); + println!("For more information on how to update your machine:\n\n {profile_path}/bin/switch-to-configuration --help"); } } else { tracing::debug!( From 8e0139aa0999a3991ded192a5d9fc07f69aa7dc2 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 14:15:40 -0700 Subject: [PATCH 23/69] Pipe nix build output --- src/cli/cmd/apply.rs | 6 ------ src/cli/cmd/mod.rs | 26 ++++++++++---------------- src/cli/error.rs | 4 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 81b4cab1..722e52b9 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -89,12 +89,6 @@ impl CommandExecute for ApplySubcommand { return Err(FhError::MissingProfile(profile_path).into()); } - tracing::debug!( - "Running: nix build --print-build-logs --max-jobs 0 --profile {} {}", - &profile_path, - &resolved_path.store_path, - ); - nix_command(&[ "build", "--print-build-logs", diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 83edbc55..12fc3fbd 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -385,30 +385,24 @@ macro_rules! flakehub_url { async fn nix_command(args: &[&str]) -> Result<(), FhError> { command_exists("nix")?; + tracing::debug!( + "Running: nix build --extra-experimental-features 'nix-command-flakes' {}", + args.join(" ") + ); + let output = tokio::process::Command::new("nix") .args(["--extra-experimental-features", "nix-command flakes"]) .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .output() + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()? + .wait_with_output() .await?; if output.status.success() { Ok(()) } else { - let mut s = String::new(); - - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - if !stdout.is_empty() { - s.push_str(stdout.trim()); - } - - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - if !stderr.is_empty() { - s.push_str(stderr.trim()); - } - - Err(FhError::FailedCommand(s)) + Err(FhError::FailedNixCommand) } } diff --git a/src/cli/error.rs b/src/cli/error.rs index 1c818c1c..f7e8db6e 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -1,7 +1,7 @@ #[derive(Debug, thiserror::Error)] pub(crate) enum FhError { - #[error("failed command: {0}")] - FailedCommand(String), + #[error("Nix command failed")] + FailedNixCommand, #[error("file error: {0}")] Filesystem(#[from] std::io::Error), From cfdb98078516ce6732c9e530f58ee722c1b52b31 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 14:17:18 -0700 Subject: [PATCH 24/69] Remove NixOS-specific doc string --- src/cli/cmd/apply.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 722e52b9..fba37665 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -26,7 +26,6 @@ pub(crate) struct ApplySubcommand { json: bool, /// The command to run with the profile: bin/switch-to-configuration - /// For NixOS, this would be `switch`: bin/switch-to-configuration switch #[arg(long, env = "FH_RESOLVE_VERB")] verb: Option, From e644b42e36abb54b006a20aa9400cf35cf9719b9 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 14:19:01 -0700 Subject: [PATCH 25/69] Use Stdio::inherit instead of ::piped --- src/cli/cmd/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 12fc3fbd..ba1d5b56 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -393,8 +393,8 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { let output = tokio::process::Command::new("nix") .args(["--extra-experimental-features", "nix-command flakes"]) .args(args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) .spawn()? .wait_with_output() .await?; From 2c38ed25ad06f24b2a94f078e7ad9c44550f6105 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 14:25:38 -0700 Subject: [PATCH 26/69] Add more traces --- src/cli/cmd/apply.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index fba37665..24044f44 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -59,6 +59,8 @@ impl Display for Verb { #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { + tracing::info!("Resolving store path for output: {}", self.output_ref); + let output_ref = parse_output_ref(self.output_ref)?; let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; @@ -75,7 +77,7 @@ impl CommandExecute for ApplySubcommand { format!("/nix/var/nix/profiles/{}", self.profile) }; - tracing::debug!("Successfully located Nix profile {profile_path}"); + tracing::debug!("Successfully located Nix profile at {profile_path}"); if let Ok(path) = tokio::fs::metadata(&profile_path).await { if !path.is_dir() { @@ -88,6 +90,11 @@ impl CommandExecute for ApplySubcommand { return Err(FhError::MissingProfile(profile_path).into()); } + tracing::info!( + "Building resolved store path with Nix: {}", + &resolved_path.store_path, + ); + nix_command(&[ "build", "--print-build-logs", From a791e876f67018899ba8e31a5958c1f27db55e0b Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Mon, 22 Jul 2024 15:49:48 -0700 Subject: [PATCH 27/69] Refine the doc strings further --- src/cli/cmd/apply.rs | 15 ++++++++++----- src/cli/cmd/mod.rs | 6 ++++-- src/cli/error.rs | 2 +- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs index 24044f44..609b1958 100644 --- a/src/cli/cmd/apply.rs +++ b/src/cli/cmd/apply.rs @@ -10,7 +10,7 @@ use crate::cli::{ use super::{CommandExecute, FlakeHubClient}; -/// Update the specified Nix profile with the path resolved from a flake output reference. +/// Update the specified Nix profile with the path resolved from a FlakeHub output reference. #[derive(Parser)] pub(crate) struct ApplySubcommand { /// The Nix profile to which you want to apply the resolved store path. @@ -25,14 +25,17 @@ pub(crate) struct ApplySubcommand { #[arg(long, env = "FH_JSON_OUTPUT")] json: bool, - /// The command to run with the profile: bin/switch-to-configuration - #[arg(long, env = "FH_RESOLVE_VERB")] - verb: Option, + /// The command to run from the profile's bin/switch-to-configuration. + /// Takes the form bin/switch-to-configuration . + #[arg(long, env = "FH_RESOLVE_VERB", name = "CMD")] + run: Option, #[clap(from_global)] api_addr: url::Url, } +// For available commands, see +// https://github.com/NixOS/nixpkgs/blob/12100837a815473e96c9c86fdacf6e88d0e6b113/nixos/modules/system/activation/switch-to-configuration.pl#L85-L88 #[derive(Clone, Debug, ValueEnum)] pub enum Verb { Switch, @@ -98,6 +101,8 @@ impl CommandExecute for ApplySubcommand { nix_command(&[ "build", "--print-build-logs", + // `--max-jobs 0` ensures that `nix build` doesn't really *build* anything + // and acts more as a fetch operation "--max-jobs", "0", "--profile", @@ -133,7 +138,7 @@ impl CommandExecute for ApplySubcommand { if let Ok(switch_bin_path_metadata) = tokio::fs::metadata(&switch_bin_path).await { let permissions = switch_bin_path_metadata.permissions(); if permissions.mode() & 0o111 != 0 { - if let Some(verb) = self.verb { + if let Some(verb) = self.run { tracing::info!( "Switching configuration by running {} {}", &switch_bin_path.display().to_string(), diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index ba1d5b56..0d90c54e 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -395,9 +395,11 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { .args(args) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) - .spawn()? + .spawn() + .wrap_err("failed to spawn Nix command")? .wait_with_output() - .await?; + .await + .wrap_err("failed to wait for Nix command output")?; if output.status.success() { Ok(()) diff --git a/src/cli/error.rs b/src/cli/error.rs index f7e8db6e..edc814be 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -1,6 +1,6 @@ #[derive(Debug, thiserror::Error)] pub(crate) enum FhError { - #[error("Nix command failed")] + #[error("Nix command failed; check prior Nix output for details")] FailedNixCommand, #[error("file error: {0}")] From e0a262277c7e48a39fc8ba52e16c53d4c4383908 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 13:18:47 -0700 Subject: [PATCH 28/69] Add nixos subcommand to apply --- src/cli/cmd/apply.rs | 180 ------------------------------------- src/cli/cmd/apply/mod.rs | 161 +++++++++++++++++++++++++++++++++ src/cli/cmd/apply/nixos.rs | 40 +++++++++ 3 files changed, 201 insertions(+), 180 deletions(-) delete mode 100644 src/cli/cmd/apply.rs create mode 100644 src/cli/cmd/apply/mod.rs create mode 100644 src/cli/cmd/apply/nixos.rs diff --git a/src/cli/cmd/apply.rs b/src/cli/cmd/apply.rs deleted file mode 100644 index 609b1958..00000000 --- a/src/cli/cmd/apply.rs +++ /dev/null @@ -1,180 +0,0 @@ -use std::{fmt::Display, os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; - -use clap::{Parser, ValueEnum}; -use color_eyre::eyre::Context; - -use crate::cli::{ - cmd::{nix_command, parse_output_ref}, - error::FhError, -}; - -use super::{CommandExecute, FlakeHubClient}; - -/// Update the specified Nix profile with the path resolved from a FlakeHub output reference. -#[derive(Parser)] -pub(crate) struct ApplySubcommand { - /// The Nix profile to which you want to apply the resolved store path. - #[arg(env = "FH_RESOLVE_PROFILE")] - profile: String, - - /// The FlakeHub output reference to apply to the profile. - /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} - output_ref: String, - - /// Output the result as JSON displaying the store path plus the original attribute path. - #[arg(long, env = "FH_JSON_OUTPUT")] - json: bool, - - /// The command to run from the profile's bin/switch-to-configuration. - /// Takes the form bin/switch-to-configuration . - #[arg(long, env = "FH_RESOLVE_VERB", name = "CMD")] - run: Option, - - #[clap(from_global)] - api_addr: url::Url, -} - -// For available commands, see -// https://github.com/NixOS/nixpkgs/blob/12100837a815473e96c9c86fdacf6e88d0e6b113/nixos/modules/system/activation/switch-to-configuration.pl#L85-L88 -#[derive(Clone, Debug, ValueEnum)] -pub enum Verb { - Switch, - Boot, - Test, - DryActivate, -} - -impl Display for Verb { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Switch => "switch", - Self::Boot => "boot", - Self::Test => "test", - Self::DryActivate => "dry-activate", - } - ) - } -} - -#[async_trait::async_trait] -impl CommandExecute for ApplySubcommand { - async fn execute(self) -> color_eyre::Result { - tracing::info!("Resolving store path for output: {}", self.output_ref); - - let output_ref = parse_output_ref(self.output_ref)?; - - let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; - - tracing::debug!( - "Successfully resolved reference {} to path {}", - &output_ref, - &resolved_path.store_path - ); - - let profile_path = if self.profile.starts_with("/nix/var/nix/profiles") { - self.profile - } else { - format!("/nix/var/nix/profiles/{}", self.profile) - }; - - tracing::debug!("Successfully located Nix profile at {profile_path}"); - - if let Ok(path) = tokio::fs::metadata(&profile_path).await { - if !path.is_dir() { - tracing::debug!( - "Profile path {path:?} exists but isn't a directory; this should never happen" - ); - return Err(FhError::MissingProfile(profile_path).into()); - } - } else { - return Err(FhError::MissingProfile(profile_path).into()); - } - - tracing::info!( - "Building resolved store path with Nix: {}", - &resolved_path.store_path, - ); - - nix_command(&[ - "build", - "--print-build-logs", - // `--max-jobs 0` ensures that `nix build` doesn't really *build* anything - // and acts more as a fetch operation - "--max-jobs", - "0", - "--profile", - &profile_path, - &resolved_path.store_path, - ]) - .await - .wrap_err("failed to build resolved store path with Nix")?; - - tracing::info!( - "Successfully applied resolved path {} to profile at {profile_path}", - &resolved_path.store_path - ); - - let switch_bin_path = { - let mut path = PathBuf::from(&profile_path); - path.push("bin"); - path.push("switch-to-configuration"); - path - }; - - tracing::debug!( - "Checking for switch-to-configuration executable at {}", - &switch_bin_path.display().to_string(), - ); - - if switch_bin_path.exists() && switch_bin_path.is_file() { - tracing::debug!( - "Found switch-to-configuration executable at {}", - &switch_bin_path.display().to_string(), - ); - - if let Ok(switch_bin_path_metadata) = tokio::fs::metadata(&switch_bin_path).await { - let permissions = switch_bin_path_metadata.permissions(); - if permissions.mode() & 0o111 != 0 { - if let Some(verb) = self.run { - tracing::info!( - "Switching configuration by running {} {}", - &switch_bin_path.display().to_string(), - verb.to_string(), - ); - - let output = tokio::process::Command::new(&switch_bin_path) - .args([&verb.to_string()]) - .output() - .await - .wrap_err("failed to run switch-to-configuration")?; - - println!("{}", String::from_utf8_lossy(&output.stdout)); - } else { - tracing::info!( - "Successfully resolved path {} to profile {}", - &resolved_path.store_path, - profile_path - ); - - println!("For more information on how to update your machine:\n\n {profile_path}/bin/switch-to-configuration --help"); - } - } else { - tracing::debug!( - "switch-to-configuration executable at {} isn't executable; skipping", - &switch_bin_path.display().to_string() - ); - } - } - } else { - tracing::debug!( - "No switch-to-configuration executable found at {}", - &switch_bin_path.display().to_string(), - ); - } - - Ok(ExitCode::SUCCESS) - } -} diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs new file mode 100644 index 00000000..316a4a31 --- /dev/null +++ b/src/cli/cmd/apply/mod.rs @@ -0,0 +1,161 @@ +mod nixos; + +use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; + +use clap::{Parser, Subcommand}; +use color_eyre::eyre::Context; + +use crate::cli::{ + cmd::{nix_command, parse_output_ref}, + error::FhError, +}; + +use self::nixos::NixOS; + +use super::{CommandExecute, FlakeHubClient}; + +/// Update the specified Nix profile with the path resolved from a FlakeHub output reference. +#[derive(Parser)] +pub(crate) struct ApplySubcommand { + #[clap(subcommand)] + system: System, + + #[clap(from_global)] + api_addr: url::Url, +} + +#[derive(Subcommand)] +enum System { + /// Apply the resolved store path on a NixOS system + #[clap(name = "nixos")] + NixOS(NixOS), +} + +#[async_trait::async_trait] +impl CommandExecute for ApplySubcommand { + async fn execute(self) -> color_eyre::Result { + let (profile, output_ref) = match &self.system { + System::NixOS(NixOS { output_ref, .. }) => ("system", output_ref), + }; + + tracing::info!("Resolving store path for output: {}", output_ref); + + let output_ref = parse_output_ref(output_ref.to_string())?; + + let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; + + tracing::debug!( + "Successfully resolved reference {} to path {}", + &output_ref, + &resolved_path.store_path + ); + + let profile_path = apply_path_to_profile(profile, &resolved_path.store_path).await?; + + match self.system { + System::NixOS(NixOS { run, .. }) => { + let switch_bin_path = { + let mut path = PathBuf::from(&profile_path); + path.push("bin"); + path.push("switch-to-configuration"); + path + }; + + tracing::debug!( + "Checking for switch-to-configuration executable at {}", + &switch_bin_path.display().to_string(), + ); + + if switch_bin_path.exists() && switch_bin_path.is_file() { + tracing::debug!( + "Found switch-to-configuration executable at {}", + &switch_bin_path.display().to_string(), + ); + + if let Ok(switch_bin_path_metadata) = + tokio::fs::metadata(&switch_bin_path).await + { + let permissions = switch_bin_path_metadata.permissions(); + if permissions.mode() & 0o111 != 0 { + if let Some(verb) = run { + tracing::info!( + "Switching configuration by running {} {}", + &switch_bin_path.display().to_string(), + verb.to_string(), + ); + + let output = tokio::process::Command::new(&switch_bin_path) + .args([&verb.to_string()]) + .output() + .await + .wrap_err("failed to run switch-to-configuration")?; + + println!("{}", String::from_utf8_lossy(&output.stdout)); + } else { + tracing::info!( + "Successfully resolved path {} to profile {}", + &resolved_path.store_path, + profile_path + ); + + println!("For more information on how to update your machine:\n\n {profile_path}/bin/switch-to-configuration --help"); + } + } else { + tracing::debug!( + "switch-to-configuration executable at {} isn't executable; skipping", + &switch_bin_path.display().to_string() + ); + } + } + } else { + tracing::debug!( + "No switch-to-configuration executable found at {}", + &switch_bin_path.display().to_string(), + ); + } + } + } + + Ok(ExitCode::SUCCESS) + } +} + +async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result { + let profile_path = format!("/nix/var/nix/profiles/{profile}"); + + tracing::debug!("Successfully located Nix profile at {profile_path}"); + + if let Ok(path) = tokio::fs::metadata(&profile_path).await { + if !path.is_dir() { + tracing::debug!( + "Profile path {path:?} exists but isn't a directory; this should never happen" + ); + return Err(FhError::MissingProfile(profile_path.to_string())); + } + } else { + return Err(FhError::MissingProfile(profile_path.to_string())); + } + + tracing::info!("Building resolved store path with Nix: {}", store_path); + + nix_command(&[ + "build", + "--print-build-logs", + // `--max-jobs 0` ensures that `nix build` doesn't really *build* anything + // and acts more as a fetch operation + "--max-jobs", + "0", + "--profile", + &profile_path, + store_path, + ]) + .await + .wrap_err("failed to build resolved store path with Nix")?; + + tracing::info!( + "Successfully applied resolved path {} to profile at {profile_path}", + store_path + ); + + Ok(profile_path) +} diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs new file mode 100644 index 00000000..942a5e09 --- /dev/null +++ b/src/cli/cmd/apply/nixos.rs @@ -0,0 +1,40 @@ +use std::fmt::Display; + +use clap::{Parser, ValueEnum}; + +#[derive(Parser)] +pub(super) struct NixOS { + /// The command to run from the profile's bin/switch-to-configuration. + /// Takes the form bin/switch-to-configuration . + #[arg(long, env = "FH_RESOLVE_VERB", name = "CMD")] + pub(super) run: Option, + + /// The FlakeHub output reference to apply to the profile. + /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} + pub(super) output_ref: String, +} + +// For available commands, see +// https://github.com/NixOS/nixpkgs/blob/12100837a815473e96c9c86fdacf6e88d0e6b113/nixos/modules/system/activation/switch-to-configuration.pl#L85-L88 +#[derive(Clone, Debug, ValueEnum)] +pub enum Verb { + Switch, + Boot, + Test, + DryActivate, +} + +impl Display for Verb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Switch => "switch", + Self::Boot => "boot", + Self::Test => "test", + Self::DryActivate => "dry-activate", + } + ) + } +} From 75f8377b1912bf19ae4167b707499a45b2e7ec27 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 15:42:57 -0700 Subject: [PATCH 29/69] Make execution less system specific --- src/cli/cmd/apply/mod.rs | 78 +++++++++++++++++--------------------- src/cli/cmd/apply/nixos.rs | 2 +- src/cli/cmd/mod.rs | 13 +++++++ 3 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 316a4a31..ec1591af 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -5,9 +5,12 @@ use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; use clap::{Parser, Subcommand}; use color_eyre::eyre::Context; -use crate::cli::{ - cmd::{nix_command, parse_output_ref}, - error::FhError, +use crate::{ + cli::{ + cmd::{nix_command, parse_output_ref}, + error::FhError, + }, + path, }; use self::nixos::NixOS; @@ -34,8 +37,10 @@ enum System { #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { - let (profile, output_ref) = match &self.system { - System::NixOS(NixOS { output_ref, .. }) => ("system", output_ref), + let (profile, script, output_ref) = match &self.system { + System::NixOS(NixOS { output_ref, .. }) => { + ("system", "switch-to-configuration", output_ref) + } }; tracing::info!("Resolving store path for output: {}", output_ref); @@ -52,39 +57,34 @@ impl CommandExecute for ApplySubcommand { let profile_path = apply_path_to_profile(profile, &resolved_path.store_path).await?; - match self.system { - System::NixOS(NixOS { run, .. }) => { - let switch_bin_path = { - let mut path = PathBuf::from(&profile_path); - path.push("bin"); - path.push("switch-to-configuration"); - path - }; - - tracing::debug!( - "Checking for switch-to-configuration executable at {}", - &switch_bin_path.display().to_string(), - ); - - if switch_bin_path.exists() && switch_bin_path.is_file() { - tracing::debug!( - "Found switch-to-configuration executable at {}", - &switch_bin_path.display().to_string(), - ); - - if let Ok(switch_bin_path_metadata) = - tokio::fs::metadata(&switch_bin_path).await - { - let permissions = switch_bin_path_metadata.permissions(); - if permissions.mode() & 0o111 != 0 { + let script_path = path!(&profile_path, "bin", script); + + tracing::debug!( + "Checking for {} script at {}", + script, + &script_path.display().to_string(), + ); + + if script_path.exists() && script_path.is_file() { + tracing::debug!( + "Found {} script at {}", + script, + &script_path.display().to_string(), + ); + + if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { + let permissions = script_path_metadata.permissions(); + if permissions.mode() & 0o111 != 0 { + match self.system { + System::NixOS(NixOS { ref run, .. }) => { if let Some(verb) = run { tracing::info!( - "Switching configuration by running {} {}", - &switch_bin_path.display().to_string(), + "Switching NixOS configuration by running {} {}", + &script_path.display().to_string(), verb.to_string(), ); - let output = tokio::process::Command::new(&switch_bin_path) + let output = tokio::process::Command::new(&script_path) .args([&verb.to_string()]) .output() .await @@ -98,20 +98,10 @@ impl CommandExecute for ApplySubcommand { profile_path ); - println!("For more information on how to update your machine:\n\n {profile_path}/bin/switch-to-configuration --help"); + println!("For more information on how to update your machine:\n\n {profile_path}/bin/{} --help", script); } - } else { - tracing::debug!( - "switch-to-configuration executable at {} isn't executable; skipping", - &switch_bin_path.display().to_string() - ); } } - } else { - tracing::debug!( - "No switch-to-configuration executable found at {}", - &switch_bin_path.display().to_string(), - ); } } } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 942a5e09..69b83075 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -6,7 +6,7 @@ use clap::{Parser, ValueEnum}; pub(super) struct NixOS { /// The command to run from the profile's bin/switch-to-configuration. /// Takes the form bin/switch-to-configuration . - #[arg(long, env = "FH_RESOLVE_VERB", name = "CMD")] + #[arg(long, env = "FH_NIXOS_VERB", name = "CMD")] pub(super) run: Option, /// The FlakeHub output reference to apply to the profile. diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 0d90c54e..1b44e0f9 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -382,6 +382,19 @@ macro_rules! flakehub_url { }}; } +#[macro_export] +macro_rules! path { + ($root:expr, $($segment:expr),+ $(,)?) => {{ + let mut path = PathBuf::from($root); + + $( + path.push($segment); + )+ + + path + }}; +} + async fn nix_command(args: &[&str]) -> Result<(), FhError> { command_exists("nix")?; From ed5b1c904080fe4b06d55723258acc793df5e8eb Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 15:46:28 -0700 Subject: [PATCH 30/69] Make switch the default command for NixOS --- src/cli/cmd/apply/nixos.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 69b83075..3211e831 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -6,10 +6,10 @@ use clap::{Parser, ValueEnum}; pub(super) struct NixOS { /// The command to run from the profile's bin/switch-to-configuration. /// Takes the form bin/switch-to-configuration . - #[arg(long, env = "FH_NIXOS_VERB", name = "CMD")] + #[arg(long, env = "FH_NIXOS_VERB", name = "CMD", default_value = "switch")] pub(super) run: Option, - /// The FlakeHub output reference to apply to the profile. + /// The FlakeHub output reference to apply to the system profile. /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} pub(super) output_ref: String, } From adc6fb2e00a3845a7d1b3d5cea987eb7f30a49b9 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 16:08:38 -0700 Subject: [PATCH 31/69] Clarify NixOS parameter docs --- src/cli/cmd/apply/mod.rs | 36 +++++++++++++----------------------- src/cli/cmd/apply/nixos.rs | 13 +++++++++---- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index ec1591af..5d5ac8b2 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -77,29 +77,19 @@ impl CommandExecute for ApplySubcommand { if permissions.mode() & 0o111 != 0 { match self.system { System::NixOS(NixOS { ref run, .. }) => { - if let Some(verb) = run { - tracing::info!( - "Switching NixOS configuration by running {} {}", - &script_path.display().to_string(), - verb.to_string(), - ); - - let output = tokio::process::Command::new(&script_path) - .args([&verb.to_string()]) - .output() - .await - .wrap_err("failed to run switch-to-configuration")?; - - println!("{}", String::from_utf8_lossy(&output.stdout)); - } else { - tracing::info!( - "Successfully resolved path {} to profile {}", - &resolved_path.store_path, - profile_path - ); - - println!("For more information on how to update your machine:\n\n {profile_path}/bin/{} --help", script); - } + tracing::info!( + "{} {}", + &script_path.display().to_string(), + run.to_string(), + ); + + let output = tokio::process::Command::new(&script_path) + .args([&run.to_string()]) + .output() + .await + .wrap_err("failed to run switch-to-configuration")?; + + println!("{}", String::from_utf8_lossy(&output.stdout)); } } } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 3211e831..c34fe95f 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -4,10 +4,15 @@ use clap::{Parser, ValueEnum}; #[derive(Parser)] pub(super) struct NixOS { - /// The command to run from the profile's bin/switch-to-configuration. - /// Takes the form bin/switch-to-configuration . - #[arg(long, env = "FH_NIXOS_VERB", name = "CMD", default_value = "switch")] - pub(super) run: Option, + /// The command to run from the profile's switch-to-configuration script. + /// Takes the form: switch-to-configuration . + #[arg( + long, + env = "FH_APPLY_NIXOS_CMD", + name = "CMD", + default_value = "switch" + )] + pub(super) run: Verb, /// The FlakeHub output reference to apply to the system profile. /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} From e26269e9dd5fd721214dab1c8a16df8436566877 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 16:16:29 -0700 Subject: [PATCH 32/69] Make switch-to-configuration command an optional argument after output ref --- src/cli/cmd/apply/nixos.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index c34fe95f..b2b9cca1 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -4,19 +4,14 @@ use clap::{Parser, ValueEnum}; #[derive(Parser)] pub(super) struct NixOS { - /// The command to run from the profile's switch-to-configuration script. - /// Takes the form: switch-to-configuration . - #[arg( - long, - env = "FH_APPLY_NIXOS_CMD", - name = "CMD", - default_value = "switch" - )] - pub(super) run: Verb, - /// The FlakeHub output reference to apply to the system profile. /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} pub(super) output_ref: String, + + /// The command to run from the profile's switch-to-configuration script. + /// Takes the form: switch-to-configuration . + #[clap(name = "CMD", default_value = "switch")] + pub(super) run: Verb, } // For available commands, see From 4c450dd301d210d2a39aeddb68db8ca98677a888 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 16:19:43 -0700 Subject: [PATCH 33/69] Rename cmd to action --- src/cli/cmd/apply/mod.rs | 7 ++++--- src/cli/cmd/apply/nixos.rs | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 5d5ac8b2..814378cc 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -76,15 +76,16 @@ impl CommandExecute for ApplySubcommand { let permissions = script_path_metadata.permissions(); if permissions.mode() & 0o111 != 0 { match self.system { - System::NixOS(NixOS { ref run, .. }) => { + System::NixOS(NixOS { ref action, .. }) => { tracing::info!( "{} {}", &script_path.display().to_string(), - run.to_string(), + action.to_string(), ); + // switch-to-configuration let output = tokio::process::Command::new(&script_path) - .args([&run.to_string()]) + .args([&action.to_string()]) .output() .await .wrap_err("failed to run switch-to-configuration")?; diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index b2b9cca1..f9ba663f 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -9,9 +9,9 @@ pub(super) struct NixOS { pub(super) output_ref: String, /// The command to run from the profile's switch-to-configuration script. - /// Takes the form: switch-to-configuration . - #[clap(name = "CMD", default_value = "switch")] - pub(super) run: Verb, + /// Takes the form: switch-to-configuration . + #[clap(name = "ACTION", default_value = "switch")] + pub(super) action: Verb, } // For available commands, see From b340f2ab95f862365f9e44d020ab61ada109167b Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 16:46:26 -0700 Subject: [PATCH 34/69] Make path inference a bit more magical --- Cargo.toml | 2 +- src/cli/cmd/apply/mod.rs | 4 +-- src/cli/cmd/apply/nixos.rs | 58 ++++++++++++++++++++++++++++++++++++++ src/cli/error.rs | 3 ++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f56af00b..66877e0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ color-eyre = { version = "0.6.2", default-features = false, features = [ "issue-url", ] } csv = "1.3.0" -gethostname = "0.4.3" +gethostname = { version = "0.4.3", default-features = false } handlebars = "4.4.0" indicatif = { version = "0.17.6", default-features = false } inquire = "0.6.2" diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 814378cc..4d511b5c 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -38,9 +38,7 @@ enum System { impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { let (profile, script, output_ref) = match &self.system { - System::NixOS(NixOS { output_ref, .. }) => { - ("system", "switch-to-configuration", output_ref) - } + System::NixOS(nixos) => ("system", "switch-to-configuration", nixos.output_ref()?), }; tracing::info!("Resolving store path for output: {}", output_ref); diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index f9ba663f..5cfa3310 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -2,6 +2,8 @@ use std::fmt::Display; use clap::{Parser, ValueEnum}; +use crate::cli::error::FhError; + #[derive(Parser)] pub(super) struct NixOS { /// The FlakeHub output reference to apply to the system profile. @@ -14,6 +16,12 @@ pub(super) struct NixOS { pub(super) action: Verb, } +impl NixOS { + pub(super) fn output_ref(&self) -> Result { + parse_output_ref(&self.output_ref) + } +} + // For available commands, see // https://github.com/NixOS/nixpkgs/blob/12100837a815473e96c9c86fdacf6e88d0e6b113/nixos/modules/system/activation/switch-to-configuration.pl#L85-L88 #[derive(Clone, Debug, ValueEnum)] @@ -38,3 +46,53 @@ impl Display for Verb { ) } } + +// This function enables you to provide simplified paths: +// +// fh apply nixos omnicorp/systems/0 +// +// Here, `omnicorp/systems/0`` resolves to `omnicorp/systems/0#nixosConfigurations.$(hostname)`. +// If you need to apply a configuration at a path that doesn't conform to this pattern, you +// can still provide an explicit path. +fn parse_output_ref(path: &str) -> Result { + let hostname = gethostname::gethostname().to_string_lossy().to_string(); + + Ok(match path.split('#').collect::>()[..] { + [_release, _output_path] => path.to_string(), + [release] => format!("{release}#nixosConfigurations.{hostname}"), + _ => return Err(FhError::MalformedNixOSConfigPath(path.to_string())), + }) +} + +#[cfg(test)] +mod tests { + use crate::cli::cmd::apply::nixos::parse_output_ref; + + #[test] + fn test_parse_profile_path() { + let hostname = gethostname::gethostname().to_string_lossy().to_string(); + + let cases: Vec<(&str, String)> = vec![ + ( + "foo/bar/*", + format!("foo/bar/*#nixosConfigurations.{hostname}"), + ), + ( + "foo/bar/0.1.*", + format!("foo/bar/0.1.*#nixosConfigurations.{hostname}"), + ), + ( + "omnicorp/web/0.1.2#nixosConfigurations.auth-server", + "omnicorp/web/0.1.2#nixosConfigurations.auth-server".to_string(), + ), + ( + "omnicorp/web/0.1.2#packages.x86_64-linux.default", + "omnicorp/web/0.1.2#packages.x86_64-linux.default".to_string(), + ), + ]; + + for case in cases { + assert_eq!(parse_output_ref(case.0).unwrap(), case.1); + } + } +} diff --git a/src/cli/error.rs b/src/cli/error.rs index edc814be..00a67cbe 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -24,6 +24,9 @@ pub(crate) enum FhError { #[error("label parsing error: {0}")] LabelParse(String), + #[error("malformed NixOS configuration path: {0}")] + MalformedNixOSConfigPath(String), + #[error("malformed flake reference")] MalformedFlakeOutputRef, From 6214f40f343a61eb0c71d4636853f9c9dd05d31c Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 16:49:39 -0700 Subject: [PATCH 35/69] Update doc string in light of new path inference --- src/cli/cmd/apply/nixos.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 5cfa3310..091f68c8 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -7,7 +7,8 @@ use crate::cli::error::FhError; #[derive(Parser)] pub(super) struct NixOS { /// The FlakeHub output reference to apply to the system profile. - /// References must be of this form: {org}/{flake}/{version_req}#{attr_path} + /// References must take one of two forms: {org}/{flake}/{version_req}#{attr_path} or {org}/{flake}/{version_req}. + /// If the latter, the attribute path defaults to nixosConfigurations.{hostname}. pub(super) output_ref: String, /// The command to run from the profile's switch-to-configuration script. From cf9dbc6efc3f8552cbc88b8043d359ad2576e126 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Tue, 23 Jul 2024 17:17:34 -0700 Subject: [PATCH 36/69] Provide better validation for release references --- src/cli/cmd/apply/mod.rs | 2 +- src/cli/cmd/apply/nixos.rs | 4 ++-- src/cli/cmd/mod.rs | 32 ++++++++++++++++++++++++++++++-- src/cli/cmd/resolve.rs | 2 +- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 4d511b5c..ffadd16f 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -43,7 +43,7 @@ impl CommandExecute for ApplySubcommand { tracing::info!("Resolving store path for output: {}", output_ref); - let output_ref = parse_output_ref(output_ref.to_string())?; + let output_ref = parse_output_ref(&output_ref)?; let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 091f68c8..3ea79fd2 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -2,7 +2,7 @@ use std::fmt::Display; use clap::{Parser, ValueEnum}; -use crate::cli::error::FhError; +use crate::cli::{cmd::parse_release_ref, error::FhError}; #[derive(Parser)] pub(super) struct NixOS { @@ -59,7 +59,7 @@ fn parse_output_ref(path: &str) -> Result { let hostname = gethostname::gethostname().to_string_lossy().to_string(); Ok(match path.split('#').collect::>()[..] { - [_release, _output_path] => path.to_string(), + [_release, _output_path] => parse_release_ref(path)?, [release] => format!("{release}#nixosConfigurations.{hostname}"), _ => return Err(FhError::MalformedNixOSConfigPath(path.to_string())), }) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 1b44e0f9..6059243b 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -421,19 +421,47 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { } } -fn parse_output_ref(output_ref: String) -> Result { +fn parse_output_ref(output_ref: &str) -> Result { // Ensures that users can use both forms: // 1. https://flakehub/f/{org}/{project}/{version_req}#{output} // 2. {org}/{project}/{version_req}#{output} let output_ref = String::from( output_ref .strip_prefix("https://flakehub.com/f/") - .unwrap_or(&output_ref), + .unwrap_or(output_ref), ); output_ref.try_into() } +// Ensure that release refs are of the form {org}/{project}/{version_req} +fn parse_release_ref(flake_ref: &str) -> Result { + match flake_ref.split('/').collect::>()[..] { + [org, project, version_req] => { + validate_segment(org)?; + validate_segment(project)?; + validate_segment(version_req)?; + + Ok(String::from(flake_ref)) + } + _ => Err(FhError::FlakeParse(format!( + "flake ref {flake_ref} invalid; must be of the form {{org}}/{{project}}/{{version_req}}" + ))), + } +} + +// Ensure that orgs, project names, and the like don't contain whitespace +fn validate_segment(s: &str) -> Result<(), FhError> { + if s.chars().any(char::is_whitespace) { + return Err(FhError::FlakeParse(format!( + "path segment {} contains whitespace", + s + ))); + } + + Ok(()) +} + #[cfg(test)] mod tests { #[test] diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index f414f0fc..efb30a23 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -32,7 +32,7 @@ pub(crate) struct ResolvedPath { impl CommandExecute for ResolveSubcommand { #[tracing::instrument(skip_all)] async fn execute(self) -> color_eyre::Result { - let output_ref = parse_output_ref(self.flake_ref)?; + let output_ref = parse_output_ref(&self.flake_ref)?; let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; From 0d43955b1bd2fd3936e8b57bee2434d1d313f1ba Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 24 Jul 2024 09:41:38 -0700 Subject: [PATCH 37/69] Add Home Manager support --- Cargo.lock | 28 +++++++++- Cargo.toml | 1 + flake.nix | 2 + src/cli/cmd/apply/home_manager.rs | 69 +++++++++++++++++++++++ src/cli/cmd/apply/mod.rs | 93 ++++++++++++++++++++++--------- src/cli/cmd/apply/nixos.rs | 5 +- 6 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 src/cli/cmd/apply/home_manager.rs diff --git a/Cargo.lock b/Cargo.lock index e4c3c29d..d1c378fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "tracing-subscriber", "url", "urlencoding", + "whoami", "xdg", ] @@ -1171,7 +1172,7 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.3.5", "smallvec", "windows-targets 0.48.5", ] @@ -1322,6 +1323,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" version = "1.9.5" @@ -2115,6 +2125,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.87" @@ -2191,6 +2207,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "whoami" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall 0.4.1", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 66877e0f..363be3ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ tracing-subscriber = { version = "0.3.17", default-features = false, features = ] } url = { version = "2.4.0", default-features = false, features = ["serde"] } urlencoding = "2.1.3" +whoami = { version = "1.5.1", default-features = false } xdg = "2.5.2" [dev-dependencies] diff --git a/flake.nix b/flake.nix index 8c405f22..4b526da9 100644 --- a/flake.nix +++ b/flake.nix @@ -76,6 +76,7 @@ ] ++ lib.optionals (stdenv.isDarwin) (with darwin.apple_sdk.frameworks; [ Security + SystemConfiguration ]); postInstall = '' @@ -115,6 +116,7 @@ ] ++ lib.optionals (stdenv.isDarwin) ([ libiconv ] ++ (with darwin.apple_sdk.frameworks; [ Security + SystemConfiguration ])); env = { diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs new file mode 100644 index 00000000..026b299d --- /dev/null +++ b/src/cli/cmd/apply/home_manager.rs @@ -0,0 +1,69 @@ +use clap::Parser; + +use crate::cli::{cmd::parse_release_ref, error::FhError}; + +pub(super) const HOME_MANAGER_SCRIPT: &str = "activate"; + +#[derive(Parser)] +pub(super) struct HomeManager { + /// The FlakeHub output reference for the Home Manager configuration. + /// References must take one of two forms: {org}/{flake}/{version_req}#{attr_path} or {org}/{flake}/{version_req}. + /// If the latter, the attribute path defaults to homeConfigurations.{whoami}. + pub(super) output_ref: String, +} + +impl HomeManager { + pub(super) fn output_ref(&self) -> Result { + parse_output_ref(&self.output_ref) + } +} + +// This function enables you to provide simplified paths: +// +// fh apply home-manager omnicorp/home/0 +// +// Here, `omnicorp/systems/0`` resolves to `omnicorp/systems/0#homeConfigurations.$(whoami)`. +// If you need to apply a configuration at a path that doesn't conform to this pattern, you +// can still provide an explicit path. +fn parse_output_ref(path: &str) -> Result { + let username = whoami::username(); + + Ok(match path.split('#').collect::>()[..] { + [_release, _output_path] => parse_release_ref(path)?, + [release] => format!("{release}#homeConfigurations.{username}"), + _ => return Err(FhError::MalformedNixOSConfigPath(path.to_string())), + }) +} + +#[cfg(test)] +mod tests { + use crate::cli::cmd::apply::home_manager::parse_output_ref; + + #[test] + fn test_parse_home_manager_output_ref() { + let username = whoami::username(); + + let cases: Vec<(&str, String)> = vec![ + ( + "foo/bar/*", + format!("foo/bar/*#homeConfigurations.{username}"), + ), + ( + "foo/bar/0.1.*", + format!("foo/bar/0.1.*#homeConfigurations.{username}"), + ), + ( + "omnicorp/web/0.1.2#homeConfigurations.auth-server", + "omnicorp/web/0.1.2#homeConfigurations.auth-server".to_string(), + ), + ( + "omnicorp/web/0.1.2#packages.x86_64-linux.default", + "omnicorp/web/0.1.2#packages.x86_64-linux.default".to_string(), + ), + ]; + + for case in cases { + assert_eq!(parse_output_ref(case.0).unwrap(), case.1); + } + } +} diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index ffadd16f..e4a7fe71 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -1,3 +1,4 @@ +mod home_manager; mod nixos; use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; @@ -13,7 +14,10 @@ use crate::{ path, }; -use self::nixos::NixOS; +use self::{ + home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, + nixos::{NixOS, NIXOS_PROFILE, NIXOS_SCRIPT}, +}; use super::{CommandExecute, FlakeHubClient}; @@ -32,13 +36,17 @@ enum System { /// Apply the resolved store path on a NixOS system #[clap(name = "nixos")] NixOS(NixOS), + + /// Resolve the store path for a Home Manager configuration and run its activate script + HomeManager(HomeManager), } #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { - let (profile, script, output_ref) = match &self.system { - System::NixOS(nixos) => ("system", "switch-to-configuration", nixos.output_ref()?), + let output_ref = match &self.system { + System::NixOS(nixos) => nixos.output_ref()?, + System::HomeManager(home_manager) => home_manager.output_ref()?, }; tracing::info!("Resolving store path for output: {}", output_ref); @@ -53,28 +61,29 @@ impl CommandExecute for ApplySubcommand { &resolved_path.store_path ); - let profile_path = apply_path_to_profile(profile, &resolved_path.store_path).await?; - - let script_path = path!(&profile_path, "bin", script); - - tracing::debug!( - "Checking for {} script at {}", - script, - &script_path.display().to_string(), - ); - - if script_path.exists() && script_path.is_file() { - tracing::debug!( - "Found {} script at {}", - script, - &script_path.display().to_string(), - ); - - if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { - let permissions = script_path_metadata.permissions(); - if permissions.mode() & 0o111 != 0 { - match self.system { - System::NixOS(NixOS { ref action, .. }) => { + match self.system { + System::NixOS(NixOS { action, .. }) => { + let profile_path = + apply_path_to_profile(NIXOS_PROFILE, &resolved_path.store_path).await?; + + let script_path = path!(&profile_path, "bin", NIXOS_SCRIPT); + + tracing::debug!( + "Checking for {} script at {}", + NIXOS_SCRIPT, + &script_path.display().to_string(), + ); + + if script_path.exists() && script_path.is_file() { + tracing::debug!( + "Found {} script at {}", + NIXOS_SCRIPT, + &script_path.display().to_string(), + ); + + if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { + let permissions = script_path_metadata.permissions(); + if permissions.mode() & 0o111 != 0 { tracing::info!( "{} {}", &script_path.display().to_string(), @@ -86,7 +95,39 @@ impl CommandExecute for ApplySubcommand { .args([&action.to_string()]) .output() .await - .wrap_err("failed to run switch-to-configuration")?; + .wrap_err("failed to run switch-to-configuration script")?; + + println!("{}", String::from_utf8_lossy(&output.stdout)); + } + } + } + } + System::HomeManager(_) => { + let script_path = path!(&resolved_path.store_path, HOME_MANAGER_SCRIPT); + + tracing::debug!( + "Checking for {} script at {}", + HOME_MANAGER_SCRIPT, + &script_path.display().to_string(), + ); + + if script_path.exists() && script_path.is_file() { + tracing::debug!( + "Found {} script at {}", + HOME_MANAGER_SCRIPT, + &script_path.display().to_string(), + ); + + if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { + let permissions = script_path_metadata.permissions(); + if permissions.mode() & 0o111 != 0 { + tracing::info!("{}", &script_path.display().to_string()); + + // activate + let output = tokio::process::Command::new(&script_path) + .output() + .await + .wrap_err("failed to run activate script")?; println!("{}", String::from_utf8_lossy(&output.stdout)); } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 3ea79fd2..d28aa739 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -4,6 +4,9 @@ use clap::{Parser, ValueEnum}; use crate::cli::{cmd::parse_release_ref, error::FhError}; +pub(super) const NIXOS_PROFILE: &str = "system"; +pub(super) const NIXOS_SCRIPT: &str = "switch-to-configuration"; + #[derive(Parser)] pub(super) struct NixOS { /// The FlakeHub output reference to apply to the system profile. @@ -70,7 +73,7 @@ mod tests { use crate::cli::cmd::apply::nixos::parse_output_ref; #[test] - fn test_parse_profile_path() { + fn test_parse_nixos_output_ref() { let hostname = gethostname::gethostname().to_string_lossy().to_string(); let cases: Vec<(&str, String)> = vec![ From 7e8a131f258a8e65703b380c9ac1e0ee78fcc0e7 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 24 Jul 2024 12:42:14 -0700 Subject: [PATCH 38/69] Add apply nix-darwin command --- src/cli/cmd/apply/home_manager.rs | 10 +- src/cli/cmd/apply/mod.rs | 147 +++++++++++++++--------------- src/cli/cmd/apply/nix_darwin.rs | 71 +++++++++++++++ src/cli/cmd/apply/nixos.rs | 16 ++-- src/cli/error.rs | 4 +- 5 files changed, 162 insertions(+), 86 deletions(-) create mode 100644 src/cli/cmd/apply/nix_darwin.rs diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index 026b299d..9054134c 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -20,9 +20,9 @@ impl HomeManager { // This function enables you to provide simplified paths: // -// fh apply home-manager omnicorp/home/0 +// fh apply home-manager omnicorp/home/0.1 // -// Here, `omnicorp/systems/0`` resolves to `omnicorp/systems/0#homeConfigurations.$(whoami)`. +// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#homeConfigurations.$(whoami)`. // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. fn parse_output_ref(path: &str) -> Result { @@ -31,7 +31,7 @@ fn parse_output_ref(path: &str) -> Result { Ok(match path.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(path)?, [release] => format!("{release}#homeConfigurations.{username}"), - _ => return Err(FhError::MalformedNixOSConfigPath(path.to_string())), + _ => return Err(FhError::MalformedOutputRef(path.to_string())), }) } @@ -53,8 +53,8 @@ mod tests { format!("foo/bar/0.1.*#homeConfigurations.{username}"), ), ( - "omnicorp/web/0.1.2#homeConfigurations.auth-server", - "omnicorp/web/0.1.2#homeConfigurations.auth-server".to_string(), + "omnicorp/web/0.1.2#homeConfigurations.my-config", + "omnicorp/web/0.1.2#homeConfigurations.my-config".to_string(), ), ( "omnicorp/web/0.1.2#packages.x86_64-linux.default", diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index e4a7fe71..ff05b514 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -1,4 +1,5 @@ mod home_manager; +mod nix_darwin; mod nixos; use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; @@ -16,7 +17,8 @@ use crate::{ use self::{ home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, - nixos::{NixOS, NIXOS_PROFILE, NIXOS_SCRIPT}, + nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_SCRIPT}, + nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}, }; use super::{CommandExecute, FlakeHubClient}; @@ -33,20 +35,24 @@ pub(crate) struct ApplySubcommand { #[derive(Subcommand)] enum System { + /// Resolve the store path for a Home Manager configuration and run its activation script + HomeManager(HomeManager), + + /// Resolve the store path for a nix-darwin configuration and run its activation script + NixDarwin(NixDarwin), + /// Apply the resolved store path on a NixOS system #[clap(name = "nixos")] - NixOS(NixOS), - - /// Resolve the store path for a Home Manager configuration and run its activate script - HomeManager(HomeManager), + NixOs(NixOs), } #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { let output_ref = match &self.system { - System::NixOS(nixos) => nixos.output_ref()?, + System::NixOs(nixos) => nixos.output_ref()?, System::HomeManager(home_manager) => home_manager.output_ref()?, + System::NixDarwin(nix_darwin) => nix_darwin.output_ref()?, }; tracing::info!("Resolving store path for output: {}", output_ref); @@ -62,82 +68,81 @@ impl CommandExecute for ApplySubcommand { ); match self.system { - System::NixOS(NixOS { action, .. }) => { + System::HomeManager(_) => { + // /nix/store/{path}/activate + let script_path = path!(&resolved_path.store_path, HOME_MANAGER_SCRIPT); + run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; + } + System::NixDarwin(_) => { + // /nix/store/{path}/sw/bin/darwin-rebuild + let script_path = path!(&resolved_path.store_path, "sw", "bin", NIX_DARWIN_SCRIPT); + run_script( + script_path, + Some(NIX_DARWIN_ACTION.to_string()), + NIX_DARWIN_SCRIPT, + ) + .await?; + } + System::NixOs(NixOs { action, .. }) => { let profile_path = apply_path_to_profile(NIXOS_PROFILE, &resolved_path.store_path).await?; let script_path = path!(&profile_path, "bin", NIXOS_SCRIPT); - tracing::debug!( - "Checking for {} script at {}", - NIXOS_SCRIPT, - &script_path.display().to_string(), - ); - - if script_path.exists() && script_path.is_file() { - tracing::debug!( - "Found {} script at {}", - NIXOS_SCRIPT, - &script_path.display().to_string(), - ); - - if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { - let permissions = script_path_metadata.permissions(); - if permissions.mode() & 0o111 != 0 { - tracing::info!( - "{} {}", - &script_path.display().to_string(), - action.to_string(), - ); - - // switch-to-configuration - let output = tokio::process::Command::new(&script_path) - .args([&action.to_string()]) - .output() - .await - .wrap_err("failed to run switch-to-configuration script")?; - - println!("{}", String::from_utf8_lossy(&output.stdout)); - } - } - } + run_script(script_path, Some(action.to_string()), NIXOS_SCRIPT).await?; } - System::HomeManager(_) => { - let script_path = path!(&resolved_path.store_path, HOME_MANAGER_SCRIPT); + } - tracing::debug!( - "Checking for {} script at {}", - HOME_MANAGER_SCRIPT, - &script_path.display().to_string(), - ); - - if script_path.exists() && script_path.is_file() { - tracing::debug!( - "Found {} script at {}", - HOME_MANAGER_SCRIPT, - &script_path.display().to_string(), - ); - - if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { - let permissions = script_path_metadata.permissions(); - if permissions.mode() & 0o111 != 0 { - tracing::info!("{}", &script_path.display().to_string()); - - // activate - let output = tokio::process::Command::new(&script_path) - .output() - .await - .wrap_err("failed to run activate script")?; - - println!("{}", String::from_utf8_lossy(&output.stdout)); - } - } + Ok(ExitCode::SUCCESS) + } +} + +async fn run_script( + script_path: PathBuf, + action: Option, + script_name: &str, +) -> Result<(), FhError> { + tracing::debug!( + "Checking for {} script at {}", + script_name, + &script_path.display().to_string(), + ); + + if script_path.exists() && script_path.is_file() { + tracing::debug!( + "Found {} script at {}", + script_name, + &script_path.display().to_string(), + ); + + if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { + let permissions = script_path_metadata.permissions(); + if permissions.mode() & 0o111 != 0 { + if let Some(action) = &action { + tracing::info!("{} {}", &script_path.display().to_string(), action); + } else { + tracing::info!("{}", &script_path.display().to_string()); } + + let output = if let Some(action) = &action { + tokio::process::Command::new(&script_path) + .arg(action) + .output() + .await + .wrap_err(format!("failed to run {script_name} script"))? + } else { + tokio::process::Command::new(&script_path) + .output() + .await + .wrap_err(format!("failed to run {script_name} script"))? + }; + + println!("{}", String::from_utf8_lossy(&output.stdout)); } } - - Ok(ExitCode::SUCCESS) } + + Ok(()) } async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result { diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs new file mode 100644 index 00000000..cc59585e --- /dev/null +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -0,0 +1,71 @@ +use clap::Parser; + +use crate::cli::{cmd::parse_release_ref, error::FhError}; + +pub(super) const NIX_DARWIN_ACTION: &str = "activate"; +pub(super) const NIX_DARWIN_SCRIPT: &str = "darwin-rebuild"; + +#[derive(Parser)] +pub(super) struct NixDarwin { + /// The FlakeHub output reference for the nix-darwin configuration. + /// References must take one of two forms: {org}/{flake}/{version_req}#{attr_path} or {org}/{flake}/{version_req}. + /// If the latter, the attribute path defaults to darwinConfigurations.{devicename}.system, where devicename + /// is the output of scutil --get LocalHostName. + pub(super) output_ref: String, +} + +impl NixDarwin { + pub(super) fn output_ref(&self) -> Result { + parse_output_ref(&self.output_ref) + } +} + +// This function enables you to provide simplified paths: +// +// fh apply nix-darwin omnicorp/home/0.1 +// +// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0#darwinConfigurations.$(devicename).system`. +// If you need to apply a configuration at a path that doesn't conform to this pattern, you +// can still provide an explicit path. +fn parse_output_ref(path: &str) -> Result { + let devicename = whoami::devicename(); + + Ok(match path.split('#').collect::>()[..] { + [_release, _output_path] => parse_release_ref(path)?, + [release] => format!("{release}#darwinConfigurations.{devicename}.system"), + _ => return Err(FhError::MalformedOutputRef(path.to_string())), + }) +} + +#[cfg(test)] +mod tests { + use crate::cli::cmd::apply::nix_darwin::parse_output_ref; + + #[test] + fn test_parse_nixos_output_ref() { + let devicename = whoami::devicename(); + + let cases: Vec<(&str, String)> = vec![ + ( + "foo/bar/*", + format!("foo/bar/*#darwinConfigurations.{devicename}.system"), + ), + ( + "foo/bar/0.1.*", + format!("foo/bar/0.1.*#darwinConfigurations.{devicename}.system"), + ), + ( + "omnicorp/web/0.1.2#darwinConfigurations.my-config", + "omnicorp/web/0.1.2#darwinConfigurations.my-config".to_string(), + ), + ( + "omnicorp/web/0.1.2#packages.x86_64-linux.default", + "omnicorp/web/0.1.2#packages.x86_64-linux.default".to_string(), + ), + ]; + + for case in cases { + assert_eq!(parse_output_ref(case.0).unwrap(), case.1); + } + } +} diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index d28aa739..29cf4f5b 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -8,7 +8,7 @@ pub(super) const NIXOS_PROFILE: &str = "system"; pub(super) const NIXOS_SCRIPT: &str = "switch-to-configuration"; #[derive(Parser)] -pub(super) struct NixOS { +pub(super) struct NixOs { /// The FlakeHub output reference to apply to the system profile. /// References must take one of two forms: {org}/{flake}/{version_req}#{attr_path} or {org}/{flake}/{version_req}. /// If the latter, the attribute path defaults to nixosConfigurations.{hostname}. @@ -17,10 +17,10 @@ pub(super) struct NixOS { /// The command to run from the profile's switch-to-configuration script. /// Takes the form: switch-to-configuration . #[clap(name = "ACTION", default_value = "switch")] - pub(super) action: Verb, + pub(super) action: NixOsAction, } -impl NixOS { +impl NixOs { pub(super) fn output_ref(&self) -> Result { parse_output_ref(&self.output_ref) } @@ -29,14 +29,14 @@ impl NixOS { // For available commands, see // https://github.com/NixOS/nixpkgs/blob/12100837a815473e96c9c86fdacf6e88d0e6b113/nixos/modules/system/activation/switch-to-configuration.pl#L85-L88 #[derive(Clone, Debug, ValueEnum)] -pub enum Verb { +pub enum NixOsAction { Switch, Boot, Test, DryActivate, } -impl Display for Verb { +impl Display for NixOsAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, @@ -53,9 +53,9 @@ impl Display for Verb { // This function enables you to provide simplified paths: // -// fh apply nixos omnicorp/systems/0 +// fh apply nixos omnicorp/systems/0.1 // -// Here, `omnicorp/systems/0`` resolves to `omnicorp/systems/0#nixosConfigurations.$(hostname)`. +// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#nixosConfigurations.$(hostname)`. // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. fn parse_output_ref(path: &str) -> Result { @@ -64,7 +64,7 @@ fn parse_output_ref(path: &str) -> Result { Ok(match path.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(path)?, [release] => format!("{release}#nixosConfigurations.{hostname}"), - _ => return Err(FhError::MalformedNixOSConfigPath(path.to_string())), + _ => return Err(FhError::MalformedOutputRef(path.to_string())), }) } diff --git a/src/cli/error.rs b/src/cli/error.rs index 00a67cbe..a93679d0 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -24,8 +24,8 @@ pub(crate) enum FhError { #[error("label parsing error: {0}")] LabelParse(String), - #[error("malformed NixOS configuration path: {0}")] - MalformedNixOSConfigPath(String), + #[error("malformed output reference: {0}")] + MalformedOutputRef(String), #[error("malformed flake reference")] MalformedFlakeOutputRef, From 5f8bd704eb8f08d2a15dd4983249fe952b315301 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 24 Jul 2024 15:00:05 -0700 Subject: [PATCH 39/69] Fetch closure for nix-darwin first --- src/cli/cmd/apply/mod.rs | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index ff05b514..8a8f4957 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -2,14 +2,18 @@ mod home_manager; mod nix_darwin; mod nixos; -use std::{os::unix::prelude::PermissionsExt, path::PathBuf, process::ExitCode}; +use std::{ + os::unix::prelude::PermissionsExt, + path::PathBuf, + process::{ExitCode, Stdio}, +}; use clap::{Parser, Subcommand}; use color_eyre::eyre::Context; use crate::{ cli::{ - cmd::{nix_command, parse_output_ref}, + cmd::{init::command_exists, nix_command, parse_output_ref}, error::FhError, }, path, @@ -71,11 +75,20 @@ impl CommandExecute for ApplySubcommand { System::HomeManager(_) => { // /nix/store/{path}/activate let script_path = path!(&resolved_path.store_path, HOME_MANAGER_SCRIPT); + run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } System::NixDarwin(_) => { + tracing::info!( + "Using nix-store to fetch closure from path {}", + &resolved_path.store_path + ); + + fetch_closure(&resolved_path.store_path).await?; + // /nix/store/{path}/sw/bin/darwin-rebuild let script_path = path!(&resolved_path.store_path, "sw", "bin", NIX_DARWIN_SCRIPT); + run_script( script_path, Some(NIX_DARWIN_ACTION.to_string()), @@ -145,6 +158,26 @@ async fn run_script( Ok(()) } +async fn fetch_closure(store_path: &str) -> Result<(), FhError> { + command_exists("nix-store")?; + + let output = tokio::process::Command::new("nix-store") + .args(["--realise", store_path]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .wrap_err("failed to spawn Nix command")? + .wait_with_output() + .await + .wrap_err("failed to wait for Nix command output")?; + + if output.status.success() { + Ok(()) + } else { + Err(FhError::FailedNixCommand) + } +} + async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result { let profile_path = format!("/nix/var/nix/profiles/{profile}"); From 10f4670f3594c025f1652736c6a50f19a2f2c9d7 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 24 Jul 2024 15:20:51 -0700 Subject: [PATCH 40/69] Streamline command calling --- src/cli/cmd/apply/mod.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 8a8f4957..5dd082b8 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -84,9 +84,10 @@ impl CommandExecute for ApplySubcommand { &resolved_path.store_path ); + // nix-store -r {path} fetch_closure(&resolved_path.store_path).await?; - // /nix/store/{path}/sw/bin/darwin-rebuild + // {path}/sw/bin/darwin-rebuild let script_path = path!(&resolved_path.store_path, "sw", "bin", NIX_DARWIN_SCRIPT); run_script( @@ -137,18 +138,20 @@ async fn run_script( tracing::info!("{}", &script_path.display().to_string()); } - let output = if let Some(action) = &action { - tokio::process::Command::new(&script_path) - .arg(action) - .output() - .await - .wrap_err(format!("failed to run {script_name} script"))? - } else { - tokio::process::Command::new(&script_path) - .output() - .await - .wrap_err(format!("failed to run {script_name} script"))? - }; + let mut cmd = tokio::process::Command::new(&script_path); + + if let Some(action) = action { + cmd.arg(action); + } + + let output = cmd + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .wrap_err("failed to spawn Nix command")? + .wait_with_output() + .await + .wrap_err(format!("failed to run {script_name} script"))?; println!("{}", String::from_utf8_lossy(&output.stdout)); } From cb530e79a0f974903f61c5b5ca8ede79c1155a43 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Wed, 24 Jul 2024 15:25:00 -0700 Subject: [PATCH 41/69] Clarify variable names --- src/cli/cmd/apply/home_manager.rs | 8 ++++---- src/cli/cmd/apply/mod.rs | 4 ++-- src/cli/cmd/apply/nix_darwin.rs | 8 ++++---- src/cli/cmd/apply/nixos.rs | 8 ++++---- src/cli/cmd/mod.rs | 2 +- src/cli/cmd/resolve.rs | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index 9054134c..bddf3f26 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -25,13 +25,13 @@ impl HomeManager { // Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#homeConfigurations.$(whoami)`. // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. -fn parse_output_ref(path: &str) -> Result { +fn parse_output_ref(output_ref: &str) -> Result { let username = whoami::username(); - Ok(match path.split('#').collect::>()[..] { - [_release, _output_path] => parse_release_ref(path)?, + Ok(match output_ref.split('#').collect::>()[..] { + [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!("{release}#homeConfigurations.{username}"), - _ => return Err(FhError::MalformedOutputRef(path.to_string())), + _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) } diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 5dd082b8..7935b3cf 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -13,7 +13,7 @@ use color_eyre::eyre::Context; use crate::{ cli::{ - cmd::{init::command_exists, nix_command, parse_output_ref}, + cmd::{init::command_exists, nix_command, parse_flake_output_ref}, error::FhError, }, path, @@ -61,7 +61,7 @@ impl CommandExecute for ApplySubcommand { tracing::info!("Resolving store path for output: {}", output_ref); - let output_ref = parse_output_ref(&output_ref)?; + let output_ref = parse_flake_output_ref(&output_ref)?; let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index cc59585e..ba329383 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -27,13 +27,13 @@ impl NixDarwin { // Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0#darwinConfigurations.$(devicename).system`. // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. -fn parse_output_ref(path: &str) -> Result { +fn parse_output_ref(output_ref: &str) -> Result { let devicename = whoami::devicename(); - Ok(match path.split('#').collect::>()[..] { - [_release, _output_path] => parse_release_ref(path)?, + Ok(match output_ref.split('#').collect::>()[..] { + [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!("{release}#darwinConfigurations.{devicename}.system"), - _ => return Err(FhError::MalformedOutputRef(path.to_string())), + _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 29cf4f5b..e60b1a31 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -58,13 +58,13 @@ impl Display for NixOsAction { // Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#nixosConfigurations.$(hostname)`. // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. -fn parse_output_ref(path: &str) -> Result { +fn parse_output_ref(output_ref: &str) -> Result { let hostname = gethostname::gethostname().to_string_lossy().to_string(); - Ok(match path.split('#').collect::>()[..] { - [_release, _output_path] => parse_release_ref(path)?, + Ok(match output_ref.split('#').collect::>()[..] { + [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!("{release}#nixosConfigurations.{hostname}"), - _ => return Err(FhError::MalformedOutputRef(path.to_string())), + _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) } diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 6059243b..a8b7c8fe 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -421,7 +421,7 @@ async fn nix_command(args: &[&str]) -> Result<(), FhError> { } } -fn parse_output_ref(output_ref: &str) -> Result { +fn parse_flake_output_ref(output_ref: &str) -> Result { // Ensures that users can use both forms: // 1. https://flakehub/f/{org}/{project}/{version_req}#{output} // 2. {org}/{project}/{version_req}#{output} diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index efb30a23..caf8d9f8 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -3,7 +3,7 @@ use std::process::ExitCode; use clap::Parser; use serde::{Deserialize, Serialize}; -use super::{parse_output_ref, print_json, CommandExecute, FlakeHubClient}; +use super::{parse_flake_output_ref, print_json, CommandExecute, FlakeHubClient}; /// Resolves a FlakeHub flake reference into a store path. #[derive(Debug, Parser)] @@ -32,7 +32,7 @@ pub(crate) struct ResolvedPath { impl CommandExecute for ResolveSubcommand { #[tracing::instrument(skip_all)] async fn execute(self) -> color_eyre::Result { - let output_ref = parse_output_ref(&self.flake_ref)?; + let output_ref = parse_flake_output_ref(&self.flake_ref)?; let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; From 8a33c7f60da5686116162468dfda606b5c9e013a Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 11:12:08 -0700 Subject: [PATCH 42/69] Remove check for existing profile --- src/cli/cmd/apply/mod.rs | 55 ++++++--------------------------- src/cli/cmd/apply/nix_darwin.rs | 1 + src/cli/error.rs | 3 -- 3 files changed, 11 insertions(+), 48 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 7935b3cf..5c566c9e 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -13,7 +13,7 @@ use color_eyre::eyre::Context; use crate::{ cli::{ - cmd::{init::command_exists, nix_command, parse_flake_output_ref}, + cmd::{nix_command, parse_flake_output_ref}, error::FhError, }, path, @@ -21,7 +21,7 @@ use crate::{ use self::{ home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, - nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_SCRIPT}, + nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}, nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}, }; @@ -79,16 +79,11 @@ impl CommandExecute for ApplySubcommand { run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } System::NixDarwin(_) => { - tracing::info!( - "Using nix-store to fetch closure from path {}", - &resolved_path.store_path - ); - - // nix-store -r {path} - fetch_closure(&resolved_path.store_path).await?; + let profile_path = + apply_path_to_profile(NIX_DARWIN_PROFILE, &resolved_path.store_path).await?; // {path}/sw/bin/darwin-rebuild - let script_path = path!(&resolved_path.store_path, "sw", "bin", NIX_DARWIN_SCRIPT); + let script_path = path!(&profile_path, "sw", "bin", NIX_DARWIN_SCRIPT); run_script( script_path, @@ -139,7 +134,6 @@ async fn run_script( } let mut cmd = tokio::process::Command::new(&script_path); - if let Some(action) = action { cmd.arg(action); } @@ -161,43 +155,14 @@ async fn run_script( Ok(()) } -async fn fetch_closure(store_path: &str) -> Result<(), FhError> { - command_exists("nix-store")?; - - let output = tokio::process::Command::new("nix-store") - .args(["--realise", store_path]) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .wrap_err("failed to spawn Nix command")? - .wait_with_output() - .await - .wrap_err("failed to wait for Nix command output")?; - - if output.status.success() { - Ok(()) - } else { - Err(FhError::FailedNixCommand) - } -} - async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result { let profile_path = format!("/nix/var/nix/profiles/{profile}"); - tracing::debug!("Successfully located Nix profile at {profile_path}"); - - if let Ok(path) = tokio::fs::metadata(&profile_path).await { - if !path.is_dir() { - tracing::debug!( - "Profile path {path:?} exists but isn't a directory; this should never happen" - ); - return Err(FhError::MissingProfile(profile_path.to_string())); - } - } else { - return Err(FhError::MissingProfile(profile_path.to_string())); - } - - tracing::info!("Building resolved store path with Nix: {}", store_path); + tracing::info!( + "Applying resolved store path {} to profile at {}", + store_path, + profile_path + ); nix_command(&[ "build", diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index ba329383..f587929a 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -4,6 +4,7 @@ use crate::cli::{cmd::parse_release_ref, error::FhError}; pub(super) const NIX_DARWIN_ACTION: &str = "activate"; pub(super) const NIX_DARWIN_SCRIPT: &str = "darwin-rebuild"; +pub(super) const NIX_DARWIN_PROFILE: &str = "system"; #[derive(Parser)] pub(super) struct NixDarwin { diff --git a/src/cli/error.rs b/src/cli/error.rs index a93679d0..cbcffdc9 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -36,9 +36,6 @@ pub(crate) enum FhError { #[error("missing from flake output reference: {0}")] MissingFromOutputRef(String), - #[error("Nix profile {0} not found")] - MissingProfile(String), - #[error("the flake has no inputs")] NoInputs, From b094865ce8f56e1de4283992a55d65a05d6129f1 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 11:15:36 -0700 Subject: [PATCH 43/69] Make apply commands available only on specific systems --- src/cli/cmd/apply/mod.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 5c566c9e..dc07080e 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -1,5 +1,7 @@ mod home_manager; +#[cfg(target_os = "macos")] mod nix_darwin; +#[cfg(target_os = "linux")] mod nixos; use std::{ @@ -22,9 +24,11 @@ use crate::{ use self::{ home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}, - nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}, }; +#[cfg(target_os = "linux")] +use self::nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}; + use super::{CommandExecute, FlakeHubClient}; /// Update the specified Nix profile with the path resolved from a FlakeHub output reference. @@ -42,9 +46,11 @@ enum System { /// Resolve the store path for a Home Manager configuration and run its activation script HomeManager(HomeManager), + #[cfg(target_os = "macos")] /// Resolve the store path for a nix-darwin configuration and run its activation script NixDarwin(NixDarwin), + #[cfg(target_os = "linux")] /// Apply the resolved store path on a NixOS system #[clap(name = "nixos")] NixOs(NixOs), @@ -54,8 +60,10 @@ enum System { impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { let output_ref = match &self.system { - System::NixOs(nixos) => nixos.output_ref()?, System::HomeManager(home_manager) => home_manager.output_ref()?, + #[cfg(target_os = "linux")] + System::NixOs(nixos) => nixos.output_ref()?, + #[cfg(target_os = "macos")] System::NixDarwin(nix_darwin) => nix_darwin.output_ref()?, }; @@ -78,6 +86,7 @@ impl CommandExecute for ApplySubcommand { run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } + #[cfg(target_os = "macos")] System::NixDarwin(_) => { let profile_path = apply_path_to_profile(NIX_DARWIN_PROFILE, &resolved_path.store_path).await?; @@ -92,6 +101,7 @@ impl CommandExecute for ApplySubcommand { ) .await?; } + #[cfg(target_os = "linux")] System::NixOs(NixOs { action, .. }) => { let profile_path = apply_path_to_profile(NIXOS_PROFILE, &resolved_path.store_path).await?; From c93b8516e7a7e7095cfaa0ce0e03118a9d80bdff Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 12:30:09 -0700 Subject: [PATCH 44/69] Run nix build command using sudo if not root --- Cargo.lock | 23 ++++++++++++++-- Cargo.toml | 1 + src/cli/cmd/apply/mod.rs | 25 ++++++++++-------- src/cli/cmd/convert.rs | 2 +- src/cli/cmd/mod.rs | 57 +++++++++++++++++++++++++++++----------- 5 files changed, 79 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1c378fe..0cac8d15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.31" @@ -595,6 +601,7 @@ dependencies = [ "handlebars", "indicatif", "inquire", + "nix", "nix-config-parser", "nixel", "once_cell", @@ -957,9 +964,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "lock_api" @@ -1040,6 +1047,18 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nix-config-parser" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 363be3ea..78467ae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ gethostname = { version = "0.4.3", default-features = false } handlebars = "4.4.0" indicatif = { version = "0.17.6", default-features = false } inquire = "0.6.2" +nix = { version = "0.29.0", default-features = false, features = ["user"] } nix-config-parser = "0.2.0" nixel = "5.2.0" once_cell = "1.18.0" diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index dc07080e..6753f396 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -174,17 +174,20 @@ async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result Result<(), FhError> { +async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhError> { command_exists("nix")?; - tracing::debug!( - "Running: nix build --extra-experimental-features 'nix-command-flakes' {}", - args.join(" ") - ); + let is_root_user = nix::unistd::getuid().is_root(); + let use_sudo = sudo_if_necessary && !is_root_user; + + if use_sudo { + tracing::warn!( + "Current user is {} rather than root; running Nix command using sudo", + whoami::username() + ); + tracing::debug!( + "Running: sudo nix --extra-experimental-features 'nix-command-flakes' {}", + args.join(" ") + ); + } else { + tracing::debug!( + "Running: nix --extra-experimental-features 'nix-command-flakes' {}", + args.join(" ") + ); + } - let output = tokio::process::Command::new("nix") - .args(["--extra-experimental-features", "nix-command flakes"]) - .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .wrap_err("failed to spawn Nix command")? - .wait_with_output() - .await - .wrap_err("failed to wait for Nix command output")?; + let output = if use_sudo { + tokio::process::Command::new("sudo") + .args(["nix", "--extra-experimental-features", "nix-command flakes"]) + .args(args) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .wrap_err("failed to spawn Nix command")? + .wait_with_output() + .await + .wrap_err("failed to wait for Nix command output")? + } else { + tokio::process::Command::new("nix") + .args(["--extra-experimental-features", "nix-command flakes"]) + .args(args) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .wrap_err("failed to spawn Nix command")? + .wait_with_output() + .await + .wrap_err("failed to wait for Nix command output")? + }; if output.status.success() { Ok(()) From 5c5073d35c0c6bd75015a02bb8d3a125c71d754f Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 12:56:05 -0700 Subject: [PATCH 45/69] Fix Linux Clippy warning --- src/cli/cmd/apply/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 6753f396..d4acaac4 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -21,14 +21,14 @@ use crate::{ path, }; -use self::{ - home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, - nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}, -}; +use self::home_manager::{HomeManager, HOME_MANAGER_SCRIPT}; #[cfg(target_os = "linux")] use self::nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}; +#[cfg(target_os = "macos")] +use self::nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}; + use super::{CommandExecute, FlakeHubClient}; /// Update the specified Nix profile with the path resolved from a FlakeHub output reference. From 8dd7d2af7072e7b86012c7ae1880c5faab0389ff Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 14:20:24 -0700 Subject: [PATCH 46/69] Improve output ref parsing --- src/cli/cmd/apply/home_manager.rs | 5 ++++- src/cli/cmd/apply/nix_darwin.rs | 5 ++++- src/cli/cmd/apply/nixos.rs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index bddf3f26..0ad85961 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -30,7 +30,10 @@ fn parse_output_ref(output_ref: &str) -> Result { Ok(match output_ref.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(output_ref)?, - [release] => format!("{release}#homeConfigurations.{username}"), + [release] => format!( + "{}#homeConfigurations.{username}", + parse_release_ref(release)? + ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) } diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index f587929a..2d65244b 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -33,7 +33,10 @@ fn parse_output_ref(output_ref: &str) -> Result { Ok(match output_ref.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(output_ref)?, - [release] => format!("{release}#darwinConfigurations.{devicename}.system"), + [release] => format!( + "{}#darwinConfigurations.{devicename}.system", + parse_release_ref(release)? + ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index e60b1a31..189e4d72 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -63,7 +63,10 @@ fn parse_output_ref(output_ref: &str) -> Result { Ok(match output_ref.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(output_ref)?, - [release] => format!("{release}#nixosConfigurations.{hostname}"), + [release] => format!( + "{}#nixosConfigurations.{hostname}", + parse_release_ref(release)? + ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) } From b8996984225e74d6be023430535a58b8f42868aa Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 14:28:52 -0700 Subject: [PATCH 47/69] Add Linux vs macOS runners for Rust checks --- .github/workflows/flakehub-cache.yml | 2 +- .github/workflows/flakehub.yml | 2 +- .github/workflows/nix.yml | 2 +- .github/workflows/release-branches.yml | 2 +- .github/workflows/release-prs.yml | 2 +- .github/workflows/release-tags.yml | 2 +- .github/workflows/rust.yml | 26 ++++++++++++++++++++++++-- 7 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/flakehub-cache.yml b/.github/workflows/flakehub-cache.yml index 363fb8e8..c7545b25 100644 --- a/.github/workflows/flakehub-cache.yml +++ b/.github/workflows/flakehub-cache.yml @@ -20,7 +20,7 @@ jobs: - nix-system: "x86_64-linux" runner: "ubuntu-22.04" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main with: diff --git a/.github/workflows/flakehub.yml b/.github/workflows/flakehub.yml index 7976cf20..783c4770 100644 --- a/.github/workflows/flakehub.yml +++ b/.github/workflows/flakehub.yml @@ -18,7 +18,7 @@ jobs: id-token: "write" contents: "read" steps: - - uses: "actions/checkout@v3" + - uses: "actions/checkout@v4" with: ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" - uses: "DeterminateSystems/nix-installer-action@main" diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 6d0f0ed2..c7b13089 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -9,7 +9,7 @@ jobs: flake-check: runs-on: UbuntuLatest32Cores128G steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: DeterminateSystems/flake-checker-action@main with: diff --git a/.github/workflows/release-branches.yml b/.github/workflows/release-branches.yml index b8a839fe..28fdec9a 100644 --- a/.github/workflows/release-branches.yml +++ b/.github/workflows/release-branches.yml @@ -21,7 +21,7 @@ jobs: id-token: write # In order to request a JWT for AWS auth steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 with: diff --git a/.github/workflows/release-prs.yml b/.github/workflows/release-prs.yml index 03bfbd69..31083664 100644 --- a/.github/workflows/release-prs.yml +++ b/.github/workflows/release-prs.yml @@ -30,7 +30,7 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create the artifacts directory run: rm -rf ./artifacts && mkdir ./artifacts diff --git a/.github/workflows/release-tags.yml b/.github/workflows/release-tags.yml index 2e510044..cc326f45 100644 --- a/.github/workflows/release-tags.yml +++ b/.github/workflows/release-tags.yml @@ -19,7 +19,7 @@ jobs: id-token: write # In order to request a JWT for AWS auth steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create the artifacts directory run: rm -rf ./artifacts && mkdir ./artifacts diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f03e669d..9ef76cdf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,10 +6,10 @@ on: branches: [main] jobs: - rust-fmt-and-clippy: + rust-fmt: runs-on: UbuntuLatest32Cores128G steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@v4 - uses: DeterminateSystems/magic-nix-cache-action@main @@ -17,5 +17,27 @@ jobs: - name: Rust formatting run: nix develop --command cargo fmt --check + rust-clippy-and-test: + # We need to run the checks on Linux and macOS because there are system-specific + # versions of fh (e.g. `fh apply nix-darwin` is only on macOS while `fh apply + # nixos` is only on Linux) + strategy: + matrix: + runners: + # Linux + - UbuntuLatest32Cores128G + # macOS + - macos-latest-xlarge + + runs-on: ${{ matrix.runners }} + steps: + - uses: actions/checkout@v4 + + - uses: DeterminateSystems/nix-installer-action@v4 + - uses: DeterminateSystems/magic-nix-cache-action@main + - name: Clippy run: nix develop --command cargo clippy --all-targets --all-features -- -Dwarnings + + - name: Test + run: cargo test --all-features From 5b5c88075f40ea994f85d032e4e0bd56d1c321b0 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 14:33:21 -0700 Subject: [PATCH 48/69] Run cargo test in Nix dev shell --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9ef76cdf..e27e71e6 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,4 +40,4 @@ jobs: run: nix develop --command cargo clippy --all-targets --all-features -- -Dwarnings - name: Test - run: cargo test --all-features + run: nix develop --command cargo test --all-features From d4d6e3adb06752885c9a7be311e6ac4fbbfe2cc5 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 15:27:44 -0700 Subject: [PATCH 49/69] Add README docs --- README.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/README.md b/README.md index 3b56c5aa..3c801130 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,72 @@ nix build \ /nix/var/nix/profiles/system/bin/switch-to-configuration switch ``` +### Apply configurations to the current system + +The `fh apply` command enables you to apply a configuration for one of the following systems to the current host: + +- [NixOS](#nixos) +- [Home Manager](#home-manager) +- [nix-darwin](#nix-darwin) + +For all three systems, you only need to supply a flake output reference for the configuration and `fh` does the rest. + +#### NixOS + +On a [NixOS] system, you can use `fh apply nixos` to apply a configuration from an output path: + +```shell +fh apply nixos "my-org/system-configs/0.1#nixosConfigurations.staging-box" +``` + +If you don't specify a flake output path, `fh apply nixos` defaults to `nixosConfigurations.$(hostname)`. +These two commands are thus equivalent: + +```shell +fh apply nixos "my-org/system-configs/0.1#nixosConfigurations.$(hostname)" +fh apply nixos "my-org/system-configs/0.1" +``` + +`fh apply nixos` first resolves the supplied output reference to a store path, builds the `switch-to-configuration` script for that path, and then runs `switch-to-configuration switch` by default. +You can also supply a different command from `switch` (`boot`, `test`, or `dry-activate`). +Here's an example: + +```shell +fh apply nixos "my-org/system-configs/0.1" boot +``` + +#### Home Manager + +If you're on a system that uses [Home Manager][hm], you can use `fh apply home-manager` to apply a configuration from an output path: + +```shell +fh apply home-manager "my-org/home-configs/0.1#homeConfigurations.standard-home-config" +``` + +If you don't specify a flake output path, `fh apply home-manager` defaults to `homeConfigurations.$(whoami)`. +These two commands are thus equivalent: + +```shell +fh apply home-manager "my-org/home-configs/0.1#homeConfigurations.$(whoami)" +fh apply home-manager "my-org/home-configs/0.1" +``` + +#### nix-darwin + +If you're on a macOS system that uses [nix-darwin], you can use `fh apply nix-darwin` to apply a configuration from an output path: + +```shell +fh apply nix-darwin "my-org/macos-configs/0.1#darwinConfigurations.justme-aarch64-darwin" +``` + +If you don't specify a flake output path, `fh apply nix-darwin` defaults to `darwinConfigurations.${devicename}.system`, where `devicename` is the output of `scutil --get LocalHostName`. +These two commands are thus equivalent: + +```shell +fh apply nix-darwin "my-org/macos-configs/0.1#darwinConfigurations.$(scutil --get LocalHostName)" +fh apply nix-darwin "my-org/macos-configs/0.1" +``` + ### Searching published flakes You can search publicly listed flakes using the `fh search` command and passing in a search query. @@ -336,9 +402,11 @@ For support, email support@flakehub.com or [join our Discord](https://discord.gg [flakehub-push-params]: https://github.com/determinateSystems/flakehub-push?tab=readme-ov-file#available-parameters [flakes]: https://flakehub.com/flakes [go]: https://golang.org +[hm]: https://github.com/nix-community/home-manager [inputs]: https://zero-to-nix.com/concepts/flakes#inputs [java]: https://java.com [javascript]: https://javascript.info +[nix-darwin]: https://github.com/LnL7/nix-darwin [nix-flakes]: https://zero-to-nix.com/concepts/flakes [nixos]: https://zero-to-nix.com/concepts/nixos [nixpkgs]: https://zero-to-nix.com/concepts/nixpkgs From 9bce5dd64fc3f3bcf363ec178d11053073dd15e8 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 15:38:55 -0700 Subject: [PATCH 50/69] Enable passing commands to darwin-rebuild --- src/cli/cmd/apply/mod.rs | 12 ++++-------- src/cli/cmd/apply/nix_darwin.rs | 5 ++++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index d4acaac4..91912ef5 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -27,7 +27,7 @@ use self::home_manager::{HomeManager, HOME_MANAGER_SCRIPT}; use self::nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}; #[cfg(target_os = "macos")] -use self::nix_darwin::{NixDarwin, NIX_DARWIN_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}; +use self::nix_darwin::{NixDarwin, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}; use super::{CommandExecute, FlakeHubClient}; @@ -87,19 +87,15 @@ impl CommandExecute for ApplySubcommand { run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } #[cfg(target_os = "macos")] - System::NixDarwin(_) => { + System::NixDarwin(NixDarwin { command, .. }) => { let profile_path = apply_path_to_profile(NIX_DARWIN_PROFILE, &resolved_path.store_path).await?; // {path}/sw/bin/darwin-rebuild let script_path = path!(&profile_path, "sw", "bin", NIX_DARWIN_SCRIPT); - run_script( - script_path, - Some(NIX_DARWIN_ACTION.to_string()), - NIX_DARWIN_SCRIPT, - ) - .await?; + let darwin_rebuild_command = command.join(" "); + run_script(script_path, Some(darwin_rebuild_command), NIX_DARWIN_SCRIPT).await?; } #[cfg(target_os = "linux")] System::NixOs(NixOs { action, .. }) => { diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index 2d65244b..805b0cb5 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -2,7 +2,6 @@ use clap::Parser; use crate::cli::{cmd::parse_release_ref, error::FhError}; -pub(super) const NIX_DARWIN_ACTION: &str = "activate"; pub(super) const NIX_DARWIN_SCRIPT: &str = "darwin-rebuild"; pub(super) const NIX_DARWIN_PROFILE: &str = "system"; @@ -13,6 +12,10 @@ pub(super) struct NixDarwin { /// If the latter, the attribute path defaults to darwinConfigurations.{devicename}.system, where devicename /// is the output of scutil --get LocalHostName. pub(super) output_ref: String, + + /// The command or commands to pass to darwin-rebuild. + #[arg(trailing_var_arg = true, default_value = "activate")] + pub(super) command: Vec, } impl NixDarwin { From 5b2af5b6d2e81498f58366a20405acd18ee18e68 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 15:40:34 -0700 Subject: [PATCH 51/69] Document arbitrary darwin-rebuild commands --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 3c801130..01b2e4d5 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,14 @@ fh apply nix-darwin "my-org/macos-configs/0.1#darwinConfigurations.$(scutil --ge fh apply nix-darwin "my-org/macos-configs/0.1" ``` +`fh apply nix-darwin` first resolves the supplied output reference to a store path, builds the `darwin-rebuild` script for that path, and then runs `darwin-rebuild activate` by default. +You can also supply a [different command][darwin-rebuild] from `activate`. +Here's an example: + +```shell +fh apply nix-darwin "my-org/macos-configs/0.1" check +``` + ### Searching published flakes You can search publicly listed flakes using the `fh search` command and passing in a search query. @@ -394,6 +402,7 @@ For support, email support@flakehub.com or [join our Discord](https://discord.gg [bash]: https://gnu.org/software/bash [cache]: https://determinate.systems/posts/flakehub-cache-beta [csv]: https://en.wikipedia.org/wiki/Comma-separated_values +[darwin-rebuild]: https://github.com/LnL7/nix-darwin/blob/884f3fe6d9bf056ba0017c132c39c1f0d07d4fec/pkgs/nix-tools/darwin-rebuild.sh#L8-L17 [elm]: https://elm-lang.org [elvish]: https://elv.sh [fish]: https://fishshell.com From 89c36af2d8811d22baa2544e13391764a587cae3 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 15:44:21 -0700 Subject: [PATCH 52/69] Remove darwin-rebuild command --- README.md | 11 +++-------- src/cli/cmd/apply/mod.rs | 12 ++++++++---- src/cli/cmd/apply/nix_darwin.rs | 5 +---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 01b2e4d5..093030dc 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,8 @@ fh apply home-manager "my-org/home-configs/0.1#homeConfigurations.$(whoami)" fh apply home-manager "my-org/home-configs/0.1" ``` +`fh apply home-manager` first resolves the supplied output reference to a store path, builds the `activate` script for that path, and then runs it. + #### nix-darwin If you're on a macOS system that uses [nix-darwin], you can use `fh apply nix-darwin` to apply a configuration from an output path: @@ -217,13 +219,7 @@ fh apply nix-darwin "my-org/macos-configs/0.1#darwinConfigurations.$(scutil --ge fh apply nix-darwin "my-org/macos-configs/0.1" ``` -`fh apply nix-darwin` first resolves the supplied output reference to a store path, builds the `darwin-rebuild` script for that path, and then runs `darwin-rebuild activate` by default. -You can also supply a [different command][darwin-rebuild] from `activate`. -Here's an example: - -```shell -fh apply nix-darwin "my-org/macos-configs/0.1" check -``` +`fh apply nix-darwin` first resolves the supplied output reference to a store path, builds the `darwin-rebuild` script for that path, and then runs `darwin-rebuild activate`. ### Searching published flakes @@ -402,7 +398,6 @@ For support, email support@flakehub.com or [join our Discord](https://discord.gg [bash]: https://gnu.org/software/bash [cache]: https://determinate.systems/posts/flakehub-cache-beta [csv]: https://en.wikipedia.org/wiki/Comma-separated_values -[darwin-rebuild]: https://github.com/LnL7/nix-darwin/blob/884f3fe6d9bf056ba0017c132c39c1f0d07d4fec/pkgs/nix-tools/darwin-rebuild.sh#L8-L17 [elm]: https://elm-lang.org [elvish]: https://elv.sh [fish]: https://fishshell.com diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 91912ef5..7a1e16bf 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -27,7 +27,7 @@ use self::home_manager::{HomeManager, HOME_MANAGER_SCRIPT}; use self::nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}; #[cfg(target_os = "macos")] -use self::nix_darwin::{NixDarwin, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}; +use self::nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}; use super::{CommandExecute, FlakeHubClient}; @@ -87,15 +87,19 @@ impl CommandExecute for ApplySubcommand { run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } #[cfg(target_os = "macos")] - System::NixDarwin(NixDarwin { command, .. }) => { + System::NixDarwin(_) => { let profile_path = apply_path_to_profile(NIX_DARWIN_PROFILE, &resolved_path.store_path).await?; // {path}/sw/bin/darwin-rebuild let script_path = path!(&profile_path, "sw", "bin", NIX_DARWIN_SCRIPT); - let darwin_rebuild_command = command.join(" "); - run_script(script_path, Some(darwin_rebuild_command), NIX_DARWIN_SCRIPT).await?; + run_script( + script_path, + Some(DARWIN_REBUILD_ACTION.to_string()), + NIX_DARWIN_SCRIPT, + ) + .await?; } #[cfg(target_os = "linux")] System::NixOs(NixOs { action, .. }) => { diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index 805b0cb5..e6d1d638 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -2,6 +2,7 @@ use clap::Parser; use crate::cli::{cmd::parse_release_ref, error::FhError}; +pub(super) const DARWIN_REBUILD_ACTION: &str = "activate"; pub(super) const NIX_DARWIN_SCRIPT: &str = "darwin-rebuild"; pub(super) const NIX_DARWIN_PROFILE: &str = "system"; @@ -12,10 +13,6 @@ pub(super) struct NixDarwin { /// If the latter, the attribute path defaults to darwinConfigurations.{devicename}.system, where devicename /// is the output of scutil --get LocalHostName. pub(super) output_ref: String, - - /// The command or commands to pass to darwin-rebuild. - #[arg(trailing_var_arg = true, default_value = "activate")] - pub(super) command: Vec, } impl NixDarwin { From 11b2e13b097a0d929720701f42a1a8feff50c0a6 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Thu, 25 Jul 2024 15:53:00 -0700 Subject: [PATCH 53/69] Fix fh apply description --- src/cli/cmd/apply/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 7a1e16bf..c50edcff 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -31,7 +31,7 @@ use self::nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_PROFILE, NIX use super::{CommandExecute, FlakeHubClient}; -/// Update the specified Nix profile with the path resolved from a FlakeHub output reference. +/// Apply the configuration at the specified FlakeHub output reference to the current system #[derive(Parser)] pub(crate) struct ApplySubcommand { #[clap(subcommand)] From 8386e0e61895035efea694dd24211143cc80a95e Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Sun, 28 Jul 2024 19:23:59 -0700 Subject: [PATCH 54/69] Remove compile-time target restrictions --- src/cli/cmd/apply/home_manager.rs | 7 +++---- src/cli/cmd/apply/mod.rs | 20 +++++--------------- src/cli/cmd/apply/nix_darwin.rs | 7 +++---- src/cli/cmd/apply/nixos.rs | 5 ++--- src/cli/error.rs | 3 +++ 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index 0ad85961..b53564b6 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -26,13 +26,12 @@ impl HomeManager { // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. fn parse_output_ref(output_ref: &str) -> Result { - let username = whoami::username(); - Ok(match output_ref.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!( - "{}#homeConfigurations.{username}", - parse_release_ref(release)? + "{}#homeConfigurations.{}", + parse_release_ref(release)?, + whoami::username(), ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index c50edcff..e03e296a 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -1,7 +1,5 @@ mod home_manager; -#[cfg(target_os = "macos")] mod nix_darwin; -#[cfg(target_os = "linux")] mod nixos; use std::{ @@ -21,13 +19,11 @@ use crate::{ path, }; -use self::home_manager::{HomeManager, HOME_MANAGER_SCRIPT}; - -#[cfg(target_os = "linux")] -use self::nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}; - -#[cfg(target_os = "macos")] -use self::nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}; +use self::{ + home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, + nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}, + nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}, +}; use super::{CommandExecute, FlakeHubClient}; @@ -46,11 +42,9 @@ enum System { /// Resolve the store path for a Home Manager configuration and run its activation script HomeManager(HomeManager), - #[cfg(target_os = "macos")] /// Resolve the store path for a nix-darwin configuration and run its activation script NixDarwin(NixDarwin), - #[cfg(target_os = "linux")] /// Apply the resolved store path on a NixOS system #[clap(name = "nixos")] NixOs(NixOs), @@ -61,9 +55,7 @@ impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { let output_ref = match &self.system { System::HomeManager(home_manager) => home_manager.output_ref()?, - #[cfg(target_os = "linux")] System::NixOs(nixos) => nixos.output_ref()?, - #[cfg(target_os = "macos")] System::NixDarwin(nix_darwin) => nix_darwin.output_ref()?, }; @@ -86,7 +78,6 @@ impl CommandExecute for ApplySubcommand { run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } - #[cfg(target_os = "macos")] System::NixDarwin(_) => { let profile_path = apply_path_to_profile(NIX_DARWIN_PROFILE, &resolved_path.store_path).await?; @@ -101,7 +92,6 @@ impl CommandExecute for ApplySubcommand { ) .await?; } - #[cfg(target_os = "linux")] System::NixOs(NixOs { action, .. }) => { let profile_path = apply_path_to_profile(NIXOS_PROFILE, &resolved_path.store_path).await?; diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index e6d1d638..3245703c 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -29,13 +29,12 @@ impl NixDarwin { // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. fn parse_output_ref(output_ref: &str) -> Result { - let devicename = whoami::devicename(); - Ok(match output_ref.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!( - "{}#darwinConfigurations.{devicename}.system", - parse_release_ref(release)? + "{}#darwinConfigurations.{}.system", + parse_release_ref(release)?, + whoami::devicename(), ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 189e4d72..836ccfd8 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -59,12 +59,11 @@ impl Display for NixOsAction { // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. fn parse_output_ref(output_ref: &str) -> Result { - let hostname = gethostname::gethostname().to_string_lossy().to_string(); - Ok(match output_ref.split('#').collect::>()[..] { [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!( - "{}#nixosConfigurations.{hostname}", + "{}#nixosConfigurations.{}", + gethostname::gethostname().to_string_lossy(), parse_release_ref(release)? ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), diff --git a/src/cli/error.rs b/src/cli/error.rs index cbcffdc9..ccd1f5fa 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -51,6 +51,9 @@ pub(crate) enum FhError { #[error("a presumably unreachable point was reached: {0}")] Unreachable(String), + #[error("this functionality only supported on {0}")] + UnsupportedOs(String), + #[error("url parse error: {0}")] Url(#[from] url::ParseError), From b614e0d9bff36667d7ed97f5827e6cec75c9b31d Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Sun, 28 Jul 2024 19:25:34 -0700 Subject: [PATCH 55/69] More detailed info message for path resolution Co-authored-by: Graham Christensen --- src/cli/cmd/apply/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index e03e296a..3b245030 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -59,7 +59,7 @@ impl CommandExecute for ApplySubcommand { System::NixDarwin(nix_darwin) => nix_darwin.output_ref()?, }; - tracing::info!("Resolving store path for output: {}", output_ref); + tracing::info!("Resolving {}", output_ref); let output_ref = parse_flake_output_ref(&output_ref)?; From 53cb0770ab6a2663ff21bbea3e417d63d04bb5bc Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Sun, 28 Jul 2024 19:26:44 -0700 Subject: [PATCH 56/69] s/FH_JSON_OUTPUT/FH_OUTPUT_JSON --- src/cli/cmd/resolve.rs | 2 +- src/cli/cmd/search.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index caf8d9f8..a74e7409 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -13,7 +13,7 @@ pub(crate) struct ResolveSubcommand { flake_ref: String, /// Output the result as JSON displaying the store path plus the original attribute path. - #[arg(long, env = "FH_JSON_OUTPUT")] + #[arg(long, env = "FH_OUTPUT_JSON")] json: bool, #[clap(from_global)] diff --git a/src/cli/cmd/search.rs b/src/cli/cmd/search.rs index 9c398db2..f3312d0a 100644 --- a/src/cli/cmd/search.rs +++ b/src/cli/cmd/search.rs @@ -20,7 +20,7 @@ pub(crate) struct SearchSubcommand { max_results: usize, /// Output results as JSON. - #[clap(long, env = "FH_JSON_OUTPUT")] + #[clap(long, env = "FH_OUTPUT_JSON")] json: bool, #[clap(from_global)] From 970a5a1764a3181c22317bcf74a6a69e3ed80a8d Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Sun, 28 Jul 2024 19:30:36 -0700 Subject: [PATCH 57/69] Streamline sudo vs non-sudo command logic --- src/cli/cmd/mod.rs | 39 ++++++++++++++++----------------------- src/cli/error.rs | 3 --- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 74e8f6d0..0909064b 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -401,7 +401,7 @@ async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhErr let is_root_user = nix::unistd::getuid().is_root(); let use_sudo = sudo_if_necessary && !is_root_user; - if use_sudo { + let mut cmd = if use_sudo { tracing::warn!( "Current user is {} rather than root; running Nix command using sudo", whoami::username() @@ -410,37 +410,30 @@ async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhErr "Running: sudo nix --extra-experimental-features 'nix-command-flakes' {}", args.join(" ") ); + + let mut cmd = tokio::process::Command::new("sudo"); + cmd.arg("nix"); + cmd } else { tracing::debug!( "Running: nix --extra-experimental-features 'nix-command-flakes' {}", args.join(" ") ); - } - let output = if use_sudo { - tokio::process::Command::new("sudo") - .args(["nix", "--extra-experimental-features", "nix-command flakes"]) - .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .wrap_err("failed to spawn Nix command")? - .wait_with_output() - .await - .wrap_err("failed to wait for Nix command output")? - } else { tokio::process::Command::new("nix") - .args(["--extra-experimental-features", "nix-command flakes"]) - .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn() - .wrap_err("failed to spawn Nix command")? - .wait_with_output() - .await - .wrap_err("failed to wait for Nix command output")? }; + let output = cmd + .args(["--extra-experimental-features", "nix-command flakes"]) + .args(args) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn() + .wrap_err("failed to spawn Nix command")? + .wait_with_output() + .await + .wrap_err("failed to wait for Nix command output")?; + if output.status.success() { Ok(()) } else { diff --git a/src/cli/error.rs b/src/cli/error.rs index ccd1f5fa..cbcffdc9 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -51,9 +51,6 @@ pub(crate) enum FhError { #[error("a presumably unreachable point was reached: {0}")] Unreachable(String), - #[error("this functionality only supported on {0}")] - UnsupportedOs(String), - #[error("url parse error: {0}")] Url(#[from] url::ParseError), From a1f73464d98ca545d70895e7d71c27437f98ea6e Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Sun, 28 Jul 2024 19:44:21 -0700 Subject: [PATCH 58/69] Don't use sudo nix build for Home Manager --- src/cli/cmd/apply/home_manager.rs | 3 ++ src/cli/cmd/apply/mod.rs | 46 ++++++++++++++++++++++++------- src/cli/cmd/apply/nix_darwin.rs | 4 ++- src/cli/cmd/mod.rs | 7 +++-- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index b53564b6..679a1c6f 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -10,6 +10,9 @@ pub(super) struct HomeManager { /// References must take one of two forms: {org}/{flake}/{version_req}#{attr_path} or {org}/{flake}/{version_req}. /// If the latter, the attribute path defaults to homeConfigurations.{whoami}. pub(super) output_ref: String, + + #[arg(long, short, env = "FH_APPLY_PROFILE", default_value = "system")] + pub(super) profile: String, } impl HomeManager { diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 3b245030..045ff372 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -21,7 +21,7 @@ use crate::{ use self::{ home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, - nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_PROFILE, NIX_DARWIN_SCRIPT}, + nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_SCRIPT}, nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}, }; @@ -72,15 +72,26 @@ impl CommandExecute for ApplySubcommand { ); match self.system { - System::HomeManager(_) => { + System::HomeManager(HomeManager { profile, .. }) => { + let profile_path = apply_path_to_profile( + &profile, + &resolved_path.store_path, + false, // don't sudo when running `nix build` + ) + .await?; + // /nix/store/{path}/activate - let script_path = path!(&resolved_path.store_path, HOME_MANAGER_SCRIPT); + let script_path = path!(&profile_path, HOME_MANAGER_SCRIPT); run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } - System::NixDarwin(_) => { - let profile_path = - apply_path_to_profile(NIX_DARWIN_PROFILE, &resolved_path.store_path).await?; + System::NixDarwin(NixDarwin { profile, .. }) => { + let profile_path = apply_path_to_profile( + &profile, + &resolved_path.store_path, + true, // sudo if necessary when running `nix build` + ) + .await?; // {path}/sw/bin/darwin-rebuild let script_path = path!(&profile_path, "sw", "bin", NIX_DARWIN_SCRIPT); @@ -93,8 +104,12 @@ impl CommandExecute for ApplySubcommand { .await?; } System::NixOs(NixOs { action, .. }) => { - let profile_path = - apply_path_to_profile(NIXOS_PROFILE, &resolved_path.store_path).await?; + let profile_path = apply_path_to_profile( + NIXOS_PROFILE, + &resolved_path.store_path, + true, // sudo if necessary when running `nix build` + ) + .await?; let script_path = path!(&profile_path, "bin", NIXOS_SCRIPT); @@ -155,7 +170,18 @@ async fn run_script( Ok(()) } -async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result { +async fn apply_path_to_profile( + profile: &str, + store_path: &str, + sudo_if_necessary: bool, +) -> Result { + // Ensure that we support both formats: + // 1. some-profile + // 2. /nix/var/nix/profiles/some-profile + let profile = profile + .strip_prefix("h/nix/var/nix/profiles/") + .unwrap_or(profile); + let profile_path = format!("/nix/var/nix/profiles/{profile}"); tracing::info!( @@ -176,7 +202,7 @@ async fn apply_path_to_profile(profile: &str, store_path: &str) -> Result bool { + nix::unistd::getuid().is_root() +} + async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhError> { command_exists("nix")?; - let is_root_user = nix::unistd::getuid().is_root(); - let use_sudo = sudo_if_necessary && !is_root_user; + let use_sudo = sudo_if_necessary && !is_root_user(); let mut cmd = if use_sudo { tracing::warn!( From ee9a3182881090ed3c429e37b80bee29619ece13 Mon Sep 17 00:00:00 2001 From: Luc Perkins Date: Sun, 28 Jul 2024 19:47:50 -0700 Subject: [PATCH 59/69] Fix broken output ref test --- src/cli/cmd/apply/nix_darwin.rs | 2 +- src/cli/cmd/apply/nixos.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index 7e92b311..68900116 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -47,7 +47,7 @@ mod tests { use crate::cli::cmd::apply::nix_darwin::parse_output_ref; #[test] - fn test_parse_nixos_output_ref() { + fn test_parse_nix_darwin_output_ref() { let devicename = whoami::devicename(); let cases: Vec<(&str, String)> = vec![ diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 836ccfd8..92a87d99 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -63,8 +63,8 @@ fn parse_output_ref(output_ref: &str) -> Result { [_release, _output_path] => parse_release_ref(output_ref)?, [release] => format!( "{}#nixosConfigurations.{}", + parse_release_ref(release)?, gethostname::gethostname().to_string_lossy(), - parse_release_ref(release)? ), _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), }) From 945f7fa8da8ba196bbca4121e684e9c4f583e3ae Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 29 Jul 2024 16:34:33 -0400 Subject: [PATCH 60/69] Simplify profile handling by not adding heuristics, and dropping the profile from home manager --- Cargo.lock | 48 +++++++++++++++++++++ Cargo.toml | 1 + src/cli/cmd/apply/home_manager.rs | 3 -- src/cli/cmd/apply/mod.rs | 69 +++++++++++++++++-------------- src/cli/cmd/apply/nix_darwin.rs | 9 +++- src/cli/cmd/apply/nixos.rs | 2 +- src/cli/error.rs | 3 ++ 7 files changed, 99 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cac8d15..c75de347 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,6 +575,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "eyre" version = "0.6.8" @@ -585,6 +595,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "fh" version = "0.1.14" @@ -612,6 +628,7 @@ dependencies = [ "serde", "serde_json", "tabled", + "tempfile", "thiserror", "tokio", "tracing", @@ -968,6 +985,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "lock_api" version = "0.4.10" @@ -1471,6 +1494,19 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.21.12" @@ -1787,6 +1823,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.55" diff --git a/Cargo.toml b/Cargo.toml index 78467ae2..ce0ff846 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ semver = { version = "1.0.18", default-features = false, features = ["serde"] } serde = { version = "1.0.188", default-features = false, features = ["derive"] } serde_json = "1.0.105" tabled = { version = "0.14.0", features = ["color"] } +tempfile = "3.10.1" thiserror = { version = "1.0.44", default-features = false } tokio = { version = "1.30.0", default-features = false, features = ["full"] } tracing = "0.1.37" diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index 679a1c6f..b53564b6 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -10,9 +10,6 @@ pub(super) struct HomeManager { /// References must take one of two forms: {org}/{flake}/{version_req}#{attr_path} or {org}/{flake}/{version_req}. /// If the latter, the attribute path defaults to homeConfigurations.{whoami}. pub(super) output_ref: String, - - #[arg(long, short, env = "FH_APPLY_PROFILE", default_value = "system")] - pub(super) profile: String, } impl HomeManager { diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index 045ff372..a6c48162 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -4,12 +4,13 @@ mod nixos; use std::{ os::unix::prelude::PermissionsExt, - path::PathBuf, + path::{Path, PathBuf}, process::{ExitCode, Stdio}, }; use clap::{Parser, Subcommand}; use color_eyre::eyre::Context; +use tempfile::{tempdir, TempDir}; use crate::{ cli::{ @@ -72,9 +73,9 @@ impl CommandExecute for ApplySubcommand { ); match self.system { - System::HomeManager(HomeManager { profile, .. }) => { - let profile_path = apply_path_to_profile( - &profile, + System::HomeManager(HomeManager { .. }) => { + let (profile_path, _tempdir) = apply_path_to_profile( + None, &resolved_path.store_path, false, // don't sudo when running `nix build` ) @@ -86,15 +87,15 @@ impl CommandExecute for ApplySubcommand { run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; } System::NixDarwin(NixDarwin { profile, .. }) => { - let profile_path = apply_path_to_profile( - &profile, + apply_path_to_profile( + Some(&profile), &resolved_path.store_path, true, // sudo if necessary when running `nix build` ) .await?; // {path}/sw/bin/darwin-rebuild - let script_path = path!(&profile_path, "sw", "bin", NIX_DARWIN_SCRIPT); + let script_path = path!(&profile, "sw", "bin", NIX_DARWIN_SCRIPT); run_script( script_path, @@ -104,8 +105,9 @@ impl CommandExecute for ApplySubcommand { .await?; } System::NixOs(NixOs { action, .. }) => { - let profile_path = apply_path_to_profile( - NIXOS_PROFILE, + let profile_path = Path::new(NIXOS_PROFILE); + apply_path_to_profile( + Some(Path::new(NIXOS_PROFILE)), &resolved_path.store_path, true, // sudo if necessary when running `nix build` ) @@ -171,24 +173,28 @@ async fn run_script( } async fn apply_path_to_profile( - profile: &str, + input_profile_path: Option<&Path>, store_path: &str, sudo_if_necessary: bool, -) -> Result { - // Ensure that we support both formats: - // 1. some-profile - // 2. /nix/var/nix/profiles/some-profile - let profile = profile - .strip_prefix("h/nix/var/nix/profiles/") - .unwrap_or(profile); - - let profile_path = format!("/nix/var/nix/profiles/{profile}"); - - tracing::info!( - "Applying resolved store path {} to profile at {}", - store_path, +) -> Result<(PathBuf, Option), FhError> { + let temp_handle: Option; + + let profile_path: PathBuf = if let Some(profile_path) = input_profile_path { + temp_handle = None; + tracing::info!( + "Applying resolved store path {} to profile at {}", + store_path, + profile_path.display() + ); + + profile_path.into() + } else { + let dir = tempdir()?; + let profile_path = dir.path().join("profile"); + + temp_handle = Some(dir); profile_path - ); + }; nix_command( &[ @@ -199,7 +205,7 @@ async fn apply_path_to_profile( "--max-jobs", "0", "--profile", - &profile_path, + profile_path.to_str().ok_or(FhError::InvalidProfile)?, store_path, ], sudo_if_necessary, @@ -207,10 +213,13 @@ async fn apply_path_to_profile( .await .wrap_err("failed to build resolved store path with Nix")?; - tracing::info!( - "Successfully applied resolved path {} to profile at {profile_path}", - store_path - ); + if input_profile_path.is_some() { + tracing::info!( + "Successfully applied resolved path {} to profile at {}", + store_path, + profile_path.display() + ); + } - Ok(profile_path) + Ok((profile_path, temp_handle)) } diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index 68900116..1fe43a29 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -13,8 +13,13 @@ pub(super) struct NixDarwin { /// is the output of scutil --get LocalHostName. pub(super) output_ref: String, - #[arg(long, short, env = "FH_APPLY_PROFILE", default_value = "system")] - pub(super) profile: String, + #[arg( + long, + short, + env = "FH_APPLY_PROFILE", + default_value = "/nix/var/nix/profiles/system" + )] + pub(super) profile: std::path::PathBuf, } impl NixDarwin { diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 92a87d99..1e3cbe0f 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -4,7 +4,7 @@ use clap::{Parser, ValueEnum}; use crate::cli::{cmd::parse_release_ref, error::FhError}; -pub(super) const NIXOS_PROFILE: &str = "system"; +pub(super) const NIXOS_PROFILE: &str = "/nix/var/nix/profiles/system"; pub(super) const NIXOS_SCRIPT: &str = "switch-to-configuration"; #[derive(Parser)] diff --git a/src/cli/error.rs b/src/cli/error.rs index cbcffdc9..6286654d 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -18,6 +18,9 @@ pub(crate) enum FhError { #[error("interactive initializer error: {0}")] Interactive(#[from] inquire::InquireError), + #[error("Profile path is not valid UTF-8")] + InvalidProfile, + #[error("json parsing error: {0}")] Json(#[from] serde_json::Error), From 104effc58c36eb9f7221ea70c2add269729b6a9b Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 29 Jul 2024 16:35:03 -0400 Subject: [PATCH 61/69] Update src/cli/cmd/apply/nix_darwin.rs --- src/cli/cmd/apply/nix_darwin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index 1fe43a29..acac9661 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -32,7 +32,7 @@ impl NixDarwin { // // fh apply nix-darwin omnicorp/home/0.1 // -// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0#darwinConfigurations.$(devicename).system`. +// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0#darwinConfigurations.$(devicename)`. // If you need to apply a configuration at a path that doesn't conform to this pattern, you // can still provide an explicit path. fn parse_output_ref(output_ref: &str) -> Result { From e70ad96f41c0184ab5184cb18b0cdcc32bcf04fa Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 29 Jul 2024 20:58:56 -0400 Subject: [PATCH 62/69] Centralize and fixup the handling of https://flakehub.com/f/... flakerefs --- src/cli/cmd/apply/home_manager.rs | 61 ++------------------ src/cli/cmd/apply/mod.rs | 95 +++++++++++++++++++++++++++---- src/cli/cmd/apply/nix_darwin.rs | 60 ++----------------- src/cli/cmd/apply/nixos.rs | 67 ++++------------------ src/cli/cmd/mod.rs | 11 ++-- src/cli/cmd/resolve.rs | 5 +- 6 files changed, 116 insertions(+), 183 deletions(-) diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index b53564b6..4c84ef2a 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -1,7 +1,5 @@ use clap::Parser; -use crate::cli::{cmd::parse_release_ref, error::FhError}; - pub(super) const HOME_MANAGER_SCRIPT: &str = "activate"; #[derive(Parser)] @@ -12,60 +10,11 @@ pub(super) struct HomeManager { pub(super) output_ref: String, } -impl HomeManager { - pub(super) fn output_ref(&self) -> Result { - parse_output_ref(&self.output_ref) +impl super::ApplyType for HomeManager { + fn get_ref(&self) -> &str { + &self.output_ref } -} - -// This function enables you to provide simplified paths: -// -// fh apply home-manager omnicorp/home/0.1 -// -// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#homeConfigurations.$(whoami)`. -// If you need to apply a configuration at a path that doesn't conform to this pattern, you -// can still provide an explicit path. -fn parse_output_ref(output_ref: &str) -> Result { - Ok(match output_ref.split('#').collect::>()[..] { - [_release, _output_path] => parse_release_ref(output_ref)?, - [release] => format!( - "{}#homeConfigurations.{}", - parse_release_ref(release)?, - whoami::username(), - ), - _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), - }) -} - -#[cfg(test)] -mod tests { - use crate::cli::cmd::apply::home_manager::parse_output_ref; - - #[test] - fn test_parse_home_manager_output_ref() { - let username = whoami::username(); - - let cases: Vec<(&str, String)> = vec![ - ( - "foo/bar/*", - format!("foo/bar/*#homeConfigurations.{username}"), - ), - ( - "foo/bar/0.1.*", - format!("foo/bar/0.1.*#homeConfigurations.{username}"), - ), - ( - "omnicorp/web/0.1.2#homeConfigurations.my-config", - "omnicorp/web/0.1.2#homeConfigurations.my-config".to_string(), - ), - ( - "omnicorp/web/0.1.2#packages.x86_64-linux.default", - "omnicorp/web/0.1.2#packages.x86_64-linux.default".to_string(), - ), - ]; - - for case in cases { - assert_eq!(parse_output_ref(case.0).unwrap(), case.1); - } + fn default_ref(&self) -> String { + format!("homeConfigurations.{}", whoami::username()) } } diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index a6c48162..c38403cb 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -13,10 +13,7 @@ use color_eyre::eyre::Context; use tempfile::{tempdir, TempDir}; use crate::{ - cli::{ - cmd::{nix_command, parse_flake_output_ref}, - error::FhError, - }, + cli::{cmd::nix_command, error::FhError}, path, }; @@ -36,6 +33,9 @@ pub(crate) struct ApplySubcommand { #[clap(from_global)] api_addr: url::Url, + + #[clap(from_global)] + frontend_addr: url::Url, } #[derive(Subcommand)] @@ -51,19 +51,30 @@ enum System { NixOs(NixOs), } +pub trait ApplyType { + fn get_ref(&self) -> &str; + fn default_ref(&self) -> String; +} + #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { - let output_ref = match &self.system { - System::HomeManager(home_manager) => home_manager.output_ref()?, - System::NixOs(nixos) => nixos.output_ref()?, - System::NixDarwin(nix_darwin) => nix_darwin.output_ref()?, + let output_ref = { + let applyer: Box<&dyn ApplyType> = match &self.system { + System::HomeManager(home_manager) => Box::new(home_manager), + System::NixOs(nixos) => Box::new(nixos), + System::NixDarwin(nix_darwin) => Box::new(nix_darwin), + }; + + parse_output_ref( + &self.frontend_addr, + applyer.get_ref(), + &applyer.default_ref(), + )? }; tracing::info!("Resolving {}", output_ref); - let output_ref = parse_flake_output_ref(&output_ref)?; - let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; tracing::debug!( @@ -123,6 +134,32 @@ impl CommandExecute for ApplySubcommand { } } +// This function enables you to provide simplified paths: +// +// fh apply nixos omnicorp/systems/0.1 +// +// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#nixosConfigurations.$(hostname)`. +// If you need to apply a configuration at a path that doesn't conform to this pattern, you +// can still provide an explicit path. +fn parse_output_ref( + frontend_addr: &url::Url, + output_ref: &str, + default_path: &str, +) -> Result { + let with_default_output_path = match output_ref.split('#').collect::>()[..] { + [_release, _output_path] => output_ref.to_string(), + [_release] => format!("{}#{}", output_ref, default_path), + _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), + }; + + let output_ref = + super::parse_flake_output_ref(frontend_addr, &with_default_output_path)?.to_string(); + + let parsed = super::parse_release_ref(&output_ref)?; + + parsed.try_into() +} + async fn run_script( script_path: PathBuf, action: Option, @@ -223,3 +260,41 @@ async fn apply_path_to_profile( Ok((profile_path, temp_handle)) } + +#[cfg(test)] +mod tests { + use super::parse_output_ref; + + #[test] + fn test_parse_output_ref() { + let cases: Vec<(&str, &str)> = vec![ + ("foo/bar/*", "foo/bar/*#DefaultFooBar"), + ("foo/bar/0.1.*", "foo/bar/0.1.*#DefaultFooBar"), + ( + "omnicorp/web/0.1.2#homeConfigurations.my-config", + "omnicorp/web/0.1.2#homeConfigurations.my-config", + ), + ( + "omnicorp/web/0.1.2#packages.x86_64-linux.default", + "omnicorp/web/0.1.2#packages.x86_64-linux.default", + ), + ( + "https://flakehub.com/f/omnicorp/web/0.1.2#packages.x86_64-linux.default", + "omnicorp/web/0.1.2#packages.x86_64-linux.default", + ), + ]; + + for (input, expect) in cases { + assert_eq!( + &parse_output_ref( + &url::Url::parse("https://flakehub.com/f").unwrap(), + input, + "DefaultFooBar" + ) + .expect(&format!("failing case: {input}")) + .to_string(), + expect, + ); + } + } +} diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index acac9661..b25f722e 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -1,7 +1,5 @@ use clap::Parser; -use crate::cli::{cmd::parse_release_ref, error::FhError}; - pub(super) const DARWIN_REBUILD_ACTION: &str = "activate"; pub(super) const NIX_DARWIN_SCRIPT: &str = "darwin-rebuild"; @@ -22,60 +20,12 @@ pub(super) struct NixDarwin { pub(super) profile: std::path::PathBuf, } -impl NixDarwin { - pub(super) fn output_ref(&self) -> Result { - parse_output_ref(&self.output_ref) +impl super::ApplyType for NixDarwin { + fn get_ref(&self) -> &str { + &self.output_ref } -} - -// This function enables you to provide simplified paths: -// -// fh apply nix-darwin omnicorp/home/0.1 -// -// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0#darwinConfigurations.$(devicename)`. -// If you need to apply a configuration at a path that doesn't conform to this pattern, you -// can still provide an explicit path. -fn parse_output_ref(output_ref: &str) -> Result { - Ok(match output_ref.split('#').collect::>()[..] { - [_release, _output_path] => parse_release_ref(output_ref)?, - [release] => format!( - "{}#darwinConfigurations.{}.system", - parse_release_ref(release)?, - whoami::devicename(), - ), - _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), - }) -} - -#[cfg(test)] -mod tests { - use crate::cli::cmd::apply::nix_darwin::parse_output_ref; - - #[test] - fn test_parse_nix_darwin_output_ref() { - let devicename = whoami::devicename(); - - let cases: Vec<(&str, String)> = vec![ - ( - "foo/bar/*", - format!("foo/bar/*#darwinConfigurations.{devicename}.system"), - ), - ( - "foo/bar/0.1.*", - format!("foo/bar/0.1.*#darwinConfigurations.{devicename}.system"), - ), - ( - "omnicorp/web/0.1.2#darwinConfigurations.my-config", - "omnicorp/web/0.1.2#darwinConfigurations.my-config".to_string(), - ), - ( - "omnicorp/web/0.1.2#packages.x86_64-linux.default", - "omnicorp/web/0.1.2#packages.x86_64-linux.default".to_string(), - ), - ]; - for case in cases { - assert_eq!(parse_output_ref(case.0).unwrap(), case.1); - } + fn default_ref(&self) -> String { + format!("darwinConfigurations.{}", whoami::devicename(),) } } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 1e3cbe0f..6aeca15a 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -2,8 +2,6 @@ use std::fmt::Display; use clap::{Parser, ValueEnum}; -use crate::cli::{cmd::parse_release_ref, error::FhError}; - pub(super) const NIXOS_PROFILE: &str = "/nix/var/nix/profiles/system"; pub(super) const NIXOS_SCRIPT: &str = "switch-to-configuration"; @@ -20,9 +18,16 @@ pub(super) struct NixOs { pub(super) action: NixOsAction, } -impl NixOs { - pub(super) fn output_ref(&self) -> Result { - parse_output_ref(&self.output_ref) +impl super::ApplyType for NixOs { + fn get_ref(&self) -> &str { + &self.output_ref + } + + fn default_ref(&self) -> String { + format!( + "nixosConfigurations.{}", + gethostname::gethostname().to_string_lossy() + ) } } @@ -50,55 +55,3 @@ impl Display for NixOsAction { ) } } - -// This function enables you to provide simplified paths: -// -// fh apply nixos omnicorp/systems/0.1 -// -// Here, `omnicorp/systems/0.1` resolves to `omnicorp/systems/0.1#nixosConfigurations.$(hostname)`. -// If you need to apply a configuration at a path that doesn't conform to this pattern, you -// can still provide an explicit path. -fn parse_output_ref(output_ref: &str) -> Result { - Ok(match output_ref.split('#').collect::>()[..] { - [_release, _output_path] => parse_release_ref(output_ref)?, - [release] => format!( - "{}#nixosConfigurations.{}", - parse_release_ref(release)?, - gethostname::gethostname().to_string_lossy(), - ), - _ => return Err(FhError::MalformedOutputRef(output_ref.to_string())), - }) -} - -#[cfg(test)] -mod tests { - use crate::cli::cmd::apply::nixos::parse_output_ref; - - #[test] - fn test_parse_nixos_output_ref() { - let hostname = gethostname::gethostname().to_string_lossy().to_string(); - - let cases: Vec<(&str, String)> = vec![ - ( - "foo/bar/*", - format!("foo/bar/*#nixosConfigurations.{hostname}"), - ), - ( - "foo/bar/0.1.*", - format!("foo/bar/0.1.*#nixosConfigurations.{hostname}"), - ), - ( - "omnicorp/web/0.1.2#nixosConfigurations.auth-server", - "omnicorp/web/0.1.2#nixosConfigurations.auth-server".to_string(), - ), - ( - "omnicorp/web/0.1.2#packages.x86_64-linux.default", - "omnicorp/web/0.1.2#packages.x86_64-linux.default".to_string(), - ), - ]; - - for case in cases { - assert_eq!(parse_output_ref(case.0).unwrap(), case.1); - } - } -} diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 45fd46fc..22ffeab7 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -444,13 +444,16 @@ async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhErr } } -fn parse_flake_output_ref(output_ref: &str) -> Result { +fn parse_flake_output_ref( + frontend_addr: &url::Url, + output_ref: &str, +) -> Result { // Ensures that users can use both forms: // 1. https://flakehub/f/{org}/{project}/{version_req}#{output} // 2. {org}/{project}/{version_req}#{output} let output_ref = String::from( output_ref - .strip_prefix("https://flakehub.com/f/") + .strip_prefix(frontend_addr.join("f/")?.as_str()) .unwrap_or(output_ref), ); @@ -459,13 +462,13 @@ fn parse_flake_output_ref(output_ref: &str) -> Result { // Ensure that release refs are of the form {org}/{project}/{version_req} fn parse_release_ref(flake_ref: &str) -> Result { - match flake_ref.split('/').collect::>()[..] { + match flake_ref.to_string().split('/').collect::>()[..] { [org, project, version_req] => { validate_segment(org)?; validate_segment(project)?; validate_segment(version_req)?; - Ok(String::from(flake_ref)) + Ok(flake_ref.to_string()) } _ => Err(FhError::FlakeParse(format!( "flake ref {flake_ref} invalid; must be of the form {{org}}/{{project}}/{{version_req}}" diff --git a/src/cli/cmd/resolve.rs b/src/cli/cmd/resolve.rs index a74e7409..88853238 100644 --- a/src/cli/cmd/resolve.rs +++ b/src/cli/cmd/resolve.rs @@ -18,6 +18,9 @@ pub(crate) struct ResolveSubcommand { #[clap(from_global)] api_addr: url::Url, + + #[clap(from_global)] + frontend_addr: url::Url, } #[derive(Deserialize, Serialize)] @@ -32,7 +35,7 @@ pub(crate) struct ResolvedPath { impl CommandExecute for ResolveSubcommand { #[tracing::instrument(skip_all)] async fn execute(self) -> color_eyre::Result { - let output_ref = parse_flake_output_ref(&self.flake_ref)?; + let output_ref = parse_flake_output_ref(&self.frontend_addr, &self.flake_ref)?; let resolved_path = FlakeHubClient::resolve(self.api_addr.as_ref(), &output_ref).await?; From 69dfff8f190794dad6a26ff544e3d440ab51070a Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 29 Jul 2024 21:01:40 -0400 Subject: [PATCH 63/69] Put back the rust.yml --- .github/workflows/rust.yml | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e27e71e6..725a7b6b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -6,7 +6,7 @@ on: branches: [main] jobs: - rust-fmt: + rust-fmt-and-clippy: runs-on: UbuntuLatest32Cores128G steps: - uses: actions/checkout@v4 @@ -17,25 +17,6 @@ jobs: - name: Rust formatting run: nix develop --command cargo fmt --check - rust-clippy-and-test: - # We need to run the checks on Linux and macOS because there are system-specific - # versions of fh (e.g. `fh apply nix-darwin` is only on macOS while `fh apply - # nixos` is only on Linux) - strategy: - matrix: - runners: - # Linux - - UbuntuLatest32Cores128G - # macOS - - macos-latest-xlarge - - runs-on: ${{ matrix.runners }} - steps: - - uses: actions/checkout@v4 - - - uses: DeterminateSystems/nix-installer-action@v4 - - uses: DeterminateSystems/magic-nix-cache-action@main - - name: Clippy run: nix develop --command cargo clippy --all-targets --all-features -- -Dwarnings From ad8672c8b7ee0c5881c157224b74292380bfa9e4 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 29 Jul 2024 21:25:57 -0400 Subject: [PATCH 64/69] Flatten the implementation into a trait --- src/cli/cmd/apply/home_manager.rs | 19 +++++- src/cli/cmd/apply/mod.rs | 97 ++++++++++++------------------- src/cli/cmd/apply/nix_darwin.rs | 19 +++++- src/cli/cmd/apply/nixos.rs | 19 +++++- src/cli/cmd/mod.rs | 13 ----- 5 files changed, 85 insertions(+), 82 deletions(-) diff --git a/src/cli/cmd/apply/home_manager.rs b/src/cli/cmd/apply/home_manager.rs index 4c84ef2a..55d2dc03 100644 --- a/src/cli/cmd/apply/home_manager.rs +++ b/src/cli/cmd/apply/home_manager.rs @@ -1,7 +1,5 @@ use clap::Parser; -pub(super) const HOME_MANAGER_SCRIPT: &str = "activate"; - #[derive(Parser)] pub(super) struct HomeManager { /// The FlakeHub output reference for the Home Manager configuration. @@ -14,7 +12,24 @@ impl super::ApplyType for HomeManager { fn get_ref(&self) -> &str { &self.output_ref } + fn default_ref(&self) -> String { format!("homeConfigurations.{}", whoami::username()) } + + fn profile_path(&self) -> Option<&std::path::Path> { + None + } + + fn requires_root(&self) -> bool { + false + } + + fn relative_path(&self) -> &std::path::Path { + std::path::Path::new("activate") + } + + fn action(&self) -> Option { + None + } } diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index c38403cb..d1bfa585 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -12,16 +12,9 @@ use clap::{Parser, Subcommand}; use color_eyre::eyre::Context; use tempfile::{tempdir, TempDir}; -use crate::{ - cli::{cmd::nix_command, error::FhError}, - path, -}; +use crate::cli::{cmd::nix_command, error::FhError}; -use self::{ - home_manager::{HomeManager, HOME_MANAGER_SCRIPT}, - nix_darwin::{NixDarwin, DARWIN_REBUILD_ACTION, NIX_DARWIN_SCRIPT}, - nixos::{NixOs, NIXOS_PROFILE, NIXOS_SCRIPT}, -}; +use self::{home_manager::HomeManager, nix_darwin::NixDarwin, nixos::NixOs}; use super::{CommandExecute, FlakeHubClient}; @@ -53,19 +46,28 @@ enum System { pub trait ApplyType { fn get_ref(&self) -> &str; + fn default_ref(&self) -> String; + + fn profile_path(&self) -> Option<&Path>; + + fn requires_root(&self) -> bool; + + fn relative_path(&self) -> &Path; + + fn action(&self) -> Option; } #[async_trait::async_trait] impl CommandExecute for ApplySubcommand { async fn execute(self) -> color_eyre::Result { - let output_ref = { - let applyer: Box<&dyn ApplyType> = match &self.system { - System::HomeManager(home_manager) => Box::new(home_manager), - System::NixOs(nixos) => Box::new(nixos), - System::NixDarwin(nix_darwin) => Box::new(nix_darwin), - }; + let applyer: Box<&(dyn ApplyType + Send + Sync)> = match &self.system { + System::HomeManager(home_manager) => Box::new(home_manager), + System::NixOs(nixos) => Box::new(nixos), + System::NixDarwin(nix_darwin) => Box::new(nix_darwin), + }; + let output_ref = { parse_output_ref( &self.frontend_addr, applyer.get_ref(), @@ -83,52 +85,25 @@ impl CommandExecute for ApplySubcommand { &resolved_path.store_path ); - match self.system { - System::HomeManager(HomeManager { .. }) => { - let (profile_path, _tempdir) = apply_path_to_profile( - None, - &resolved_path.store_path, - false, // don't sudo when running `nix build` - ) - .await?; - - // /nix/store/{path}/activate - let script_path = path!(&profile_path, HOME_MANAGER_SCRIPT); - - run_script(script_path, None, HOME_MANAGER_SCRIPT).await?; - } - System::NixDarwin(NixDarwin { profile, .. }) => { - apply_path_to_profile( - Some(&profile), - &resolved_path.store_path, - true, // sudo if necessary when running `nix build` - ) - .await?; - - // {path}/sw/bin/darwin-rebuild - let script_path = path!(&profile, "sw", "bin", NIX_DARWIN_SCRIPT); - - run_script( - script_path, - Some(DARWIN_REBUILD_ACTION.to_string()), - NIX_DARWIN_SCRIPT, - ) - .await?; - } - System::NixOs(NixOs { action, .. }) => { - let profile_path = Path::new(NIXOS_PROFILE); - apply_path_to_profile( - Some(Path::new(NIXOS_PROFILE)), - &resolved_path.store_path, - true, // sudo if necessary when running `nix build` - ) - .await?; - - let script_path = path!(&profile_path, "bin", NIXOS_SCRIPT); - - run_script(script_path, Some(action.to_string()), NIXOS_SCRIPT).await?; - } - } + let (profile_path, _tempdir) = apply_path_to_profile( + applyer.profile_path(), + &resolved_path.store_path, + applyer.requires_root(), + ) + .await?; + + let script_path = profile_path.join(applyer.relative_path()); + + run_script( + script_path, + applyer.action(), + &applyer + .relative_path() + .file_name() + .expect("The apply type should absolutely have a file name.") + .to_string_lossy(), + ) + .await?; Ok(ExitCode::SUCCESS) } diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index b25f722e..c984154b 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -1,8 +1,5 @@ use clap::Parser; -pub(super) const DARWIN_REBUILD_ACTION: &str = "activate"; -pub(super) const NIX_DARWIN_SCRIPT: &str = "darwin-rebuild"; - #[derive(Parser)] pub(super) struct NixDarwin { /// The FlakeHub output reference for the nix-darwin configuration. @@ -28,4 +25,20 @@ impl super::ApplyType for NixDarwin { fn default_ref(&self) -> String { format!("darwinConfigurations.{}", whoami::devicename(),) } + + fn profile_path(&self) -> Option<&std::path::Path> { + Some(&self.profile) + } + + fn requires_root(&self) -> bool { + true + } + + fn relative_path(&self) -> &std::path::Path { + std::path::Path::new("sw/bin/darwin-rebuild") + } + + fn action(&self) -> Option { + Some("activate".to_string()) + } } diff --git a/src/cli/cmd/apply/nixos.rs b/src/cli/cmd/apply/nixos.rs index 6aeca15a..155cce87 100644 --- a/src/cli/cmd/apply/nixos.rs +++ b/src/cli/cmd/apply/nixos.rs @@ -2,9 +2,6 @@ use std::fmt::Display; use clap::{Parser, ValueEnum}; -pub(super) const NIXOS_PROFILE: &str = "/nix/var/nix/profiles/system"; -pub(super) const NIXOS_SCRIPT: &str = "switch-to-configuration"; - #[derive(Parser)] pub(super) struct NixOs { /// The FlakeHub output reference to apply to the system profile. @@ -29,6 +26,22 @@ impl super::ApplyType for NixOs { gethostname::gethostname().to_string_lossy() ) } + + fn profile_path(&self) -> Option<&std::path::Path> { + Some(std::path::Path::new("/nix/var/nix/profiles/system")) + } + + fn requires_root(&self) -> bool { + true + } + + fn relative_path(&self) -> &std::path::Path { + std::path::Path::new("bin/switch-to-configuration") + } + + fn action(&self) -> Option { + Some(self.action.to_string()) + } } // For available commands, see diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 22ffeab7..2c910505 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -382,19 +382,6 @@ macro_rules! flakehub_url { }}; } -#[macro_export] -macro_rules! path { - ($root:expr, $($segment:expr),+ $(,)?) => {{ - let mut path = PathBuf::from($root); - - $( - path.push($segment); - )+ - - path - }}; -} - fn is_root_user() -> bool { nix::unistd::getuid().is_root() } From 2526e9556be312b73405397f6f414a67be7ee52f Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 29 Jul 2024 21:27:58 -0400 Subject: [PATCH 65/69] clippy nit --- src/cli/cmd/apply/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index d1bfa585..cbb65960 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -266,7 +266,7 @@ mod tests { input, "DefaultFooBar" ) - .expect(&format!("failing case: {input}")) + .unwrap_or_else(|_| panic!("failing case: {input}")) .to_string(), expect, ); From 07efe6c44e39d8b9f6462e099f15a0b4368d0080 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 30 Jul 2024 10:46:02 -0400 Subject: [PATCH 66/69] nit --- src/cli/cmd/apply/nix_darwin.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/apply/nix_darwin.rs b/src/cli/cmd/apply/nix_darwin.rs index c984154b..de8115a4 100644 --- a/src/cli/cmd/apply/nix_darwin.rs +++ b/src/cli/cmd/apply/nix_darwin.rs @@ -23,7 +23,7 @@ impl super::ApplyType for NixDarwin { } fn default_ref(&self) -> String { - format!("darwinConfigurations.{}", whoami::devicename(),) + format!("darwinConfigurations.{}", whoami::devicename()) } fn profile_path(&self) -> Option<&std::path::Path> { From e4d26fab29d5c942d7a5179e7bbb0ef655fdb46a Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 30 Jul 2024 10:49:12 -0400 Subject: [PATCH 67/69] Tracing output the entire command --- src/cli/cmd/mod.rs | 22 ++++++++-------------- src/cli/error.rs | 4 ++-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index 2c910505..fe29368a 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -396,28 +396,22 @@ async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhErr "Current user is {} rather than root; running Nix command using sudo", whoami::username() ); - tracing::debug!( - "Running: sudo nix --extra-experimental-features 'nix-command-flakes' {}", - args.join(" ") - ); let mut cmd = tokio::process::Command::new("sudo"); cmd.arg("nix"); cmd } else { - tracing::debug!( - "Running: nix --extra-experimental-features 'nix-command-flakes' {}", - args.join(" ") - ); - tokio::process::Command::new("nix") }; + cmd.args(["--extra-experimental-features", "nix-command flakes"]); + cmd.args(args); + cmd.stdout(Stdio::inherit()); + cmd.stderr(Stdio::inherit()); + let cmd_str = format!("{:?}", cmd); + tracing::debug!("Running: {:?}", cmd_str); + let output = cmd - .args(["--extra-experimental-features", "nix-command flakes"]) - .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) .spawn() .wrap_err("failed to spawn Nix command")? .wait_with_output() @@ -427,7 +421,7 @@ async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhErr if output.status.success() { Ok(()) } else { - Err(FhError::FailedNixCommand) + Err(FhError::FailedNixCommand(cmd_str)) } } diff --git a/src/cli/error.rs b/src/cli/error.rs index 6286654d..94bd0c67 100644 --- a/src/cli/error.rs +++ b/src/cli/error.rs @@ -1,7 +1,7 @@ #[derive(Debug, thiserror::Error)] pub(crate) enum FhError { - #[error("Nix command failed; check prior Nix output for details")] - FailedNixCommand, + #[error("Nix command `{0}` failed; check prior Nix output for details")] + FailedNixCommand(String), #[error("file error: {0}")] Filesystem(#[from] std::io::Error), From 3361f2e0a8da84ad3f739b21513bccbad7c0a9e7 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 30 Jul 2024 10:50:57 -0400 Subject: [PATCH 68/69] Comment about the checks on execution --- src/cli/cmd/apply/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cli/cmd/apply/mod.rs b/src/cli/cmd/apply/mod.rs index cbb65960..ada2951b 100644 --- a/src/cli/cmd/apply/mod.rs +++ b/src/cli/cmd/apply/mod.rs @@ -154,8 +154,10 @@ async fn run_script( ); if let Ok(script_path_metadata) = tokio::fs::metadata(&script_path).await { + // The expected application script exists let permissions = script_path_metadata.permissions(); if permissions.mode() & 0o111 != 0 { + // The expected application script is executable if let Some(action) = &action { tracing::info!("{} {}", &script_path.display().to_string(), action); } else { From c0060a4939f3127869cc808a3f6da085b026713f Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 30 Jul 2024 10:55:33 -0400 Subject: [PATCH 69/69] Update src/cli/cmd/mod.rs Co-authored-by: Cole Helbling --- src/cli/cmd/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs index fe29368a..8863317a 100644 --- a/src/cli/cmd/mod.rs +++ b/src/cli/cmd/mod.rs @@ -408,7 +408,7 @@ async fn nix_command(args: &[&str], sudo_if_necessary: bool) -> Result<(), FhErr cmd.args(args); cmd.stdout(Stdio::inherit()); cmd.stderr(Stdio::inherit()); - let cmd_str = format!("{:?}", cmd); + let cmd_str = format!("{:?}", cmd.as_std()); tracing::debug!("Running: {:?}", cmd_str); let output = cmd