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(