diff --git a/README.md b/README.md index 92beffa0f..f4bc80d17 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ A fast, friendly, and reliable tool to help you use Nix with Flakes everywhere. curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install ``` -The `nix-installer` has successfully completed over 500,000 installs in a number of environments, including [Github Actions](#as-a-github-action): +The `nix-installer` has successfully completed over 1,000,000 installs in a number of environments, including [Github Actions](#as-a-github-action): | Platform | Multi User | `root` only | Maturity | |------------------------------|:------------------:|:-----------:|:-----------------:| @@ -275,49 +275,6 @@ This is especially useful when using the installer in non-interactive scripts. While `nix-installer` tries to provide a comprehensive and unquirky experience, there are unfortunately some issues which may require manual intervention or operator choices. -### Using MacOS remote SSH builders, Nix binaries are not on `$PATH` - -When connecting to a Mac remote SSH builder users may sometimes see this error: - -```bash -$ nix store ping --store "ssh://$USER@$HOST" -Store URL: ssh://$USER@$HOST -zsh:1: command not found: nix-store -error: cannot connect to '$USER@$HOST' -``` - -The way MacOS populates the `PATH` environment differs from other environments. ([Some background](https://gist.github.com/Linerre/f11ad4a6a934dcf01ee8415c9457e7b2)) - -There are two possible workarounds for this: - -* **(Preferred)** Update the remote builder URL to include the `remote-program` parameter pointing to `nix-store`. For example: - ```bash - nix store ping --store "ssh://$USER@$HOST?remote-program=/nix/var/nix/profiles/default/bin/nix-store" - ``` - If you are unsure where the `nix-store` binary is located, run `which nix-store` on the remote. -* Update `/etc/zshenv` on the remote so that `zsh` populates the Nix path for every shell, even those that are neither *interactive* or *login*: - ```bash - # Nix - if [ -e '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' ]; then - . '/nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh' - fi - # End Nix - ``` -
- This strategy has some behavioral caveats, namely, $PATH may have unexpected contents - - For example, if `$PATH` gets unset then a script invoked, `$PATH` may not be as empty as expected: - ```bash - $ cat example.sh - #! /bin/zsh - echo $PATH - $ PATH= ./example.sh - /Users/ephemeraladmin/.nix-profile/bin:/nix/var/nix/profiles/default/bin: - ``` - This strategy results in Nix's paths being present on `$PATH` twice and may have a minor impact on performance. - -
- ### Using MacOS after removing `nix` while `nix-darwin` was still installed, network requests fail If `nix` was previously uninstalled without uninstalling `nix-darwin` first, users may experience errors similar to this: @@ -462,11 +419,6 @@ curl -sSf -L https://github.com/DeterminateSystems/nix-installer/releases/downlo Differing from the upstream [Nix](https://github.com/NixOS/nix) installer scripts: -* In `nix.conf`: - + the `nix-command` and `flakes` features are enabled - + `bash-prompt-prefix` is set - + `auto-optimise-store` is set to `true` (On Linux only) - * `extra-nix-path` is set to `nixpkgs=flake:nixpkgs` * an installation receipt (for uninstalling) is stored at `/nix/receipt.json` as well as a copy of the install binary at `/nix/nix-installer` * `nix-channel --update` is not run, `~/.nix-channels` is not provisioned * `ssl-cert-file` is set in `/etc/nix/nix.conf` if the `ssl-cert-file` argument is used. @@ -479,6 +431,7 @@ Subtle differences in the shell implementations and tool used in the scripts mak The Determinate Nix installer has numerous advantages: +* survives macOS upgrades * keeping an installation receipt for easy uninstallation * offering users a chance to review an accurate, calculated install plan * having 'planners' which can create appropriate install plans for complicated targets @@ -487,6 +440,7 @@ The Determinate Nix installer has numerous advantages: * supporting a expanded test suite including 'curing' cases * supporting SELinux and OSTree based distributions without asking users to make compromises * operating as a single, static binary with external dependencies such as `openssl`, only calling existing system tools (like `useradd`) where necessary +* As a MacOS remote build target, ensures `nix` is not absent from path It has been wonderful to collaborate with other participants in the Nix Installer Working Group and members of the broader community. The working group maintains a [foundation owned fork of the installer](https://github.com/nixos/experimental-nix-installer/). diff --git a/src/action/base/create_or_merge_nix_config.rs b/src/action/base/create_or_merge_nix_config.rs index 250ac0be1..4a0239cf8 100644 --- a/src/action/base/create_or_merge_nix_config.rs +++ b/src/action/base/create_or_merge_nix_config.rs @@ -409,10 +409,10 @@ impl Action for CreateOrMergeNixConfig { new_config.push('\n'); } - new_config.push_str(&format!( - "# Generated by https://github.com/DeterminateSystems/nix-installer, version {version}.\n", - version = env!("CARGO_PKG_VERSION"), - )); + new_config + .push_str("# Generated by https://github.com/DeterminateSystems/nix-installer.\n"); + new_config.push_str("# See `/nix/nix-installer --version` for the version details.\n"); + new_config.push_str("\n"); for (name, value) in merged_nix_config.settings() { new_config.push_str(name); diff --git a/src/action/base/move_unpacked_nix.rs b/src/action/base/move_unpacked_nix.rs index 875e3903e..37e034ff8 100644 --- a/src/action/base/move_unpacked_nix.rs +++ b/src/action/base/move_unpacked_nix.rs @@ -1,5 +1,4 @@ use std::{ - fs::Permissions, os::unix::prelude::PermissionsExt, path::{Path, PathBuf}, }; @@ -110,13 +109,21 @@ impl Action for MoveUnpackedNix { .map_err(|e| ActionErrorKind::Rename(entry.path(), entry_dest.to_owned(), e)) .map_err(Self::error)?; - let perms: Permissions = PermissionsExt::from_mode(0o555); for entry_item in WalkDir::new(&entry_dest) .into_iter() .filter_map(Result::ok) .filter(|e| !e.file_type().is_symlink()) { - tokio::fs::set_permissions(&entry_item.path(), perms.clone()) + let path = entry_item.path(); + + let mut perms = path + .metadata() + .map_err(|e| ActionErrorKind::GetMetadata(path.to_owned(), e)) + .map_err(Self::error)? + .permissions(); + perms.set_readonly(true); + + tokio::fs::set_permissions(path, perms.clone()) .await .map_err(|e| { ActionErrorKind::SetPermissions( diff --git a/src/action/common/place_nix_configuration.rs b/src/action/common/place_nix_configuration.rs index ca992c4b2..8c1da8b08 100644 --- a/src/action/common/place_nix_configuration.rs +++ b/src/action/common/place_nix_configuration.rs @@ -8,7 +8,6 @@ use crate::action::{ }; use crate::parse_ssl_cert; use crate::settings::UrlOrPathOrString; -use indexmap::map::Entry; use std::path::PathBuf; const NIX_CONF_FOLDER: &str = "/etc/nix"; @@ -91,14 +90,20 @@ impl PlaceNixConfiguration { let settings = nix_config.settings_mut(); settings.insert("build-users-group".to_string(), nix_build_group_name); - let experimental_features = ["nix-command", "flakes", "repl-flake"]; - match settings.entry("experimental-features".to_string()) { - Entry::Occupied(mut slot) => { - let slot_mut = slot.get_mut(); - for experimental_feature in experimental_features { - if !slot_mut.contains(experimental_feature) { - *slot_mut += " "; - *slot_mut += experimental_feature; + + #[cfg(not(feature = "nix-community"))] + { + use indexmap::map::Entry; + + let experimental_features = ["nix-command", "flakes", "repl-flake"]; + match settings.entry("experimental-features".to_string()) { + Entry::Occupied(mut slot) => { + let slot_mut = slot.get_mut(); + for experimental_feature in experimental_features { + if !slot_mut.contains(experimental_feature) { + *slot_mut += " "; + *slot_mut += experimental_feature; + } } }, Entry::Vacant(slot) => { @@ -114,35 +119,21 @@ impl PlaceNixConfiguration { "bash-prompt-prefix".to_string(), "(nix:$name)\\040".to_string(), ); + settings.insert("max-jobs".to_string(), "auto".to_string()); + if let Some(ssl_cert_file) = ssl_cert_file { + let ssl_cert_file_canonical = ssl_cert_file + .canonicalize() + .map_err(|e| Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e)))?; + settings.insert( + "ssl-cert-file".to_string(), + ssl_cert_file_canonical.display().to_string(), + ); + } settings.insert( "extra-nix-path".to_string(), "nixpkgs=flake:nixpkgs".to_string(), ); - - // Auto-allocate uids is broken on Mac. Tools like `whoami` don't work. - // e.g. https://github.com/NixOS/nix/issues/8444 - #[cfg(not(target_os = "macos"))] - settings.insert("auto-allocate-uids".to_string(), "true".to_string()); - } - - settings.insert( - "bash-prompt-prefix".to_string(), - "(nix:$name)\\040".to_string(), - ); - settings.insert("max-jobs".to_string(), "auto".to_string()); - if let Some(ssl_cert_file) = ssl_cert_file { - let ssl_cert_file_canonical = ssl_cert_file - .canonicalize() - .map_err(|e| Self::error(ActionErrorKind::Canonicalize(ssl_cert_file, e)))?; - settings.insert( - "ssl-cert-file".to_string(), - ssl_cert_file_canonical.display().to_string(), - ); } - settings.insert( - "extra-nix-path".to_string(), - "nixpkgs=flake:nixpkgs".to_string(), - ); let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force) .await diff --git a/src/action/macos/configure_remote_building.rs b/src/action/macos/configure_remote_building.rs new file mode 100644 index 000000000..3d9e03690 --- /dev/null +++ b/src/action/macos/configure_remote_building.rs @@ -0,0 +1,96 @@ +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"; + +/** +Configure macOS's zshenv to load the Nix environment when ForceCommand is used. +This enables remote building, which requires `ssh host nix` to work. + */ +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ConfigureRemoteBuilding { + create_or_insert_into_file: StatefulAction, +} + +impl ConfigureRemoteBuilding { + #[tracing::instrument(level = "debug", skip_all)] + 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 + . '{PROFILE_NIX_FILE_SHELL}' +fi +# End Nix +"# + ); + + let create_or_insert_into_file = CreateOrInsertIntoFile::plan( + Path::new("/etc/zshenv"), + None, + None, + 0o644, + shell_buf.to_string(), + create_or_insert_into_file::Position::Beginning, + ) + .await + .map_err(Self::error)?; + + Ok(Self { + create_or_insert_into_file, + } + .into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "configure_remote_building")] +impl Action for ConfigureRemoteBuilding { + fn action_tag() -> ActionTag { + ActionTag("configure_remote_building") + } + fn tracing_synopsis(&self) -> String { + "Configuring zsh to support using Nix in non-interactive shells".to_string() + } + + fn tracing_span(&self) -> Span { + span!(tracing::Level::DEBUG, "configure_remote_building",) + } + + fn execute_description(&self) -> Vec { + vec![ActionDescription::new( + self.tracing_synopsis(), + vec!["Update `/etc/zshenv` to import Nix".to_string()], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + let span = tracing::Span::current().clone(); + self.create_or_insert_into_file + .try_execute() + .instrument(span) + .await + .map_err(Self::error)?; + + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new( + "Remove the Nix configuration from zsh's non-login shells".to_string(), + vec!["Update `/etc/zshenv` to no longer import Nix".to_string()], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + self.create_or_insert_into_file.try_revert().await?; + + Ok(()) + } +} diff --git a/src/action/macos/mod.rs b/src/action/macos/mod.rs index 4299d7fc0..7ad01fb90 100644 --- a/src/action/macos/mod.rs +++ b/src/action/macos/mod.rs @@ -2,6 +2,7 @@ */ pub(crate) mod bootstrap_launchctl_service; +pub(crate) mod configure_remote_building; pub(crate) mod create_apfs_volume; pub(crate) mod create_fstab_entry; pub(crate) mod create_nix_hook_service; @@ -16,6 +17,7 @@ pub(crate) mod set_tmutil_exclusions; pub(crate) mod unmount_apfs_volume; pub use bootstrap_launchctl_service::BootstrapLaunchctlService; +pub use configure_remote_building::ConfigureRemoteBuilding; pub use create_apfs_volume::CreateApfsVolume; pub use create_nix_hook_service::CreateNixHookService; pub use create_nix_volume::{CreateNixVolume, NIX_VOLUME_MOUNTD_DEST}; diff --git a/src/action/mod.rs b/src/action/mod.rs index 885665109..c9ef0998a 100644 --- a/src/action/mod.rs +++ b/src/action/mod.rs @@ -422,6 +422,8 @@ pub enum ActionErrorKind { std::path::PathBuf, #[source] std::io::Error, ), + #[error("Getting filesystem metadata for `{0}` on `{1}`")] + GetMetadata(std::path::PathBuf, #[source] std::io::Error), #[error("Set mode `{0:#o}` on `{1}`")] SetPermissions(u32, std::path::PathBuf, #[source] std::io::Error), #[error("Remove file `{0}`")] diff --git a/src/cli/subcommand/repair.rs b/src/cli/subcommand/repair.rs index 3b4fb1e3f..243ff20ad 100644 --- a/src/cli/subcommand/repair.rs +++ b/src/cli/subcommand/repair.rs @@ -38,9 +38,23 @@ impl CommandExecute for Repair { if let Err(err) = reconfigure.try_execute().await { println!("{:#?}", err); - Ok(ExitCode::FAILURE) - } else { - Ok(ExitCode::SUCCESS) + return Ok(ExitCode::FAILURE); } + // TODO: Using `cfg` based on OS is not a long term solution. + // Make this read the planner from the `/nix/receipt.json` to determine which tasks to run. + #[cfg(target_os = "macos")] + { + let mut reconfigure = crate::action::macos::ConfigureRemoteBuilding::plan() + .await + .map_err(PlannerError::Action)? + .boxed(); + + if let Err(err) = reconfigure.try_execute().await { + println!("{:#?}", err); + return Ok(ExitCode::FAILURE); + } + } + + Ok(ExitCode::SUCCESS) } } diff --git a/src/planner/macos.rs b/src/planner/macos.rs index 28398d5cd..a35a91a5f 100644 --- a/src/planner/macos.rs +++ b/src/planner/macos.rs @@ -12,7 +12,9 @@ use crate::{ action::{ base::RemoveDirectory, common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix}, - macos::{CreateNixHookService, CreateNixVolume, SetTmutilExclusions}, + macos::{ + ConfigureRemoteBuilding, CreateNixHookService, CreateNixVolume, SetTmutilExclusions, + }, StatefulAction, }, execute_command, @@ -166,6 +168,12 @@ impl Planner for Macos { .map_err(PlannerError::Action)? .boxed(), ); + plan.push( + ConfigureRemoteBuilding::plan() + .await + .map_err(PlannerError::Action)? + .boxed(), + ); if self.settings.modify_profile { plan.push(