Compare commits

...

17 commits

Author SHA1 Message Date
Graham Christensen 3063635b98 Fixup the profile paths in the post-install instructions 2023-11-27 18:38:40 -05:00
Graham Christensen 0ff7364c9f cargo lock 2023-11-27 14:59:28 -05:00
Graham Christensen 0e191eadd1 Use eval instead ... I'll post an update about why later. 2023-11-27 14:31:58 -05:00
Graham Christensen d3f54dfea2 Filter out empty strings 2023-11-25 15:00:47 -05:00
Graham Christensen 3da67105ca Make calls to tracing logs explicit 2023-11-25 14:59:19 -05:00
Graham Christensen b5a4d15fb8 Clean up error handling 2023-11-25 14:56:48 -05:00
Graham Christensen b5a56d5020 Update src/cli/subcommand/export.rs 2023-11-25 14:46:53 -05:00
Graham Christensen dd8a3b9714 Don't error if HOME isn't set, since it is apparentl ynot a problem? 2023-11-23 22:13:31 -05:00
Graham Christensen f072444d4d Don't try to set empty keys 2023-11-22 16:01:43 -05:00
Graham Christensen 7663d83aaf fixup fish 2023-11-22 15:55:03 -05:00
Graham Christensen 3b2713d9e9 Only extend MANPATH if it is set already 2023-11-22 15:34:03 -05:00
Graham Christensen 7907c62963 Fixup an issue where we incorrectly examine SSL cert locations 2023-11-22 15:33:54 -05:00
Graham Christensen e914be9215 spelling nit 2023-11-22 15:22:57 -05:00
Graham Christensen f8dbb4db05 fixup 2023-11-22 14:51:08 -05:00
Graham Christensen 848d223fc6 Don't turn an absent or empty path var into extra path values 2023-11-22 14:51:08 -05:00
Graham Christensen be02735121 clean up note 2023-11-22 14:51:08 -05:00
Graham Christensen a3231d4b02 Create a Rust helper for defining environment variables 2023-11-22 14:51:08 -05:00
11 changed files with 378 additions and 44 deletions

9
Cargo.lock generated
View file

@ -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",

View file

@ -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" ] }

View file

@ -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 {

View file

@ -0,0 +1,3 @@
if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && not set -q __ETC_PROFILE_NIX_SOURCED;
eval "$(/nix/nix-installer export --format fish)"
end

View file

@ -0,0 +1,5 @@
# shellcheck shell=sh
if [ -f /nix/nix-installer ] && [ -x /nix/nix-installer ] && [ -z "${__ETC_PROFILE_NIX_SOURCED:-}" ]; then
eval "$(/nix/nix-installer export --format sh)"
fi

View file

@ -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<StatefulAction<Self>, 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
"#
);

View file

@ -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,
}
}
}

View file

@ -0,0 +1,270 @@
use std::collections::HashMap;
use std::env;
use std::ffi::{OsStr, OsString};
use std::io::{stdout, Write};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::process::ExitCode;
use crate::cli::CommandExecute;
use clap::Parser;
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 from Nix for XDG_DATA_DIR are not valid, due to an illegal character, like a colon.")]
InvalidXdgDataDirs(Vec<PathBuf>),
#[error("Some of the paths from Nix for PATH are not valid, due to an illegal character, like a colon.")]
InvalidPathDirs(Vec<PathBuf>),
#[error("Some of the paths from Nix for MANPATH are not valid, due to an illegal character, like a colon.")]
InvalidManPathDirs(Vec<PathBuf>),
}
/**
Emit all the environment variables that should be set to use Nix.
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 alphanumeric 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,
#[clap(long)]
sample_output: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq, clap::ValueEnum)]
enum ExportFormat {
Fish,
Sh,
}
#[async_trait::async_trait]
impl CommandExecute for Export {
#[tracing::instrument(level = "trace", skip_all)]
async fn execute(self) -> eyre::Result<ExitCode> {
let env: HashMap<String, OsString> = 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<String, String> = serde_json::from_reader(file)?;
intermediate
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect()
},
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);
},
Ok(env) => env,
}
},
};
let mut export_env: HashMap<export::VariableName, OsString> = 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)
}
}
fn nonempty_var_os(key: &str) -> Option<OsString> {
env::var_os(key).filter(|val| !val.is_empty())
}
fn env_path(key: &str) -> Option<Vec<PathBuf>> {
let path = env::var_os(key)?;
if path.is_empty() {
return Some(vec![]);
}
Some(env::split_paths(&path).collect())
}
pub fn calculate_environment() -> Result<HashMap<String, OsString>, Error> {
let mut envs: HashMap<String, 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 nonempty_var_os("__ETC_PROFILE_NIX_SOURCED") == Some("1".into()) {
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) = nonempty_var_os("HOME") {
PathBuf::from(home)
} else {
return Err(Error::HomeNotSet);
};
envs.insert("__ETC_PROFILE_NIX_SOURCED".into(), "1".into());
let nix_link: PathBuf = {
let legacy_location = home.join(".nix-profile");
let xdg_location = nonempty_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".into(),
nix_profiles
.iter()
.map(|path| path.as_os_str())
.collect::<Vec<_>>()
.join(OsStr::new(" ")),
);
{
let mut xdg_data_dirs: Vec<PathBuf> = 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"),
]);
if let Ok(dirs) = env::join_paths(&xdg_data_dirs) {
envs.insert("XDG_DATA_DIRS".into(), dirs);
} else {
return Err(Error::InvalidXdgDataDirs(xdg_data_dirs));
}
}
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
PathBuf::from("/etc/ssl/certs/ca-bundle.crt"), // Old NixOS
PathBuf::from("/etc/pki/tls/certs/ca-bundle.crt"), // Fedora, CentOS
];
// Add the various profiles, preferring the last profile, ie: most global profile (matches upstream behavior)
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".into(), cert.into());
} else {
tracing::warn!(
"Could not identify any SSL certificates out of these candidates: {:?}",
candidate_locations
)
}
};
{
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"),
];
if let Some(old_path) = env_path("PATH") {
path.extend(old_path);
}
if let Ok(dirs) = env::join_paths(&path) {
envs.insert("PATH".into(), dirs);
} else {
return Err(Error::InvalidPathDirs(path));
}
}
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.
// If there is good reason, we can make it fancier.
PathBuf::from(LOCAL_STATE_DIR).join("nix/profiles/default/share/man"),
];
path.extend(old_path);
if let Ok(dirs) = env::join_paths(&path) {
envs.insert("MANPATH".into(), dirs);
} else {
return Err(Error::InvalidManPathDirs(path));
}
}
tracing::debug!("Calculated environment: {:#?}", envs);
Ok(envs)
}

View file

@ -1,18 +1,20 @@
use std::{
os::unix::prelude::PermissionsExt,
path::{Path, PathBuf},
process::ExitCode,
};
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},
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 +315,8 @@ impl CommandExecute for Install {
}
},
Ok(_) => {
copy_self_to_nix_dir()
.await
.wrap_err("Copying `nix-installer` to `/nix/nix-installer`")?;
let load_fish = format!(". {}", PROFILE_NIX_FILE_FISH);
let load_shell = format!(". {}", PROFILE_NIX_FILE_SHELL);
println!(
"\
{success}\n\
@ -323,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()
},
},
);
},
@ -335,11 +338,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(())
}

View file

@ -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),
}

View file

@ -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<Version, NixInstallerError> {
let nix_installer_version_str = env!("CARGO_PKG_VERSION");
Version::from_str(nix_installer_version_str).map_err(|e| {