From 7cbb6298f14768e844318bf32652bf084d8cfb30 Mon Sep 17 00:00:00 2001 From: Matthew Kenigsberg Date: Wed, 22 Nov 2023 00:14:43 -0500 Subject: [PATCH] Setup channels Add a SetupChannels field to ConfigureNix that: - Creates $ROOT_HOME/.nix-channels - Runs `nix-channels --update nixpkgs` Setting up channels can be disabled with a command line flag or NIX_INSTALLER_NO_CHANNEL_ADD. --- .github/workflows/kiteless.yml | 16 --- src/action/base/setup_default_profile.rs | 71 +----------- src/action/common/configure_nix.rs | 128 ++++++++++++++++++++- src/action/common/mod.rs | 2 + src/action/common/setup_channels.rs | 137 +++++++++++++++++++++++ src/settings.rs | 19 ++++ tests/plan.rs | 3 + 7 files changed, 289 insertions(+), 87 deletions(-) create mode 100644 src/action/common/setup_channels.rs diff --git a/.github/workflows/kiteless.yml b/.github/workflows/kiteless.yml index 2e67a423..69f1fe05 100644 --- a/.github/workflows/kiteless.yml +++ b/.github/workflows/kiteless.yml @@ -140,10 +140,6 @@ jobs: - name: Test `nix` with `$GITHUB_PATH` if: success() || failure() run: | - # TODO this should be part of the installation process - echo "https://nixos.org/channels/nixpkgs-unstable nixpkgs" > "$HOME/.nix-channels" - nix-channel --update nixpkgs - nix-shell -p hello --command hello nix-env --install hello hello @@ -251,10 +247,6 @@ jobs: - name: Test `nix` with `$GITHUB_PATH` if: success() || failure() run: | - # TODO this should be part of the installation process - sudo -i sh -c 'echo "https://nixos.org/channels/nixpkgs-unstable nixpkgs" > "$HOME/.nix-channels"' - sudo -i nix-channel --update nixpkgs - sudo -i nix-shell -p hello --command hello sudo -i nix-env --install hello sudo -i hello @@ -370,10 +362,6 @@ jobs: - name: Test `nix` with `$GITHUB_PATH` if: success() || failure() run: | - # TODO this should be part of the installation process - echo "https://nixos.org/channels/nixpkgs-unstable nixpkgs" > "$HOME/.nix-channels" - nix-channel --update nixpkgs - nix-shell -p hello --command hello nix-env --install hello hello @@ -487,10 +475,6 @@ jobs: - name: Test `nix` with `$GITHUB_PATH` if: success() || failure() run: | - # TODO this should be part of the installation process - echo "https://nixos.org/channels/nixpkgs-unstable nixpkgs" > "$HOME/.nix-channels" - nix-channel --update nixpkgs - nix-shell -p hello --command hello nix-env --install hello hello diff --git a/src/action/base/setup_default_profile.rs b/src/action/base/setup_default_profile.rs index 6a67cd3d..f8ba75f4 100644 --- a/src/action/base/setup_default_profile.rs +++ b/src/action/base/setup_default_profile.rs @@ -1,12 +1,10 @@ use std::path::PathBuf; use crate::{ - action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}, + action::{common::ConfigureNix, ActionError, ActionErrorKind, ActionTag, StatefulAction}, execute_command, set_env, }; -use glob::glob; - use tokio::{io::AsyncWriteExt, process::Command}; use tracing::{span, Span}; @@ -51,63 +49,8 @@ impl Action for SetupDefaultProfile { #[tracing::instrument(level = "debug", skip_all)] async fn execute(&mut self) -> Result<(), ActionError> { - // Find an `nix` package - let nix_pkg_glob = format!("{}/nix-*/store/*-nix-*.*.*", self.unpacked_path.display()); - let mut found_nix_pkg = None; - for entry in glob(&nix_pkg_glob).map_err(Self::error)? { - match entry { - Ok(path) => { - // If we are curing, the user may have multiple of these installed - if let Some(_existing) = found_nix_pkg { - return Err(Self::error(SetupDefaultProfileError::MultipleNixPackages))?; - } else { - found_nix_pkg = Some(path); - } - break; - }, - Err(_) => continue, /* Ignore it */ - }; - } - let nix_pkg = if let Some(nix_pkg) = found_nix_pkg { - tokio::fs::read_link(&nix_pkg) - .await - .map_err(|e| ActionErrorKind::ReadSymlink(nix_pkg, e)) - .map_err(Self::error)? - } else { - return Err(Self::error(SetupDefaultProfileError::NoNix)); - }; - - // Find an `nss-cacert` package, add it too. - let nss_ca_cert_pkg_glob = format!( - "{}/nix-*/store/*-nss-cacert-*.*", - self.unpacked_path.display() - ); - let mut found_nss_ca_cert_pkg = None; - for entry in glob(&nss_ca_cert_pkg_glob).map_err(Self::error)? { - match entry { - Ok(path) => { - // If we are curing, the user may have multiple of these installed - if let Some(_existing) = found_nss_ca_cert_pkg { - return Err(Self::error( - SetupDefaultProfileError::MultipleNssCaCertPackages, - ))?; - } else { - found_nss_ca_cert_pkg = Some(path); - } - break; - }, - Err(_) => continue, /* Ignore it */ - }; - } - let nss_ca_cert_pkg = if let Some(nss_ca_cert_pkg) = found_nss_ca_cert_pkg { - tokio::fs::read_link(&nss_ca_cert_pkg) - .await - .map_err(|e| ActionErrorKind::ReadSymlink(nss_ca_cert_pkg, e)) - .map_err(Self::error)? - } else { - return Err(Self::error(SetupDefaultProfileError::NoNssCacert)); - }; - + let (nix_pkg, nss_ca_cert_pkg) = + ConfigureNix::find_nix_and_ca_cert(&self.unpacked_path).await?; let found_nix_paths = glob::glob(&format!("{}/nix-*", self.unpacked_path.display())) .map_err(Self::error)? .collect::, _>>() @@ -236,16 +179,8 @@ impl Action for SetupDefaultProfile { #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum SetupDefaultProfileError { - #[error("Unarchived Nix store did not appear to include a `nss-cacert` location")] - NoNssCacert, - #[error("Unarchived Nix store did not appear to include a `nix` location")] - NoNix, #[error("No root home found to place channel configuration in")] NoRootHome, - #[error("Unarchived Nix store appears to contain multiple `nss-ca-cert` packages, cannot select one")] - MultipleNssCaCertPackages, - #[error("Unarchived Nix store appears to contain multiple `nix` packages, cannot select one")] - MultipleNixPackages, } impl From for ActionErrorKind { diff --git a/src/action/common/configure_nix.rs b/src/action/common/configure_nix.rs index 9088c22e..13f396fc 100644 --- a/src/action/common/configure_nix.rs +++ b/src/action/common/configure_nix.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::{ action::{ @@ -9,6 +9,9 @@ use crate::{ planner::ShellProfileLocations, settings::{CommonSettings, SCRATCH_DIR}, }; +use glob::glob; + +use crate::action::common::SetupChannels; use tracing::{span, Instrument, Span}; @@ -20,6 +23,7 @@ pub struct ConfigureNix { setup_default_profile: StatefulAction, configure_shell_profile: Option>, place_nix_configuration: StatefulAction, + setup_channels: Option>, } impl ConfigureNix { @@ -51,13 +55,83 @@ impl ConfigureNix { .await .map_err(Self::error)?; + let setup_channels = if settings.no_channel_add { + None + } else { + Some( + SetupChannels::plan(PathBuf::from(SCRATCH_DIR)) + .await + .map_err(Self::error)?, + ) + }; + Ok(Self { place_nix_configuration, setup_default_profile, configure_shell_profile, + setup_channels, } .into()) } + + pub async fn find_nix_and_ca_cert( + unpacked_path: &Path, + ) -> Result<(PathBuf, PathBuf), ActionError> { + // Find a `nix` package + let nix_pkg_glob = format!("{}/nix-*/store/*-nix-*.*.*", unpacked_path.display()); + let mut found_nix_pkg = None; + for entry in glob(&nix_pkg_glob).map_err(Self::error)? { + match entry { + Ok(path) => { + // If we are curing, the user may have multiple of these installed + if let Some(_existing) = found_nix_pkg { + return Err(Self::error(ConfigureNixError::MultipleNixPackages))?; + } else { + found_nix_pkg = Some(path); + } + break; + }, + Err(_) => continue, /* Ignore it */ + }; + } + let nix_pkg = if let Some(nix_pkg) = found_nix_pkg { + tokio::fs::read_link(&nix_pkg) + .await + .map_err(|e| ActionErrorKind::ReadSymlink(nix_pkg, e)) + .map_err(Self::error)? + } else { + return Err(Self::error(ConfigureNixError::NoNix)); + }; + + // Find an `nss-cacert` package + let nss_ca_cert_pkg_glob = + format!("{}/nix-*/store/*-nss-cacert-*.*", unpacked_path.display()); + let mut found_nss_ca_cert_pkg = None; + for entry in glob(&nss_ca_cert_pkg_glob).map_err(Self::error)? { + match entry { + Ok(path) => { + // If we are curing, the user may have multiple of these installed + if let Some(_existing) = found_nss_ca_cert_pkg { + return Err(Self::error(ConfigureNixError::MultipleNssCaCertPackages))?; + } else { + found_nss_ca_cert_pkg = Some(path); + } + break; + }, + Err(_) => continue, /* Ignore it */ + }; + } + let nss_ca_cert_pkg = if let Some(nss_ca_cert_pkg) = found_nss_ca_cert_pkg { + tokio::fs::read_link(&nss_ca_cert_pkg) + .await + .map_err(|e| ActionErrorKind::ReadSymlink(nss_ca_cert_pkg, e)) + .map_err(Self::error)? + } else { + return Err(Self::error(ConfigureNixError::NoNssCacert)); + }; + + Ok((nix_pkg, nss_ca_cert_pkg)) + } } #[async_trait::async_trait] @@ -79,10 +153,14 @@ impl Action for ConfigureNix { setup_default_profile, place_nix_configuration, configure_shell_profile, + setup_channels, } = &self; let mut buf = setup_default_profile.describe_execute(); buf.append(&mut place_nix_configuration.describe_execute()); + if let Some(setup_channels) = setup_channels { + buf.append(&mut setup_channels.describe_execute()); + } if let Some(configure_shell_profile) = configure_shell_profile { buf.append(&mut configure_shell_profile.describe_execute()); } @@ -95,10 +173,15 @@ impl Action for ConfigureNix { setup_default_profile, place_nix_configuration, configure_shell_profile, + setup_channels, } = self; + let setup_default_profile_span = tracing::Span::current().clone(); + let setup_channels_span = setup_channels + .is_some() + .then(|| setup_default_profile_span.clone()); + if let Some(configure_shell_profile) = configure_shell_profile { - let setup_default_profile_span = tracing::Span::current().clone(); let (place_nix_configuration_span, configure_shell_profile_span) = ( setup_default_profile_span.clone(), setup_default_profile_span.clone(), @@ -127,7 +210,6 @@ impl Action for ConfigureNix { }, )?; } else { - let setup_default_profile_span = tracing::Span::current().clone(); let place_nix_configuration_span = setup_default_profile_span.clone(); tokio::try_join!( async move { @@ -147,6 +229,18 @@ impl Action for ConfigureNix { )?; }; + // Keep setup_channels outside try_join to avoid the error: + // SQLite database '/nix/var/nix/db/db.sqlite' is busy + // Presumably there are conflicts with nix commands run in + // setup_default_profile. + if let Some(setup_channels) = setup_channels { + setup_channels + .try_execute() + .instrument(setup_channels_span.unwrap()) + .await + .map_err(Self::error)? + } + Ok(()) } @@ -155,6 +249,7 @@ impl Action for ConfigureNix { setup_default_profile, place_nix_configuration, configure_shell_profile, + setup_channels, } = &self; let mut buf = Vec::default(); @@ -163,6 +258,9 @@ impl Action for ConfigureNix { } buf.append(&mut place_nix_configuration.describe_revert()); buf.append(&mut setup_default_profile.describe_revert()); + if let Some(setup_channels) = setup_channels { + buf.append(&mut setup_channels.describe_revert()); + } buf } @@ -181,6 +279,11 @@ impl Action for ConfigureNix { if let Err(err) = self.setup_default_profile.try_revert().await { errors.push(err); } + if let Some(setup_channels) = &mut self.setup_channels { + if let Err(err) = setup_channels.try_revert().await { + errors.push(err); + } + } if errors.is_empty() { Ok(()) @@ -194,3 +297,22 @@ impl Action for ConfigureNix { } } } + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum ConfigureNixError { + #[error("Unarchived Nix store did not appear to include a `nss-cacert` location")] + NoNssCacert, + #[error("Unarchived Nix store did not appear to include a `nix` location")] + NoNix, + #[error("Unarchived Nix store appears to contain multiple `nss-ca-cert` packages, cannot select one")] + MultipleNssCaCertPackages, + #[error("Unarchived Nix store appears to contain multiple `nix` packages, cannot select one")] + MultipleNixPackages, +} + +impl From for ActionErrorKind { + fn from(val: ConfigureNixError) -> Self { + ActionErrorKind::Custom(Box::new(val)) + } +} diff --git a/src/action/common/mod.rs b/src/action/common/mod.rs index 9ca61983..1ba5dcba 100644 --- a/src/action/common/mod.rs +++ b/src/action/common/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod create_users_and_groups; pub(crate) mod delete_users; pub(crate) mod place_nix_configuration; pub(crate) mod provision_nix; +pub(crate) mod setup_channels; pub use configure_init_service::{ConfigureInitService, ConfigureNixDaemonServiceError}; pub use configure_nix::ConfigureNix; @@ -17,3 +18,4 @@ pub use create_users_and_groups::CreateUsersAndGroups; pub use delete_users::DeleteUsersInGroup; pub use place_nix_configuration::PlaceNixConfiguration; pub use provision_nix::ProvisionNix; +pub use setup_channels::SetupChannels; diff --git a/src/action/common/setup_channels.rs b/src/action/common/setup_channels.rs new file mode 100644 index 00000000..7fccdbad --- /dev/null +++ b/src/action/common/setup_channels.rs @@ -0,0 +1,137 @@ +use std::path::PathBuf; + +use crate::{ + action::{ActionError, ActionErrorKind, ActionTag, StatefulAction}, + execute_command, +}; + +use tokio::process::Command; +use tracing::{span, Span}; + +use crate::action::{Action, ActionDescription}; + +use crate::action::base::CreateFile; + +use super::ConfigureNix; + +/** +Setup the default system channel with nixpkgs-unstable. + */ +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct SetupChannels { + create_file: StatefulAction, + unpacked_path: PathBuf, +} + +impl SetupChannels { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan(unpacked_path: PathBuf) -> Result, ActionError> { + let create_file = CreateFile::plan( + dirs::home_dir() + .ok_or_else(|| Self::error(SetupChannelsError::NoRootHome))? + .join(".nix-channels"), + None, + None, + 0o664, + "https://nixos.org/channels/nixpkgs-unstable nixpkgs\n".to_string(), + false, + ) + .await?; + Ok(Self { + create_file, + unpacked_path, + } + .into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "setup_channels")] +impl Action for SetupChannels { + fn action_tag() -> ActionTag { + ActionTag("setup_channels") + } + fn tracing_synopsis(&self) -> String { + "Setup the default system channel".to_string() + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "setup_channels", + unpacked_path = %self.unpacked_path.display(), + ) + } + + fn execute_description(&self) -> Vec { + let mut explanation = vec![]; + + if let Some(val) = self.create_file.describe_execute().first() { + explanation.push(val.description.clone()) + } + + explanation.push("Run `nix-channel --update nixpkgs`".to_string()); + + vec![ActionDescription::new(self.tracing_synopsis(), explanation)] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + // Place channel configuration + self.create_file.try_execute().await?; + + let (nix_pkg, nss_ca_cert_pkg) = + ConfigureNix::find_nix_and_ca_cert(&self.unpacked_path).await?; + // Update nixpkgs channel + execute_command( + Command::new(nix_pkg.join("bin/nix-channel")) + .process_group(0) + .arg("--update") + .arg("nixpkgs") + .stdin(std::process::Stdio::null()) + .env( + "HOME", + dirs::home_dir().ok_or_else(|| Self::error(SetupChannelsError::NoRootHome))?, + ) + .env( + "NIX_SSL_CERT_FILE", + nss_ca_cert_pkg.join("etc/ssl/certs/ca-bundle.crt"), + ), /* This is apparently load bearing... */ + ) + .await + .map_err(Self::error)?; + + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new( + "Remove system channel configuration".to_string(), + vec![], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + self.create_file.try_revert().await?; + + // We could try to rollback + // /nix/var/nix/profiles/per-user/root/channels, but that will happen + // anyways when /nix gets cleaned up. + + Ok(()) + } +} + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum SetupChannelsError { + #[error("No root home found to place channel configuration in")] + NoRootHome, +} + +impl From for ActionErrorKind { + fn from(val: SetupChannelsError) -> Self { + ActionErrorKind::Custom(Box::new(val)) + } +} diff --git a/src/settings.rs b/src/settings.rs index 31529c61..c00d7c4d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -245,6 +245,18 @@ pub struct CommonSettings { default_value = "https://install.determinate.systems/nix/diagnostic" )] pub diagnostic_endpoint: Option, + + /// Whether to setup system channels + #[cfg_attr( + feature = "cli", + clap( + long, + default_value = "false", + env = "NIX_INSTALLER_NO_CHANNEL_ADD", + global = true + ) + )] + pub no_channel_add: bool, } impl CommonSettings { @@ -317,6 +329,7 @@ impl CommonSettings { diagnostic_attribution: None, #[cfg(feature = "diagnostics")] diagnostic_endpoint: Some("https://install.determinate.systems/nix/diagnostic".into()), + no_channel_add: false, }) } @@ -338,6 +351,7 @@ impl CommonSettings { diagnostic_attribution: _, #[cfg(feature = "diagnostics")] diagnostic_endpoint, + no_channel_add, } = self; let mut map = HashMap::default(); @@ -380,6 +394,11 @@ impl CommonSettings { serde_json::to_value(diagnostic_endpoint)?, ); + map.insert( + "no_channel_add".into(), + serde_json::to_value(no_channel_add)?, + ); + Ok(map) } } diff --git a/tests/plan.rs b/tests/plan.rs index e34ad0f7..a962d8c5 100644 --- a/tests/plan.rs +++ b/tests/plan.rs @@ -10,6 +10,7 @@ const MACOS: &str = include_str!("./fixtures/macos/macos.json"); // Ensure existing plans still parse // If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans. #[cfg(target_os = "linux")] +#[ignore] #[test] fn plan_compat_linux() -> eyre::Result<()> { let _: InstallPlan = serde_json::from_str(LINUX)?; @@ -19,6 +20,7 @@ fn plan_compat_linux() -> eyre::Result<()> { // Ensure existing plans still parse // If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans. #[cfg(target_os = "linux")] +#[ignore] #[test] fn plan_compat_steam_deck() -> eyre::Result<()> { let _: InstallPlan = serde_json::from_str(STEAM_DECK)?; @@ -28,6 +30,7 @@ fn plan_compat_steam_deck() -> eyre::Result<()> { // Ensure existing plans still parse // If this breaks and you need to update the fixture, disable these tests, bump `nix_installer` to a new version, and update the plans. #[cfg(target_os = "macos")] +#[ignore] #[test] fn plan_compat_macos() -> eyre::Result<()> { let _: InstallPlan = serde_json::from_str(MACOS)?;