From a3231d4b02255c33f3cf62134644670ca4f79683 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Tue, 21 Nov 2023 20:50:13 -0500 Subject: [PATCH 01/17] Create a Rust helper for defining environment variables --- src/action/common/configure_shell_profile.rs | 70 +++- src/action/common/profiles/profile.fish | 6 + src/action/common/profiles/profile.sh | 13 + src/action/macos/configure_remote_building.rs | 12 +- src/cli/mod.rs | 1 + src/cli/subcommand/export.rs | 317 ++++++++++++++++++ src/cli/subcommand/install.rs | 14 +- src/cli/subcommand/mod.rs | 3 + src/plan.rs | 17 +- 9 files changed, 414 insertions(+), 39 deletions(-) create mode 100644 src/action/common/profiles/profile.fish create mode 100644 src/action/common/profiles/profile.sh create mode 100644 src/cli/subcommand/export.rs diff --git a/src/action/common/configure_shell_profile.rs b/src/action/common/configure_shell_profile.rs index d6fc90017..6d80568d1 100644 --- a/src/action/common/configure_shell_profile.rs +++ b/src/action/common/configure_shell_profile.rs @@ -9,8 +9,8 @@ use std::path::{Path, PathBuf}; use tokio::task::JoinSet; use tracing::{span, Instrument, Span}; -const PROFILE_NIX_FILE_SHELL: &str = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"; -const PROFILE_NIX_FILE_FISH: &str = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.fish"; +pub(crate) const PROFILE_NIX_FILE_SHELL: &str = "/nix/nix-installer.d/profile.sh"; +pub(crate) const PROFILE_NIX_FILE_FISH: &str = "/nix/nix-installer.d/profile.fish"; /** Configure any detected shell profiles to include Nix support @@ -31,14 +31,47 @@ impl ConfigureShellProfile { let mut create_directories = Vec::default(); let shell_buf = format!( - "\n\ - # Nix\n\ - if [ -e '{PROFILE_NIX_FILE_SHELL}' ]; then\n\ - {inde}. '{PROFILE_NIX_FILE_SHELL}'\n\ - fi\n\ - # End Nix\n - \n", - inde = " ", // indent + r#" + +# Begin Nix +if [ -f '{PROFILE_NIX_FILE_SHELL}' ]; then + . '{PROFILE_NIX_FILE_SHELL}' +fi +# End Nix + +"# + ); + + create_directories.push( + CreateDirectory::plan("/nix/nix-installer.d", None, None, 0o0755, false) + .await + .map_err(Self::error)?, + ); + + create_or_insert_files.push( + CreateOrInsertIntoFile::plan( + PROFILE_NIX_FILE_SHELL, + None, + None, + 0o644, + include_str!("./profiles/profile.sh").to_string(), + create_or_insert_into_file::Position::Beginning, + ) + .await + .map_err(Self::error)?, + ); + + create_or_insert_files.push( + CreateOrInsertIntoFile::plan( + PROFILE_NIX_FILE_FISH, + None, + None, + 0o644, + include_str!("./profiles/profile.fish").to_string(), + create_or_insert_into_file::Position::Beginning, + ) + .await + .map_err(Self::error)?, ); for profile_target in locations.bash.iter().chain(locations.zsh.iter()) { @@ -67,14 +100,15 @@ impl ConfigureShellProfile { } let fish_buf = format!( - "\n\ - # Nix\n\ - if test -e '{PROFILE_NIX_FILE_FISH}'\n\ - {inde}. '{PROFILE_NIX_FILE_FISH}'\n\ - end\n\ - # End Nix\n\ - \n", - inde = " ", // indent + r#" + +# Begin Nix +if [ -f {PROFILE_NIX_FILE_FISH} ]; then + . {PROFILE_NIX_FILE_FISH} +fi +# End Nix + +"# ); for fish_prefix in &locations.fish.confd_prefixes { diff --git a/src/action/common/profiles/profile.fish b/src/action/common/profiles/profile.fish new file mode 100644 index 000000000..1dd776f74 --- /dev/null +++ b/src/action/common/profiles/profile.fish @@ -0,0 +1,6 @@ +if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then + /nix/nix-installer export --format null-separated \ + | while read --null key + read --export --null "$key" + end +fi diff --git a/src/action/common/profiles/profile.sh b/src/action/common/profiles/profile.sh new file mode 100644 index 000000000..852ec5d0d --- /dev/null +++ b/src/action/common/profiles/profile.sh @@ -0,0 +1,13 @@ +# shellcheck shell=sh +if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then + NIX_INSTALLER_EXPORT_DATA=$(/nix/nix-installer export --verbose --verbose --format space-newline-separated); + while read -r nix_installer_export_key nix_installer_export_value; do + export "$nix_installer_export_key=$nix_installer_export_value"; + done <&2 +fi diff --git a/src/action/macos/configure_remote_building.rs b/src/action/macos/configure_remote_building.rs index 3d9e03690..38e6d37e6 100644 --- a/src/action/macos/configure_remote_building.rs +++ b/src/action/macos/configure_remote_building.rs @@ -1,10 +1,10 @@ -use crate::action::base::{create_or_insert_into_file, CreateOrInsertIntoFile}; -use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; - use std::path::Path; + use tracing::{span, Instrument, Span}; -const PROFILE_NIX_FILE_SHELL: &str = "/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"; +use crate::action::base::{create_or_insert_into_file, CreateOrInsertIntoFile}; +use crate::action::common::configure_shell_profile::PROFILE_NIX_FILE_SHELL; +use crate::action::{Action, ActionDescription, ActionError, ActionTag, StatefulAction}; /** Configure macOS's zshenv to load the Nix environment when ForceCommand is used. @@ -20,12 +20,14 @@ impl ConfigureRemoteBuilding { pub async fn plan() -> Result, ActionError> { let shell_buf = format!( r#" + # Set up Nix only on SSH connections # See: https://github.com/DeterminateSystems/nix-installer/pull/714 -if [ -e '{PROFILE_NIX_FILE_SHELL}' ] && [ -n "${{SSH_CONNECTION}}" ] && [ "${{SHLVL}}" -eq 1 ]; then +if [ -n "${{SSH_CONNECTION}}" ] && [ "${{SHLVL}}" -eq 1 ] && [ -f '{PROFILE_NIX_FILE_SHELL}' ]; then . '{PROFILE_NIX_FILE_SHELL}' fi # End Nix + "# ); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 37ded7302..9c0df7741 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -49,6 +49,7 @@ impl CommandExecute for NixInstallerCli { NixInstallerSubcommand::Install(install) => install.execute().await, NixInstallerSubcommand::Repair(restore_shell) => restore_shell.execute().await, NixInstallerSubcommand::Uninstall(revert) => revert.execute().await, + NixInstallerSubcommand::Export(export) => export.execute().await, } } } diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs new file mode 100644 index 000000000..5f56372fd --- /dev/null +++ b/src/cli/subcommand/export.rs @@ -0,0 +1,317 @@ +use std::collections::HashMap; +use std::env; +use std::ffi::{OsStr, OsString}; +use std::io::{stdout, BufWriter, Write}; +use std::os::unix::ffi::OsStringExt; +use std::path::PathBuf; +use std::process::ExitCode; + +use crate::cli::CommandExecute; +use clap::Parser; + +use tracing::{debug, warn}; + +const LOCAL_STATE_DIR: &str = "/nix/var"; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("The HOME environment variable is not set.")] + HomeNotSet, + + #[error("__ETC_PROFILE_NIX_SOURCED is set, indicating the relevant environment variables have already been set.")] + AlreadyRun, + + #[error("Some of the paths for XDG_DATA_DIR are not valid, due to an illegal character, like a space or colon.")] + InvalidXdgDataDirs(Vec), + + #[error("Some of the paths for PATH are not valid, due to an illegal character, like a space or colon.")] + InvalidPathDirs(Vec), + + #[error("Some of the paths for MANPATH are not valid, due to an illegal character, like a space or colon.")] + InvalidManPathDirs(Vec), +} + +/** +Emit all the environment variables that should be set to use Nix. + +In POSIX shell: + + nix-installer export --format space-newline-separated \ + | while IFS=' ' read key value; do + export "$key"="$value" + done + +In Bash / non-POSIX shell: + + nix-installer export --format null-separated \ + | while IFS= read -r -d $'\0' key && IFS= read -r -d $'\0' value; do + export "$key"="$value" + done + +In Fish shell: + + nix-installer export --format null-separated \ + | while read --null key + read --export --null "$key" + end + + +Safety note: environment variables and values can contain any bytes except +for a null byte. This includes newlines and spaces, which requires careful +handling. + +In `space-newline-separated` mode, `nix-installer` guarantees it will: + + * only emit keys that are alpha-numeric with underscores, + * only emit values without newlines + +and will refuse to emit any output to stdout if the variables and values +would violate these safety rules. + +In `null-separated` mode, `nix-installer` emits data in this format: + + KEYNAME\0VALUE\0KEYNAME\0VALUE\0 + +*/ +#[derive(Debug, Parser)] +#[command(args_conflicts_with_subcommands = true)] +pub struct Export { + #[clap(long)] + format: ExportFormat, +} + +#[derive(Debug, Clone, PartialEq, Eq, clap::ValueEnum)] +enum ExportFormat { + NullSeparated, + SpaceNewlineSeparated, +} + +#[async_trait::async_trait] +impl CommandExecute for Export { + #[tracing::instrument(level = "trace", skip_all)] + async fn execute(self) -> eyre::Result { + let env = match calculate_environment() { + Err(Error::AlreadyRun) => { + debug!("Already set the environment vars, not doing it again."); + return Ok(ExitCode::SUCCESS); + }, + Err(e) => { + tracing::info!( + "Error calculating the environment variables required to enable Nix: {:?}", + e + ); + return Err(e.into()); + }, + + Ok(env) => env, + }; + + let mut out = BufWriter::new(stdout()); + + match self.format { + ExportFormat::NullSeparated => { + debug!("Emitting null separated fields"); + + for (key, value) in env.into_iter() { + out.write_all(key.as_bytes())?; + out.write_all(&[b'\0'])?; + out.write_all(&value.into_vec())?; + out.write_all(&[b'\0'])?; + } + }, + ExportFormat::SpaceNewlineSeparated => { + debug!("Emitting space/newline separated fields"); + + let mut validated_envs = HashMap::new(); + for (key, value) in env.into_iter() { + if !key.chars().all(|char| { + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" + .contains(char) + }) { + warn!("Key {} has an invalid character that isn't a-zA-Z_", key); + return Ok(ExitCode::FAILURE); + } + + let value_bytes = value.into_vec(); + + if value_bytes.contains(&b'\n') { + warn!( + "Value for key {} has an a newline, which is prohibited", + key + ); + return Ok(ExitCode::FAILURE); + } + + validated_envs.insert(key, value_bytes); + } + + for (key, value) in validated_envs.into_iter() { + out.write_all(key.as_bytes())?; + out.write_all(b" ")?; + out.write_all(&value)?; + out.write_all(b"\n")?; + } + }, + } + + Ok(ExitCode::SUCCESS) + } +} + +pub fn calculate_environment() -> Result, Error> { + let mut envs: HashMap<&'static str, OsString> = HashMap::new(); + + // Don't export variables twice. + // @PORT-NOTE nix-profile-daemon.sh.in and nix-profile-daemon.fish.in implemented + // this behavior, but it was not implemented in nix-profile.sh.in and nix-profile.fish.in + // even though I believe it is desirable in both cases. + if env::var_os("__ETC_PROFILE_NIX_SOURCED").is_some() { + return Err(Error::AlreadyRun); + } + + // @PORT-NOTE nix-profile.sh.in and nix-profile.fish.in check HOME and USER are set, + // but not nix-profile-daemon.sh.in and nix-profile-daemon.fish.in. + // The -daemon variants appear to just assume the values are set, which is probably + // not safe, so we check it in all cases. + let home = if let Some(home) = env::var_os("HOME") { + PathBuf::from(home) + } else { + return Err(Error::HomeNotSet); + }; + + envs.insert("__ETC_PROFILE_NIX_SOURCED", "1".into()); + + let nix_link: PathBuf = { + let legacy_location = home.join(".nix-profile"); + let xdg_location = env::var_os("XDG_STATE_HOME") + .map(PathBuf::from) + .unwrap_or_else(|| home.join(".local/state")) + .join("nix/profile"); + + if xdg_location.is_symlink() { + // In the future we'll prefer the legacy location, but + // evidently this is the intended order preference: + // https://github.com/NixOS/nix/commit/2b801d6e3c3a3be6feb6fa2d9a0b009fa9261b45 + xdg_location + } else { + legacy_location + } + }; + + let nix_profiles = &[ + PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default"), + nix_link.clone(), + ]; + envs.insert( + "NIX_PROFILES", + nix_profiles + .iter() + .map(|path| path.as_os_str()) + .collect::>() + .join(OsStr::new(" ")), + ); + + { + let xdg_data_dirs: Vec = env::var_os("XDG_DATA_DIRS") + .as_ref() + .map(|s| env::split_paths(s)) + .map(|paths| paths.collect::>()) + .unwrap_or_else(|| { + vec![ + PathBuf::from("/usr/local/share"), + PathBuf::from("/usr/share"), + ] + }); + + let nix_xdg_data_dirs = vec![ + nix_link.join("share"), + PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/share"), + ]; + + let extended_xdg_dirs: Vec<&PathBuf> = xdg_data_dirs + .iter() + .chain(nix_xdg_data_dirs.iter()) + .collect::>(); + + if let Ok(dirs) = env::join_paths(&extended_xdg_dirs) { + envs.insert("XDG_DATA_DIRS", dirs); + } else { + return Err(Error::InvalidXdgDataDirs( + extended_xdg_dirs.into_iter().cloned().collect(), + )); + } + } + + if env::var_os("NIX_SSL_CERT_FILE").is_none() { + let mut candidate_locations = vec![ + PathBuf::from("/etc/ssl/certs/ca-certificates.crt"), // NixOS, Ubuntu, Debian, Gentoo, Arch + PathBuf::from("/etc/ssl/ca-bundle.pem"), // openSUSE Tumbleweed + PathBuf::from("/etc/ssl/certs/ca-bundle.crt"), // Old NixOS + PathBuf::from("/etc/pki/tls/certs/ca-bundle.crt"), // Fedora, CentOS + nix_link.join("etc/ssl/certs/ca-bundle.crt"), // fall back to cacert in Nix profile + nix_link.join("etc/ca-bundle.crt"), // old cacert in Nix profile + ]; + + // Add the various profiles, preferring the last profile, ie: most global profile (matches upstream behavior) + candidate_locations.extend(nix_profiles.iter().rev().cloned()); + + if let Some(cert) = candidate_locations.iter().find(|path| path.is_file()) { + envs.insert("NIX_SSL_CERT_FILE", cert.into()); + } else { + warn!( + "Could not identify any SSL certificates out of these candidates: {:?}", + candidate_locations + ) + } + }; + + { + let nix_path_parts = [ + nix_link.join("bin"), + // Note: This is typically only used in single-user installs, but I chose to do it in both for simplicity. + // If there is good reason, we can make it fancier. + PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/bin"), + ]; + + let current_path = env::var_os("PATH").unwrap_or(OsString::from("")); + let existing_path_parts = env::split_paths(¤t_path); + + let all_paths: Vec = nix_path_parts + .into_iter() + .chain(existing_path_parts) + .collect(); + + if let Ok(dirs) = env::join_paths(&all_paths) { + envs.insert("PATH", dirs); + } else { + return Err(Error::InvalidPathDirs(all_paths)); + } + } + + { + let man_path_parts = [ + nix_link.join("share/man"), + // Note: This is typically only used in single-user installs, but I chose to do it in both for simplicity. + // If there is good reason, we can make it fancier. + PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/share/man"), + ]; + + let current_man_path = env::var_os("MANPATH").unwrap_or(OsString::from("")); + let existing_path_parts = env::split_paths(¤t_man_path); + + let all_paths: Vec = man_path_parts + .into_iter() + .chain(existing_path_parts) + .collect(); + + if let Ok(dirs) = env::join_paths(&all_paths) { + envs.insert("MANPATH", dirs); + } else { + return Err(Error::InvalidManPathDirs(all_paths)); + } + } + + debug!("Calculated environment: {:#?}", envs); + + Ok(envs) +} diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index a72c05b4a..f333363e9 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -1,5 +1,4 @@ use std::{ - os::unix::prelude::PermissionsExt, path::{Path, PathBuf}, process::ExitCode, }; @@ -12,7 +11,7 @@ use crate::{ signal_channel, CommandExecute, }, error::HasExpectedErrors, - plan::RECEIPT_LOCATION, + plan::{copy_self_to_nix_dir, RECEIPT_LOCATION}, planner::Planner, settings::CommonSettings, BuiltinPlanner, InstallPlan, NixInstallerError, @@ -313,9 +312,6 @@ impl CommandExecute for Install { } }, Ok(_) => { - copy_self_to_nix_dir() - .await - .wrap_err("Copying `nix-installer` to `/nix/nix-installer`")?; println!( "\ {success}\n\ @@ -335,11 +331,3 @@ impl CommandExecute for Install { Ok(ExitCode::SUCCESS) } } - -#[tracing::instrument(level = "debug")] -async fn copy_self_to_nix_dir() -> Result<(), std::io::Error> { - let path = std::env::current_exe()?; - tokio::fs::copy(path, "/nix/nix-installer").await?; - tokio::fs::set_permissions("/nix/nix-installer", PermissionsExt::from_mode(0o0755)).await?; - Ok(()) -} diff --git a/src/cli/subcommand/mod.rs b/src/cli/subcommand/mod.rs index ce8f42435..5431a90bc 100644 --- a/src/cli/subcommand/mod.rs +++ b/src/cli/subcommand/mod.rs @@ -8,6 +8,8 @@ mod uninstall; use uninstall::Uninstall; mod self_test; use self_test::SelfTest; +mod export; +pub use export::Export; #[allow(clippy::large_enum_variant)] #[derive(Debug, clap::Subcommand)] @@ -17,4 +19,5 @@ pub enum NixInstallerSubcommand { Uninstall(Uninstall), SelfTest(SelfTest), Plan(Plan), + Export(Export), } diff --git a/src/plan.rs b/src/plan.rs index 6543f0cbc..3e4ac6c39 100644 --- a/src/plan.rs +++ b/src/plan.rs @@ -1,13 +1,15 @@ +use std::os::unix::prelude::PermissionsExt; use std::{path::PathBuf, str::FromStr}; +use owo_colors::OwoColorize; +use semver::{Version, VersionReq}; +use tokio::sync::broadcast::Receiver; + use crate::{ action::{Action, ActionDescription, StatefulAction}, planner::{BuiltinPlanner, Planner}, NixInstallerError, }; -use owo_colors::OwoColorize; -use semver::{Version, VersionReq}; -use tokio::sync::broadcast::Receiver; pub const RECEIPT_LOCATION: &str = "/nix/receipt.json"; @@ -211,6 +213,7 @@ impl InstallPlan { } write_receipt(self.clone()).await?; + copy_self_to_nix_dir().await.ok(); if let Err(err) = crate::self_test::self_test() .await @@ -425,6 +428,14 @@ async fn write_receipt(plan: InstallPlan) -> Result<(), NixInstallerError> { Result::<(), NixInstallerError>::Ok(()) } +#[tracing::instrument(level = "debug")] +pub(crate) async fn copy_self_to_nix_dir() -> Result<(), std::io::Error> { + let path = std::env::current_exe()?; + tokio::fs::copy(path, "/nix/nix-installer").await?; + tokio::fs::set_permissions("/nix/nix-installer", PermissionsExt::from_mode(0o0755)).await?; + Ok(()) +} + pub fn current_version() -> Result { let nix_installer_version_str = env!("CARGO_PKG_VERSION"); Version::from_str(nix_installer_version_str).map_err(|e| { From be02735121a1296bd5ee4f18e19309d209964266 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 12:30:23 -0500 Subject: [PATCH 02/17] clean up note --- src/cli/subcommand/export.rs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index 5f56372fd..d537aed91 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -34,28 +34,6 @@ pub enum Error { /** Emit all the environment variables that should be set to use Nix. -In POSIX shell: - - nix-installer export --format space-newline-separated \ - | while IFS=' ' read key value; do - export "$key"="$value" - done - -In Bash / non-POSIX shell: - - nix-installer export --format null-separated \ - | while IFS= read -r -d $'\0' key && IFS= read -r -d $'\0' value; do - export "$key"="$value" - done - -In Fish shell: - - nix-installer export --format null-separated \ - | while read --null key - read --export --null "$key" - end - - Safety note: environment variables and values can contain any bytes except for a null byte. This includes newlines and spaces, which requires careful handling. From 848d223fc6d8e3bde6cdb178f1b4a3edcf5a75df Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 13:43:50 -0500 Subject: [PATCH 03/17] Don't turn an absent or empty path var into extra path values --- src/cli/subcommand/export.rs | 87 +++++++++++++++++------------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index d537aed91..cf996196d 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -136,6 +136,20 @@ impl CommandExecute for Export { } } +fn nonempty_var_os(key: &str) -> Option { + env::var_os(key).and_then(|val| if val.is_empty() { Some(val) } else { None }) +} + +fn env_path(key: &str) -> Option> { + let path = env::var_os(key)?; + + if path.is_empty() { + return Some(vec![]); + } + + Some(env::split_paths(&path).collect()) +} + pub fn calculate_environment() -> Result, Error> { let mut envs: HashMap<&'static str, OsString> = HashMap::new(); @@ -143,7 +157,7 @@ pub fn calculate_environment() -> Result, Error> // @PORT-NOTE nix-profile-daemon.sh.in and nix-profile-daemon.fish.in implemented // this behavior, but it was not implemented in nix-profile.sh.in and nix-profile.fish.in // even though I believe it is desirable in both cases. - if env::var_os("__ETC_PROFILE_NIX_SOURCED").is_some() { + if nonempty_var_os("__ETC_PROFILE_NIX_SOURCED") == Some("1".into()) { return Err(Error::AlreadyRun); } @@ -151,7 +165,7 @@ pub fn calculate_environment() -> Result, Error> // but not nix-profile-daemon.sh.in and nix-profile-daemon.fish.in. // The -daemon variants appear to just assume the values are set, which is probably // not safe, so we check it in all cases. - let home = if let Some(home) = env::var_os("HOME") { + let home = if let Some(home) = nonempty_var_os("HOME") { PathBuf::from(home) } else { return Err(Error::HomeNotSet); @@ -161,7 +175,7 @@ pub fn calculate_environment() -> Result, Error> let nix_link: PathBuf = { let legacy_location = home.join(".nix-profile"); - let xdg_location = env::var_os("XDG_STATE_HOME") + let xdg_location = nonempty_var_os("XDG_STATE_HOME") .map(PathBuf::from) .unwrap_or_else(|| home.join(".local/state")) .join("nix/profile"); @@ -190,37 +204,26 @@ pub fn calculate_environment() -> Result, Error> ); { - let xdg_data_dirs: Vec = env::var_os("XDG_DATA_DIRS") - .as_ref() - .map(|s| env::split_paths(s)) - .map(|paths| paths.collect::>()) - .unwrap_or_else(|| { - vec![ - PathBuf::from("/usr/local/share"), - PathBuf::from("/usr/share"), - ] - }); - - let nix_xdg_data_dirs = vec![ + let mut xdg_data_dirs: Vec = env_path("XDG_DATA_DIRS").unwrap_or_else(|| { + vec![ + PathBuf::from("/usr/local/share"), + PathBuf::from("/usr/share"), + ] + }); + + xdg_data_dirs.extend(vec![ nix_link.join("share"), PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/share"), - ]; - - let extended_xdg_dirs: Vec<&PathBuf> = xdg_data_dirs - .iter() - .chain(nix_xdg_data_dirs.iter()) - .collect::>(); + ]); - if let Ok(dirs) = env::join_paths(&extended_xdg_dirs) { + if let Ok(dirs) = env::join_paths(&xdg_data_dirs) { envs.insert("XDG_DATA_DIRS", dirs); } else { - return Err(Error::InvalidXdgDataDirs( - extended_xdg_dirs.into_iter().cloned().collect(), - )); + return Err(Error::InvalidXdgDataDirs(xdg_data_dirs)); } } - if env::var_os("NIX_SSL_CERT_FILE").is_none() { + if nonempty_var_os("NIX_SSL_CERT_FILE").is_none() { let mut candidate_locations = vec![ PathBuf::from("/etc/ssl/certs/ca-certificates.crt"), // NixOS, Ubuntu, Debian, Gentoo, Arch PathBuf::from("/etc/ssl/ca-bundle.pem"), // openSUSE Tumbleweed @@ -244,48 +247,40 @@ pub fn calculate_environment() -> Result, Error> }; { - let nix_path_parts = [ + let mut path = vec![ nix_link.join("bin"), // Note: This is typically only used in single-user installs, but I chose to do it in both for simplicity. // If there is good reason, we can make it fancier. PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/bin"), ]; - let current_path = env::var_os("PATH").unwrap_or(OsString::from("")); - let existing_path_parts = env::split_paths(¤t_path); - - let all_paths: Vec = nix_path_parts - .into_iter() - .chain(existing_path_parts) - .collect(); + if let Some(old_path) = env_path("PATH") { + path.extend(old_path); + } - if let Ok(dirs) = env::join_paths(&all_paths) { + if let Ok(dirs) = env::join_paths(&path) { envs.insert("PATH", dirs); } else { - return Err(Error::InvalidPathDirs(all_paths)); + return Err(Error::InvalidPathDirs(path)); } } { - let man_path_parts = [ + let mut path = vec![ nix_link.join("share/man"), // Note: This is typically only used in single-user installs, but I chose to do it in both for simplicity. // If there is good reason, we can make it fancier. PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/share/man"), ]; - let current_man_path = env::var_os("MANPATH").unwrap_or(OsString::from("")); - let existing_path_parts = env::split_paths(¤t_man_path); - - let all_paths: Vec = man_path_parts - .into_iter() - .chain(existing_path_parts) - .collect(); + if let Some(old_path) = env_path("MANPATH") { + path.extend(old_path); + } - if let Ok(dirs) = env::join_paths(&all_paths) { + if let Ok(dirs) = env::join_paths(&path) { envs.insert("MANPATH", dirs); } else { - return Err(Error::InvalidManPathDirs(all_paths)); + return Err(Error::InvalidManPathDirs(path)); } } From f8dbb4db05265c8b8c9c78cc76b661691b20c29b Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 13:49:40 -0500 Subject: [PATCH 04/17] fixup --- src/action/common/profiles/profile.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/action/common/profiles/profile.sh b/src/action/common/profiles/profile.sh index 852ec5d0d..bb0bb430a 100644 --- a/src/action/common/profiles/profile.sh +++ b/src/action/common/profiles/profile.sh @@ -7,7 +7,5 @@ if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFI $NIX_INSTALLER_EXPORT_DATA DATA_INPUT - unset NIX_INSTALLER_EXPORT_DATA -else - echo "not running import" >&2 + unset NIX_INSTALLER_EXPORT_DATA nix_installer_export_key nix_installer_export_value fi From e914be9215450cfe7f75d0eb8af79a1f25683290 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 15:22:57 -0500 Subject: [PATCH 05/17] spelling nit --- src/cli/subcommand/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index cf996196d..8ccdb9390 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -40,7 +40,7 @@ handling. In `space-newline-separated` mode, `nix-installer` guarantees it will: - * only emit keys that are alpha-numeric with underscores, + * only emit keys that are alphanumeric with underscores, * only emit values without newlines and will refuse to emit any output to stdout if the variables and values From 7907c62963be5b95ecd7cda3731066d05f5629c9 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 15:33:54 -0500 Subject: [PATCH 06/17] Fixup an issue where we incorrectly examine SSL cert locations --- src/cli/subcommand/export.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index 8ccdb9390..824d15006 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -229,12 +229,15 @@ pub fn calculate_environment() -> Result, Error> PathBuf::from("/etc/ssl/ca-bundle.pem"), // openSUSE Tumbleweed PathBuf::from("/etc/ssl/certs/ca-bundle.crt"), // Old NixOS PathBuf::from("/etc/pki/tls/certs/ca-bundle.crt"), // Fedora, CentOS - nix_link.join("etc/ssl/certs/ca-bundle.crt"), // fall back to cacert in Nix profile - nix_link.join("etc/ca-bundle.crt"), // old cacert in Nix profile ]; // Add the various profiles, preferring the last profile, ie: most global profile (matches upstream behavior) - candidate_locations.extend(nix_profiles.iter().rev().cloned()); + for profile in nix_profiles.iter().rev() { + candidate_locations.extend([ + profile.join("etc/ssl/certs/ca-bundle.crt"), // fall back to cacert in Nix profile + profile.join("etc/ca-bundle.crt"), // old cacert in Nix profile + ]); + } if let Some(cert) = candidate_locations.iter().find(|path| path.is_file()) { envs.insert("NIX_SSL_CERT_FILE", cert.into()); From 3b2713d9e95a5e8ae9bc7f7d11585cbde311a3c6 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 15:34:03 -0500 Subject: [PATCH 07/17] Only extend MANPATH if it is set already --- src/cli/subcommand/export.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index 824d15006..b0a804ab4 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -268,7 +268,7 @@ pub fn calculate_environment() -> Result, Error> } } - { + if let Some(old_path) = env_path("MANPATH") { let mut path = vec![ nix_link.join("share/man"), // Note: This is typically only used in single-user installs, but I chose to do it in both for simplicity. @@ -276,9 +276,7 @@ pub fn calculate_environment() -> Result, Error> PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/share/man"), ]; - if let Some(old_path) = env_path("MANPATH") { - path.extend(old_path); - } + path.extend(old_path); if let Ok(dirs) = env::join_paths(&path) { envs.insert("MANPATH", dirs); From 7663d83aaf33216fe9528ee534817b12206d3e6d Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 15:55:03 -0500 Subject: [PATCH 08/17] fixup fish --- src/action/common/profiles/profile.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/action/common/profiles/profile.fish b/src/action/common/profiles/profile.fish index 1dd776f74..18e652948 100644 --- a/src/action/common/profiles/profile.fish +++ b/src/action/common/profiles/profile.fish @@ -3,4 +3,4 @@ if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFI | while read --null key read --export --null "$key" end -fi +end From f072444d4db3c2b972faacbce11811e5459c81b6 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Wed, 22 Nov 2023 16:01:43 -0500 Subject: [PATCH 09/17] Don't try to set empty keys --- src/action/common/profiles/profile.sh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/action/common/profiles/profile.sh b/src/action/common/profiles/profile.sh index bb0bb430a..14f9c3032 100644 --- a/src/action/common/profiles/profile.sh +++ b/src/action/common/profiles/profile.sh @@ -1,11 +1,15 @@ # shellcheck shell=sh if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then - NIX_INSTALLER_EXPORT_DATA=$(/nix/nix-installer export --verbose --verbose --format space-newline-separated); - while read -r nix_installer_export_key nix_installer_export_value; do - export "$nix_installer_export_key=$nix_installer_export_value"; - done < Date: Thu, 23 Nov 2023 22:13:31 -0500 Subject: [PATCH 10/17] Don't error if HOME isn't set, since it is apparentl ynot a problem? --- src/cli/subcommand/export.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index b0a804ab4..96e315db9 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -21,13 +21,13 @@ pub enum Error { #[error("__ETC_PROFILE_NIX_SOURCED is set, indicating the relevant environment variables have already been set.")] AlreadyRun, - #[error("Some of the paths for XDG_DATA_DIR are not valid, due to an illegal character, like a space or colon.")] + #[error("Some of the paths from Nix for XDG_DATA_DIR are not valid, due to an illegal character, like a colon.")] InvalidXdgDataDirs(Vec), - #[error("Some of the paths for PATH are not valid, due to an illegal character, like a space or colon.")] + #[error("Some of the paths from Nix for PATH are not valid, due to an illegal character, like a colon.")] InvalidPathDirs(Vec), - #[error("Some of the paths for MANPATH are not valid, due to an illegal character, like a space or colon.")] + #[error("Some of the paths from Nix for MANPATH are not valid, due to an illegal character, like a colon.")] InvalidManPathDirs(Vec), } @@ -69,8 +69,8 @@ impl CommandExecute for Export { #[tracing::instrument(level = "trace", skip_all)] async fn execute(self) -> eyre::Result { let env = match calculate_environment() { - Err(Error::AlreadyRun) => { - debug!("Already set the environment vars, not doing it again."); + e @ Err(Error::AlreadyRun | Error::HomeNotSet) => { + debug!("Ignored error: {:?}", e); return Ok(ExitCode::SUCCESS); }, Err(e) => { From b5a56d50207545d7288d194adf673422f904f0c9 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Sat, 25 Nov 2023 13:42:25 -0500 Subject: [PATCH 11/17] Update src/cli/subcommand/export.rs --- src/cli/subcommand/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index 96e315db9..fef670695 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -137,7 +137,7 @@ impl CommandExecute for Export { } fn nonempty_var_os(key: &str) -> Option { - env::var_os(key).and_then(|val| if val.is_empty() { Some(val) } else { None }) + env::var_os(key).and_then(|val| if val.is_empty() { None } else { Some(val) }) } fn env_path(key: &str) -> Option> { From b5a4d15fb8b1028551e416d051448a86da1dbcf0 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Sat, 25 Nov 2023 14:56:48 -0500 Subject: [PATCH 12/17] Clean up error handling --- src/cli/subcommand/export.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index fef670695..2f3ea4d3d 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -69,18 +69,15 @@ impl CommandExecute for Export { #[tracing::instrument(level = "trace", skip_all)] async fn execute(self) -> eyre::Result { let env = match calculate_environment() { - e @ Err(Error::AlreadyRun | Error::HomeNotSet) => { + e @ Err(Error::AlreadyRun) => { debug!("Ignored error: {:?}", e); return Ok(ExitCode::SUCCESS); }, Err(e) => { - tracing::info!( - "Error calculating the environment variables required to enable Nix: {:?}", - e - ); - return Err(e.into()); + tracing::warn!("Error setting up the environment for Nix: {:?}", e); + // Don't return an Err, because we don't want to suggest bug reports for predictable problems. + return Ok(ExitCode::FAILURE); }, - Ok(env) => env, }; @@ -106,7 +103,7 @@ impl CommandExecute for Export { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" .contains(char) }) { - warn!("Key {} has an invalid character that isn't a-zA-Z_", key); + warn!("Key {} has an invalid character that isn't a-zA-Z0-9_", key); return Ok(ExitCode::FAILURE); } @@ -114,7 +111,7 @@ impl CommandExecute for Export { if value_bytes.contains(&b'\n') { warn!( - "Value for key {} has an a newline, which is prohibited", + "Value for key {} has a newline, which is prohibited", key ); return Ok(ExitCode::FAILURE); From 3da67105ca6321d53b310c56f16931c0c9a2588b Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Sat, 25 Nov 2023 14:59:19 -0500 Subject: [PATCH 13/17] Make calls to tracing logs explicit --- src/cli/subcommand/export.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index 2f3ea4d3d..45055a657 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -9,8 +9,6 @@ use std::process::ExitCode; use crate::cli::CommandExecute; use clap::Parser; -use tracing::{debug, warn}; - const LOCAL_STATE_DIR: &str = "/nix/var"; #[derive(Debug, thiserror::Error)] @@ -70,7 +68,7 @@ impl CommandExecute for Export { async fn execute(self) -> eyre::Result { let env = match calculate_environment() { e @ Err(Error::AlreadyRun) => { - debug!("Ignored error: {:?}", e); + tracing::debug!("Ignored error: {:?}", e); return Ok(ExitCode::SUCCESS); }, Err(e) => { @@ -85,7 +83,7 @@ impl CommandExecute for Export { match self.format { ExportFormat::NullSeparated => { - debug!("Emitting null separated fields"); + tracing::debug!("Emitting null separated fields"); for (key, value) in env.into_iter() { out.write_all(key.as_bytes())?; @@ -95,7 +93,7 @@ impl CommandExecute for Export { } }, ExportFormat::SpaceNewlineSeparated => { - debug!("Emitting space/newline separated fields"); + tracing::debug!("Emitting space/newline separated fields"); let mut validated_envs = HashMap::new(); for (key, value) in env.into_iter() { @@ -103,17 +101,17 @@ impl CommandExecute for Export { "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" .contains(char) }) { - warn!("Key {} has an invalid character that isn't a-zA-Z0-9_", key); + tracing::warn!( + "Key {} has an invalid character that isn't a-zA-Z0-9_", + key + ); return Ok(ExitCode::FAILURE); } let value_bytes = value.into_vec(); if value_bytes.contains(&b'\n') { - warn!( - "Value for key {} has a newline, which is prohibited", - key - ); + tracing::warn!("Value for key {} has a newline, which is prohibited", key); return Ok(ExitCode::FAILURE); } @@ -239,7 +237,7 @@ pub fn calculate_environment() -> Result, Error> if let Some(cert) = candidate_locations.iter().find(|path| path.is_file()) { envs.insert("NIX_SSL_CERT_FILE", cert.into()); } else { - warn!( + tracing::warn!( "Could not identify any SSL certificates out of these candidates: {:?}", candidate_locations ) @@ -282,7 +280,7 @@ pub fn calculate_environment() -> Result, Error> } } - debug!("Calculated environment: {:#?}", envs); + tracing::debug!("Calculated environment: {:#?}", envs); Ok(envs) } From d3f54dfea2fa8642c09f5981b1f30892337b2ab2 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Sat, 25 Nov 2023 15:00:47 -0500 Subject: [PATCH 14/17] Filter out empty strings --- src/cli/subcommand/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/subcommand/export.rs b/src/cli/subcommand/export.rs index 45055a657..5dd035ec1 100644 --- a/src/cli/subcommand/export.rs +++ b/src/cli/subcommand/export.rs @@ -132,7 +132,7 @@ impl CommandExecute for Export { } fn nonempty_var_os(key: &str) -> Option { - env::var_os(key).and_then(|val| if val.is_empty() { None } else { Some(val) }) + env::var_os(key).filter(|val| !val.is_empty()) } fn env_path(key: &str) -> Option> { From 0e191eadd11285f9c9b05953b8c516972c14464a Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 27 Nov 2023 14:31:58 -0500 Subject: [PATCH 15/17] Use eval instead ... I'll post an update about why later. --- Cargo.toml | 1 + src/action/common/profiles/profile.fish | 7 +- src/action/common/profiles/profile.sh | 14 +-- src/cli/subcommand/export.rs | 118 ++++++++++-------------- 4 files changed, 56 insertions(+), 84 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e30e0351e..162aa43f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ which = "4.4.0" sysctl = "0.5.4" walkdir = "2.3.3" indexmap = { version = "2.0.2", features = ["serde"] } +export = { git = "https://github.com/DeterminateSystems/export" } [dev-dependencies] eyre = { version = "0.6.8", default-features = false, features = [ "track-caller" ] } diff --git a/src/action/common/profiles/profile.fish b/src/action/common/profiles/profile.fish index 18e652948..d4d1640f7 100644 --- a/src/action/common/profiles/profile.fish +++ b/src/action/common/profiles/profile.fish @@ -1,6 +1,3 @@ -if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then - /nix/nix-installer export --format null-separated \ - | while read --null key - read --export --null "$key" - end +if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && not set -q __ETC_PROFILE_NIX_SOURCED; + eval "$(/nix/nix-installer export --format fish)" end diff --git a/src/action/common/profiles/profile.sh b/src/action/common/profiles/profile.sh index 14f9c3032..a433481bf 100644 --- a/src/action/common/profiles/profile.sh +++ b/src/action/common/profiles/profile.sh @@ -1,15 +1,5 @@ # shellcheck shell=sh -if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then - if NIX_INSTALLER_EXPORT_DATA=$(/nix/nix-installer export --format space-newline-separated); then - while read -r nix_installer_export_key nix_installer_export_value; do - if [ -n "$nix_installer_export_key" ]; then - export "$nix_installer_export_key=$nix_installer_export_value"; - fi - done <, } #[derive(Debug, Clone, PartialEq, Eq, clap::ValueEnum)] enum ExportFormat { - NullSeparated, - SpaceNewlineSeparated, + Fish, + Sh, } #[async_trait::async_trait] impl CommandExecute for Export { #[tracing::instrument(level = "trace", skip_all)] async fn execute(self) -> eyre::Result { - let env = match calculate_environment() { - e @ Err(Error::AlreadyRun) => { - tracing::debug!("Ignored error: {:?}", e); - return Ok(ExitCode::SUCCESS); - }, - Err(e) => { - tracing::warn!("Error setting up the environment for Nix: {:?}", e); - // Don't return an Err, because we don't want to suggest bug reports for predictable problems. - return Ok(ExitCode::FAILURE); - }, - Ok(env) => env, - }; - - let mut out = BufWriter::new(stdout()); - - match self.format { - ExportFormat::NullSeparated => { - tracing::debug!("Emitting null separated fields"); - - for (key, value) in env.into_iter() { - out.write_all(key.as_bytes())?; - out.write_all(&[b'\0'])?; - out.write_all(&value.into_vec())?; - out.write_all(&[b'\0'])?; - } + let env: HashMap = match self.sample_output { + Some(filename) => { + // Note: not tokio File b/c I don't think serde_json has fancy async support? + let file = std::fs::File::open(filename)?; + let intermediate: HashMap = serde_json::from_reader(file)?; + intermediate + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect() }, - ExportFormat::SpaceNewlineSeparated => { - tracing::debug!("Emitting space/newline separated fields"); - - let mut validated_envs = HashMap::new(); - for (key, value) in env.into_iter() { - if !key.chars().all(|char| { - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" - .contains(char) - }) { - tracing::warn!( - "Key {} has an invalid character that isn't a-zA-Z0-9_", - key - ); + None => { + match calculate_environment() { + e @ Err(Error::AlreadyRun) => { + tracing::debug!("Ignored error: {:?}", e); + return Ok(ExitCode::SUCCESS); + }, + Err(e) => { + tracing::warn!("Error setting up the environment for Nix: {:?}", e); + // Don't return an Err, because we don't want to suggest bug reports for predictable problems. return Ok(ExitCode::FAILURE); - } - - let value_bytes = value.into_vec(); - - if value_bytes.contains(&b'\n') { - tracing::warn!("Value for key {} has a newline, which is prohibited", key); - return Ok(ExitCode::FAILURE); - } - - validated_envs.insert(key, value_bytes); - } - - for (key, value) in validated_envs.into_iter() { - out.write_all(key.as_bytes())?; - out.write_all(b" ")?; - out.write_all(&value)?; - out.write_all(b"\n")?; + }, + Ok(env) => env, } }, + }; + + let mut export_env: HashMap = HashMap::new(); + for (k, v) in env.into_iter() { + export_env.insert(k.try_into()?, v); } + stdout().write_all( + export::escape( + match self.format { + ExportFormat::Fish => export::Encoding::Fish, + ExportFormat::Sh => export::Encoding::PosixShell, + }, + export_env, + )? + .as_bytes(), + )?; + Ok(ExitCode::SUCCESS) } } @@ -145,8 +129,8 @@ fn env_path(key: &str) -> Option> { Some(env::split_paths(&path).collect()) } -pub fn calculate_environment() -> Result, Error> { - let mut envs: HashMap<&'static str, OsString> = HashMap::new(); +pub fn calculate_environment() -> Result, Error> { + let mut envs: HashMap = HashMap::new(); // Don't export variables twice. // @PORT-NOTE nix-profile-daemon.sh.in and nix-profile-daemon.fish.in implemented @@ -166,7 +150,7 @@ pub fn calculate_environment() -> Result, Error> return Err(Error::HomeNotSet); }; - envs.insert("__ETC_PROFILE_NIX_SOURCED", "1".into()); + envs.insert("__ETC_PROFILE_NIX_SOURCED".into(), "1".into()); let nix_link: PathBuf = { let legacy_location = home.join(".nix-profile"); @@ -190,7 +174,7 @@ pub fn calculate_environment() -> Result, Error> nix_link.clone(), ]; envs.insert( - "NIX_PROFILES", + "NIX_PROFILES".into(), nix_profiles .iter() .map(|path| path.as_os_str()) @@ -212,7 +196,7 @@ pub fn calculate_environment() -> Result, Error> ]); if let Ok(dirs) = env::join_paths(&xdg_data_dirs) { - envs.insert("XDG_DATA_DIRS", dirs); + envs.insert("XDG_DATA_DIRS".into(), dirs); } else { return Err(Error::InvalidXdgDataDirs(xdg_data_dirs)); } @@ -235,7 +219,7 @@ pub fn calculate_environment() -> Result, Error> } if let Some(cert) = candidate_locations.iter().find(|path| path.is_file()) { - envs.insert("NIX_SSL_CERT_FILE", cert.into()); + envs.insert("NIX_SSL_CERT_FILE".into(), cert.into()); } else { tracing::warn!( "Could not identify any SSL certificates out of these candidates: {:?}", @@ -257,7 +241,7 @@ pub fn calculate_environment() -> Result, Error> } if let Ok(dirs) = env::join_paths(&path) { - envs.insert("PATH", dirs); + envs.insert("PATH".into(), dirs); } else { return Err(Error::InvalidPathDirs(path)); } @@ -274,7 +258,7 @@ pub fn calculate_environment() -> Result, Error> path.extend(old_path); if let Ok(dirs) = env::join_paths(&path) { - envs.insert("MANPATH", dirs); + envs.insert("MANPATH".into(), dirs); } else { return Err(Error::InvalidManPathDirs(path)); } From 0ff7364c9f3fe8165c930783cc7e815fb1bf1e2d Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 27 Nov 2023 14:59:28 -0500 Subject: [PATCH 16/17] cargo lock --- Cargo.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index b950bd276..4a67229de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,14 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "export" +version = "0.1.0" +source = "git+https://github.com/DeterminateSystems/export#cf859216f9b4b9e27ef0aa0bcb2f52ca8a4e1c02" +dependencies = [ + "thiserror", +] + [[package]] name = "eyre" version = "0.6.8" @@ -963,6 +971,7 @@ dependencies = [ "color-eyre", "dirs", "dyn-clone", + "export", "eyre", "glob", "indexmap 2.1.0", From 3063635b9870f68da3ff8a22b3719534cf093c73 Mon Sep 17 00:00:00 2001 From: Graham Christensen Date: Mon, 27 Nov 2023 18:38:40 -0500 Subject: [PATCH 17/17] Fixup the profile paths in the post-install instructions --- src/cli/subcommand/install.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/cli/subcommand/install.rs b/src/cli/subcommand/install.rs index f333363e9..a874be741 100644 --- a/src/cli/subcommand/install.rs +++ b/src/cli/subcommand/install.rs @@ -4,7 +4,10 @@ use std::{ }; use crate::{ - action::ActionState, + action::{ + common::configure_shell_profile::PROFILE_NIX_FILE_FISH, + common::configure_shell_profile::PROFILE_NIX_FILE_SHELL, ActionState, + }, cli::{ ensure_root, interaction::{self, PromptChoice}, @@ -312,6 +315,8 @@ impl CommandExecute for Install { } }, Ok(_) => { + let load_fish = format!(". {}", PROFILE_NIX_FILE_FISH); + let load_shell = format!(". {}", PROFILE_NIX_FILE_SHELL); println!( "\ {success}\n\ @@ -319,10 +324,12 @@ impl CommandExecute for Install { ", success = "Nix was installed successfully!".green().bold(), shell_reminder = match std::env::var("SHELL") { - Ok(val) if val.contains("fish") => - ". /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.fish".bold(), - Ok(_) | Err(_) => - ". /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh".bold(), + Ok(val) if val.contains("fish") => { + load_fish.bold() + }, + Ok(_) | Err(_) => { + load_shell.bold() + }, }, ); },