diff --git a/doc/manual/src/installation/installing-binary.md b/doc/manual/src/installation/installing-binary.md
index ae7fd458b..96fa34635 100644
--- a/doc/manual/src/installation/installing-binary.md
+++ b/doc/manual/src/installation/installing-binary.md
@@ -1,18 +1,26 @@
# Installing a Binary Distribution
-If you are using Linux or macOS versions up to 10.14 (Mojave), the
-easiest way to install Nix is to run the following command:
+The easiest way to install Nix is to run the following command:
```console
$ sh <(curl -L https://nixos.org/nix/install)
```
-If you're using macOS 10.15 (Catalina) or newer, consult [the macOS
-installation instructions](#macos-installation) before installing.
+This will run the installer interactively (causing it to explain what
+it is doing more explicitly), and perform the default "type" of install
+for your platform:
+- single-user on Linux
+- multi-user on macOS
-As of Nix 2.1.0, the Nix installer will always default to creating a
-single-user installation, however opting in to the multi-user
-installation is highly recommended.
+ > **Notes on read-only filesystem root in macOS 10.15 Catalina +**
+ >
+ > - It took some time to support this cleanly. You may see posts,
+ > examples, and tutorials using obsolete workarounds.
+ > - Supporting it cleanly made macOS installs too complex to qualify
+ > as single-user, so this type is no longer supported on macOS.
+
+We recommend the multi-user install if it supports your platform and
+you can authenticate with `sudo`.
# Single User Installation
@@ -50,9 +58,9 @@ $ rm -rf /nix
The multi-user Nix installation creates system users, and a system
service for the Nix daemon.
- - Linux running systemd, with SELinux disabled
-
- - macOS
+**Supported Systems**
+- Linux running systemd, with SELinux disabled
+- macOS
You can instruct the installer to perform a multi-user installation on
your system:
@@ -96,165 +104,28 @@ sudo rm /Library/LaunchDaemons/org.nixos.nix-daemon.plist
There may also be references to Nix in `/etc/profile`, `/etc/bashrc`,
and `/etc/zshrc` which you may remove.
-# macOS Installation
+# macOS Installation
+
-Starting with macOS 10.15 (Catalina), the root filesystem is read-only.
-This means `/nix` can no longer live on your system volume, and that
-you'll need a workaround to install Nix.
+We believe we have ironed out how to cleanly support the read-only root
+on modern macOS. New installs will do this automatically, and you can
+also re-run a new installer to convert your existing setup.
-The recommended approach, which creates an unencrypted APFS volume for
-your Nix store and a "synthetic" empty directory to mount it over at
-`/nix`, is least likely to impair Nix or your system.
+This section previously detailed the situation, options, and trade-offs,
+but it now only outlines what the installer does. You don't need to know
+this to run the installer, but it may help if you run into trouble:
-> **Note**
->
-> With all separate-volume approaches, it's possible something on your
-> system (particularly daemons/services and restored apps) may need
-> access to your Nix store before the volume is mounted. Adding
-> additional encryption makes this more likely.
-
-If you're using a recent Mac with a [T2
-chip](https://www.apple.com/euro/mac/shared/docs/Apple_T2_Security_Chip_Overview.pdf),
-your drive will still be encrypted at rest (in which case "unencrypted"
-is a bit of a misnomer). To use this approach, just install Nix with:
-
-```console
-$ sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume
-```
-
-If you don't like the sound of this, you'll want to weigh the other
-approaches and tradeoffs detailed in this section.
-
-> **Note**
->
-> All of the known workarounds have drawbacks, but we hope better
-> solutions will be available in the future. Some that we have our eye
-> on are:
->
-> 1. A true firmlink would enable the Nix store to live on the primary
-> data volume without the build problems caused by the symlink
-> approach. End users cannot currently create true firmlinks.
->
-> 2. If the Nix store volume shared FileVault encryption with the
-> primary data volume (probably by using the same volume group and
-> role), FileVault encryption could be easily supported by the
-> installer without requiring manual setup by each user.
-
-## Change the Nix store path prefix
-
-Changing the default prefix for the Nix store is a simple approach which
-enables you to leave it on your root volume, where it can take full
-advantage of FileVault encryption if enabled. Unfortunately, this
-approach also opts your device out of some benefits that are enabled by
-using the same prefix across systems:
-
- - Your system won't be able to take advantage of the binary cache
- (unless someone is able to stand up and support duplicate caching
- infrastructure), which means you'll spend more time waiting for
- builds.
-
- - It's harder to build and deploy packages to Linux systems.
-
-It would also possible (and often requested) to just apply this change
-ecosystem-wide, but it's an intrusive process that has side effects we
-want to avoid for now.
-
-## Use a separate encrypted volume
-
-If you like, you can also add encryption to the recommended approach
-taken by the installer. You can do this by pre-creating an encrypted
-volume before you run the installer--or you can run the installer and
-encrypt the volume it creates later.
-
-In either case, adding encryption to a second volume isn't quite as
-simple as enabling FileVault for your boot volume. Before you dive in,
-there are a few things to weigh:
-
-1. The additional volume won't be encrypted with your existing
- FileVault key, so you'll need another mechanism to decrypt the
- volume.
-
-2. You can store the password in Keychain to automatically decrypt the
- volume on boot--but it'll have to wait on Keychain and may not mount
- before your GUI apps restore. If any of your launchd agents or apps
- depend on Nix-installed software (for example, if you use a
- Nix-installed login shell), the restore may fail or break.
-
- On a case-by-case basis, you may be able to work around this problem
- by using `wait4path` to block execution until your executable is
- available.
-
- It's also possible to decrypt and mount the volume earlier with a
- login hook--but this mechanism appears to be deprecated and its
- future is unclear.
-
-3. You can hard-code the password in the clear, so that your store
- volume can be decrypted before Keychain is available.
-
-If you are comfortable navigating these tradeoffs, you can encrypt the
-volume with something along the lines of:
-
-```console
-$ diskutil apfs enableFileVault /nix -user disk
-```
-
-## Symlink the Nix store to a custom location
-
-Another simple approach is using `/etc/synthetic.conf` to symlink the
-Nix store to the data volume. This option also enables your store to
-share any configured FileVault encryption. Unfortunately, builds that
-resolve the symlink may leak the canonical path or even fail.
-
-Because of these downsides, we can't recommend this approach.
-
-## Notes on the recommended approach
-
-This section goes into a little more detail on the recommended approach.
-You don't need to understand it to run the installer, but it can serve
-as a helpful reference if you run into trouble.
-
-1. In order to compose user-writable locations into the new read-only
- system root, Apple introduced a new concept called `firmlinks`,
- which it describes as a "bi-directional wormhole" between two
- filesystems. You can see the current firmlinks in
- `/usr/share/firmlinks`. Unfortunately, firmlinks aren't (currently?)
- user-configurable.
-
- For special cases like NFS mount points or package manager roots,
- [synthetic.conf(5)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man5/synthetic.conf.5.html)
- supports limited user-controlled file-creation (of symlinks, and
- synthetic empty directories) at `/`. To create a synthetic empty
- directory for mounting at `/nix`, add the following line to
- `/etc/synthetic.conf` (create it if necessary):
-
- nix
-
-2. This configuration is applied at boot time, but you can use
- `apfs.util` to trigger creation (not deletion) of new entries
- without a reboot:
-
- ```console
- $ /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -B
- ```
-
-3. Create the new APFS volume with diskutil:
-
- ```console
- $ sudo diskutil apfs addVolume diskX APFS 'Nix Store' -mountpoint /nix
- ```
-
-4. Using `vifs`, add the new mount to `/etc/fstab`. If it doesn't
- already have other entries, it should look something like:
-
- #
- # Warning - this file should only be modified with vifs(8)
- #
- # Failure to do so is unsupported and may be destructive.
- #
- LABEL=Nix\040Store /nix apfs rw,nobrowse
-
- The nobrowse setting will keep Spotlight from indexing this volume,
- and keep it from showing up on your desktop.
+- create a new APFS volume for your Nix store
+- update `/etc/synthetic.conf` to direct macOS to create a "synthetic"
+ empty root directory to mount your volume
+- specify mount options for the volume in `/etc/fstab`
+- if you have FileVault enabled
+ - generate an encryption password
+ - put it in your system Keychain
+ - use it to encrypt the volume
+- create a system LaunchDaemon to mount this volume early enough in the
+ boot process to avoid problems loading or restoring any programs that
+ need access to your Nix store
# Installing a pinned Nix version from a URL
diff --git a/scripts/create-darwin-volume.sh b/scripts/create-darwin-volume.sh
index 32fa577a8..8aff03199 100755
--- a/scripts/create-darwin-volume.sh
+++ b/scripts/create-darwin-volume.sh
@@ -1,33 +1,262 @@
-#!/bin/sh
-set -e
+#!/usr/bin/env bash
+set -eu
+set -o pipefail
-root_disk() {
- diskutil info -plist /
-}
+# I'm a little agnostic on the choices, but supporting a wide
+# slate of uses for now, including:
+# - import-only: `. create-darwin-volume.sh no-main[ ...]`
+# - legacy: `./create-darwin-volume.sh` or `. create-darwin-volume.sh`
+# (both will run main())
+# - external alt-routine: `./create-darwin-volume.sh no-main func[ ...]`
+if [ "${1-}" = "no-main" ]; then
+ shift
+ readonly _CREATE_VOLUME_NO_MAIN=1
+else
+ readonly _CREATE_VOLUME_NO_MAIN=0
+ # declare some things we expect to inherit from install-multi-user
+ # I don't love this (because it's a bit of a kludge).
+ #
+ # CAUTION: (Dec 19 2020)
+ # This is a stopgap. It doesn't cover the full slate of
+ # identifiers we inherit--just those necessary to:
+ # - avoid breaking direct invocations of this script (here/now)
+ # - avoid hard-to-reverse structural changes before the call to rm
+ # single-user support is verified
+ #
+ # In the near-mid term, I (personally) think we should:
+ # - decide to deprecate the direct call and add a notice
+ # - fold all of this into install-darwin-multi-user.sh
+ # - intentionally remove the old direct-invocation form (kill the
+ # routine, replace this script w/ deprecation notice and a note
+ # on the remove-after date)
+ #
+ readonly NIX_ROOT="${NIX_ROOT:-/nix}"
-# i.e., "disk1"
+ _sudo() {
+ shift # throw away the 'explanation'
+ /usr/bin/sudo "$@"
+ }
+ failure() {
+ if [ "$*" = "" ]; then
+ cat
+ else
+ echo "$@"
+ fi
+ exit 1
+ }
+ task() {
+ echo "$@"
+ }
+fi
+
+# usually "disk1"
root_disk_identifier() {
- diskutil info -plist / | xmllint --xpath "/plist/dict/key[text()='ParentWholeDisk']/following-sibling::string[1]/text()" -
+ # For performance (~10ms vs 280ms) I'm parsing 'diskX' from stat output
+ # (~diskXsY)--but I'm retaining the more-semantic approach since
+ # it documents intent better.
+ # /usr/sbin/diskutil info -plist / | xmllint --xpath "/plist/dict/key[text()='ParentWholeDisk']/following-sibling::string[1]/text()" -
+ #
+ local special_device
+ special_device="$(/usr/bin/stat -f "%Sd" /)"
+ echo "${special_device%s[0-9]*}"
}
-find_nix_volume() {
- diskutil apfs list -plist "$1" | xmllint --xpath "(/plist/dict/array/dict/key[text()='Volumes']/following-sibling::array/dict/key[text()='Name']/following-sibling::string[starts-with(translate(text(),'N','n'),'nix')]/text())[1]" - 2>/dev/null || true
+# make it easy to play w/ 'Case-sensitive APFS'
+readonly NIX_VOLUME_FS="${NIX_VOLUME_FS:-APFS}"
+readonly NIX_VOLUME_LABEL="${NIX_VOLUME_LABEL:-Nix Store}"
+# Strongly assuming we'll make a volume on the device / is on
+# But you can override NIX_VOLUME_USE_DISK to create it on some other device
+readonly NIX_VOLUME_USE_DISK="${NIX_VOLUME_USE_DISK:-$(root_disk_identifier)}"
+NIX_VOLUME_USE_SPECIAL="${NIX_VOLUME_USE_SPECIAL:-}"
+NIX_VOLUME_USE_UUID="${NIX_VOLUME_USE_UUID:-}"
+readonly NIX_VOLUME_MOUNTD_DEST="${NIX_VOLUME_MOUNTD_DEST:-/Library/LaunchDaemons/org.nixos.darwin-store.plist}"
+
+if /usr/bin/fdesetup isactive >/dev/null; then
+ test_filevault_in_use() { return 0; }
+ # no readonly; we may modify if user refuses from cure_volume
+ NIX_VOLUME_DO_ENCRYPT="${NIX_VOLUME_DO_ENCRYPT:-1}"
+else
+ test_filevault_in_use() { return 1; }
+ NIX_VOLUME_DO_ENCRYPT="${NIX_VOLUME_DO_ENCRYPT:-0}"
+fi
+
+should_encrypt_volume() {
+ test_filevault_in_use && (( NIX_VOLUME_DO_ENCRYPT == 1 ))
+}
+
+substep() {
+ printf " %s\n" "" "- $1" "" "${@:2}"
+}
+
+
+volumes_labeled() {
+ local label="$1"
+ xsltproc --novalid --stringparam label "$label" - <(/usr/sbin/ioreg -ra -c "AppleAPFSVolume") <<'EOF'
+
+
+
+
+
+
+
+ =
+
+
+
+
+EOF
+ # I cut label out of the extracted values, but here it is for reference:
+ #
+ # =
+}
+
+right_disk() {
+ local volume_special="$1" # (i.e., disk1s7)
+ [[ "$volume_special" == "$NIX_VOLUME_USE_DISK"s* ]]
+}
+
+right_volume() {
+ local volume_special="$1" # (i.e., disk1s7)
+ # if set, it must match; otherwise ensure it's on the right disk
+ if [ -z "$NIX_VOLUME_USE_SPECIAL" ]; then
+ if right_disk "$volume_special"; then
+ NIX_VOLUME_USE_SPECIAL="$volume_special" # latch on
+ return 0
+ else
+ return 1
+ fi
+ else
+ [ "$volume_special" = "$NIX_VOLUME_USE_SPECIAL" ]
+ fi
+}
+
+right_uuid() {
+ local volume_uuid="$1"
+ # if set, it must match; otherwise allow
+ if [ -z "$NIX_VOLUME_USE_UUID" ]; then
+ NIX_VOLUME_USE_UUID="$volume_uuid" # latch on
+ return 0
+ else
+ [ "$volume_uuid" = "$NIX_VOLUME_USE_UUID" ]
+ fi
+}
+
+cure_volumes() {
+ local found volume special uuid
+ # loop just in case they have more than one volume
+ # (nothing stops you from doing this)
+ for volume in $(volumes_labeled "$NIX_VOLUME_LABEL"); do
+ # CAUTION: this could (maybe) be a more normal read
+ # loop like:
+ # while IFS== read -r special uuid; do
+ # # ...
+ # done <<<"$(volumes_labeled "$NIX_VOLUME_LABEL")"
+ #
+ # I did it with for to skirt a problem with the obvious
+ # pattern replacing stdin and causing user prompts
+ # inside (which also use read and access stdin) to skip
+ #
+ # If there's an existing encrypted volume we can't find
+ # in keychain, the user never gets prompted to delete
+ # the volume, and the install fails.
+ #
+ # If you change this, a human needs to test a very
+ # specific scenario: you already have an encrypted
+ # Nix Store volume, and have deleted its credential
+ # from keychain. Ensure the script asks you if it can
+ # delete the volume, and then prompts for your sudo
+ # password to confirm.
+ #
+ # shellcheck disable=SC1097
+ IFS== read -r special uuid <<< "$volume"
+ # take the first one that's on the right disk
+ if [ -z "${found:-}" ]; then
+ if right_volume "$special" && right_uuid "$uuid"; then
+ cure_volume "$special" "$uuid"
+ found="${special} (${uuid})"
+ else
+ warning <
+ # Cryptographic user for (1 found)
+ # Cryptographic users for (2 found)
+ /usr/sbin/diskutil apfs listCryptoUsers -plist "$volume_special" | /usr/bin/grep -q APFSCryptoUserUUID
}
test_fstab() {
- grep -q "/nix apfs rw" /etc/fstab 2>/dev/null
+ /usr/bin/grep -q "$NIX_ROOT apfs rw" /etc/fstab 2>/dev/null
}
-test_nix_symlink() {
- [ -L "/nix" ] || grep -q "^nix." /etc/synthetic.conf 2>/dev/null
+test_nix_root_is_symlink() {
+ [ -L "$NIX_ROOT" ]
}
-test_synthetic_conf() {
- grep -q "^nix$" /etc/synthetic.conf 2>/dev/null
+test_synthetic_conf_either(){
+ /usr/bin/grep -qE "^${NIX_ROOT:1}($|\t.{3,}$)" /etc/synthetic.conf 2>/dev/null
+}
+
+test_synthetic_conf_mountable() {
+ /usr/bin/grep -q "^${NIX_ROOT:1}$" /etc/synthetic.conf 2>/dev/null
+}
+
+test_synthetic_conf_symlinked() {
+ /usr/bin/grep -qE "^${NIX_ROOT:1}\t.{3,}$" /etc/synthetic.conf 2>/dev/null
+}
+
+test_nix_volume_mountd_installed() {
+ test -e "$NIX_VOLUME_MOUNTD_DEST"
+}
+
+# current volume password
+test_keychain_by_uuid() {
+ local volume_uuid="$1"
+ # Note: doesn't need sudo just to check; doesn't output pw
+ security find-generic-password -s "$volume_uuid" &>/dev/null
+}
+
+get_volume_pass() {
+ local volume_uuid="$1"
+ _sudo \
+ "to confirm keychain has a password that unlocks this volume" \
+ security find-generic-password -s "$volume_uuid" -w
+}
+
+verify_volume_pass() {
+ local volume_special="$1" # (i.e., disk1s7)
+ local volume_uuid="$2"
+ /usr/sbin/diskutil apfs unlockVolume "$volume_special" -verify -stdinpassphrase -user "$volume_uuid"
+}
+
+volume_pass_works() {
+ local volume_special="$1" # (i.e., disk1s7)
+ local volume_uuid="$2"
+ get_volume_pass "$volume_uuid" | verify_volume_pass "$volume_special" "$volume_uuid"
}
# Create the paths defined in synthetic.conf, saving us a reboot.
-create_synthetic_objects(){
+create_synthetic_objects() {
# Big Sur takes away the -B flag we were using and replaces it
# with a -t flag that appears to do the same thing (but they
# don't behave exactly the same way in terms of return values).
@@ -41,129 +270,570 @@ create_synthetic_objects(){
}
test_nix() {
- test -d "/nix"
+ test -d "$NIX_ROOT"
}
-test_t2_chip_present(){
- # Use xartutil to see if system has a t2 chip.
- #
- # This isn't well-documented on its own; until it is,
- # let's keep track of knowledge/assumptions.
- #
- # Warnings:
- # - Don't search "xart" if porn will cause you trouble :)
- # - Other xartutil flags do dangerous things. Don't run them
- # naively. If you must, search "xartutil" first.
- #
- # Assumptions:
- # - the "xART session seeds recovery utility"
- # appears to interact with xartstorageremoted
- # - `sudo xartutil --list` lists xART sessions
- # and their seeds and exits 0 if successful. If
- # not, it exits 1 and prints an error such as:
- # xartutil: ERROR: No supported link to the SEP present
- # - xART sessions/seeds are present when a T2 chip is
- # (and not, otherwise)
- # - the presence of a T2 chip means a newly-created
- # volume on the primary drive will be
- # encrypted at rest
- # - all together: `sudo xartutil --list`
- # should exit 0 if a new Nix Store volume will
- # be encrypted at rest, and exit 1 if not.
- sudo xartutil --list >/dev/null 2>/dev/null
+test_voldaemon() {
+ test -f "$NIX_VOLUME_MOUNTD_DEST"
}
-test_filevault_in_use() {
- fdesetup isactive >/dev/null
+generate_mount_command() {
+ local cmd_type="$1" # encrypted|unencrypted
+ local volume_uuid mountpoint cmd=()
+ printf -v volume_uuid "%q" "$2"
+ printf -v mountpoint "%q" "$NIX_ROOT"
+
+ case "$cmd_type" in
+ encrypted)
+ cmd=(/bin/sh -c "/usr/bin/security find-generic-password -s '$volume_uuid' -w | /usr/sbin/diskutil apfs unlockVolume '$volume_uuid' -mountpoint '$mountpoint' -stdinpassphrase");;
+ unencrypted)
+ cmd=(/usr/sbin/diskutil mount -mountPoint "$mountpoint" "$volume_uuid");;
+ *)
+ failure "Invalid first arg $cmd_type to generate_mount_command";;
+ esac
+
+ printf " %s\n" "${cmd[@]}"
}
-# use after error msg for conditions we don't understand
-suggest_report_error(){
- # ex "error: something sad happened :(" >&2
- echo " please report this @ https://github.com/nixos/nix/issues" >&2
+generate_mount_daemon() {
+ local cmd_type="$1" # encrypted|unencrypted
+ local volume_uuid="$2"
+ cat <
+
+
+
+ RunAtLoad
+
+ Label
+ org.nixos.darwin-store
+ ProgramArguments
+
+$(generate_mount_command "$cmd_type" "$volume_uuid")
+
+
+
+EOF
}
-main() {
- (
- echo ""
- echo " ------------------------------------------------------------------ "
- echo " | This installer will create a volume for the nix store and |"
- echo " | configure it to mount at /nix. Follow these steps to uninstall. |"
- echo " ------------------------------------------------------------------ "
- echo ""
- echo " 1. Remove the entry from fstab using 'sudo vifs'"
- echo " 2. Destroy the data volume using 'diskutil apfs deleteVolume'"
- echo " 3. Remove the 'nix' line from /etc/synthetic.conf or the file"
- echo ""
- ) >&2
+_eat_bootout_err() {
+ /usr/bin/grep -v "Boot-out failed: 36: Operation now in progress"
+}
- if test_nix_symlink; then
- echo "error: /nix is a symlink, please remove it and make sure it's not in synthetic.conf (in which case a reboot is required)" >&2
- echo " /nix -> $(readlink "/nix")" >&2
- exit 2
+# TODO: remove with --uninstall?
+uninstall_launch_daemon_directions() {
+ local daemon_label="$1" # i.e., org.nixos.blah-blah
+ local daemon_plist="$2" # abspath
+ substep "Uninstall LaunchDaemon $daemon_label" \
+ " sudo launchctl bootout system/$daemon_label" \
+ " sudo rm $daemon_plist"
+}
+
+uninstall_launch_daemon_prompt() {
+ local daemon_label="$1" # i.e., org.nixos.blah-blah
+ local daemon_plist="$2" # abspath
+ local reason_for_daemon="$3"
+ cat < >(_eat_bootout_err >&2) || true
+ # this can "fail" with a message like:
+ # Boot-out failed: 36: Operation now in progress
+ _sudo "to remove the daemon definition" rm "$daemon_plist"
+ fi
+}
+
+nix_volume_mountd_uninstall_directions() {
+ uninstall_launch_daemon_directions "org.nixos.darwin-store" \
+ "$NIX_VOLUME_MOUNTD_DEST"
+}
+
+nix_volume_mountd_uninstall_prompt() {
+ uninstall_launch_daemon_prompt "org.nixos.darwin-store" \
+ "$NIX_VOLUME_MOUNTD_DEST" \
+ "mount your Nix volume"
+}
+
+# TODO: move nix_daemon to install-darwin-multi-user if/when uninstall_launch_daemon_prompt moves up to install-multi-user
+nix_daemon_uninstall_prompt() {
+ uninstall_launch_daemon_prompt "org.nixos.nix-daemon" \
+ "$NIX_DAEMON_DEST" \
+ "run the nix-daemon"
+}
+
+# TODO: remove with --uninstall?
+nix_daemon_uninstall_directions() {
+ uninstall_launch_daemon_directions "org.nixos.nix-daemon" \
+ "$NIX_DAEMON_DEST"
+}
+
+
+# TODO: remove with --uninstall?
+synthetic_conf_uninstall_directions() {
+ # :1 to strip leading slash
+ substep "Remove ${NIX_ROOT:1} from /etc/synthetic.conf" \
+ " If nix is the only entry: sudo rm /etc/synthetic.conf" \
+ " Otherwise: sudo /usr/bin/sed -i '' -e '/^${NIX_ROOT:1}$/d' /etc/synthetic.conf"
+}
+
+synthetic_conf_uninstall_prompt() {
+ cat < "$SCRATCH/synthetic.conf.edit"
+
+ if test_synthetic_conf_symlinked; then
+ warning <&2
- echo nix | sudo tee -a /etc/synthetic.conf
- if ! test_synthetic_conf; then
- echo "error: failed to configure synthetic.conf;" >&2
- suggest_report_error
- exit 1
+ # ask to rm if this left the file empty aside from comments, else edit
+ if /usr/bin/diff -q <(:) <(/usr/bin/grep -v "^#" "$SCRATCH/synthetic.conf.edit") &>/dev/null; then
+ if confirm_rm "/etc/synthetic.conf"; then
+ if test_nix_root_is_symlink; then
+ failure >&2 < $(readlink "$NIX_ROOT")). The system should remove it when you reboot.
+Once you've rebooted, run the installer again.
+EOF
+ fi
+ return 0
+ fi
+ else
+ if confirm_edit "$SCRATCH/synthetic.conf.edit" "/etc/synthetic.conf"; then
+ if test_nix_root_is_symlink; then
+ failure >&2 < $(readlink "$NIX_ROOT")). The system should remove it when you reboot.
+Once you've rebooted, run the installer again.
+EOF
+ fi
+ return 0
fi
fi
+ # fallback instructions
+ echo "Manually remove nix from /etc/synthetic.conf"
+ return 1
+}
- if ! test_nix; then
- echo "Creating mountpoint for /nix..." >&2
- create_synthetic_objects # the ones we defined in synthetic.conf
- if ! test_nix; then
- sudo mkdir -p /nix 2>/dev/null || true
+add_nix_vol_fstab_line() {
+ local uuid="$1"
+ # shellcheck disable=SC1003,SC2026
+ local escaped_mountpoint="${NIX_ROOT/ /'\\\'040}"
+ shift
+ EDITOR="/usr/bin/ex" _sudo "to add nix to fstab" "$@" <multi-user reinstalls, which may cover this)
+ #
+ # I'm not sure if it's safe to approach this way?
+ #
+ # I think I think the most-proper way to test for it is:
+ # diskutil info -plist "$NIX_VOLUME_LABEL" | xmllint --xpath "(/plist/dict/key[text()='GlobalPermissionsEnabled'])/following-sibling::*[1][name()='true']" -; echo $?
+ #
+ # There's also `sudo /usr/sbin/vsdbutil -c /path` (which is much faster, but is also
+ # deprecated and needs minor parsing).
+ #
+ # If no one finds a problem with doing so, I think the simplest approach
+ # is to just eagerly set this. I found a few imperative approaches:
+ # (diskutil enableOwnership, ~100ms), a cheap one (/usr/sbin/vsdbutil -a, ~40-50ms),
+ # a very cheap one (append the internal format to /var/db/volinfo.database).
+ #
+ # But vsdbutil's deprecation notice suggests using fstab, so I want to
+ # give that a whirl first.
+ #
+ # TODO: when this is workable, poke infinisil about reproducing the issue
+ # and confirming this fix?
+}
+
+delete_nix_vol_fstab_line() {
+ # TODO: I'm scaffolding this to handle the new nix volumes
+ # but it might be nice to generalize a smidge further to
+ # go ahead and set up a pattern for curing "old" things
+ # we no longer do?
+ EDITOR="/usr/bin/patch" _sudo "to cut nix from fstab" "$@" < <(/usr/bin/diff /etc/fstab <(/usr/bin/grep -v "$NIX_ROOT apfs rw" /etc/fstab))
+ # leaving some parts out of the grep; people may fiddle this a little?
+}
+
+# TODO: hope to remove with --uninstall
+fstab_uninstall_directions() {
+ substep "Remove ${NIX_ROOT} from /etc/fstab" \
+ " If nix is the only entry: sudo rm /etc/fstab" \
+ " Otherwise, run 'sudo /usr/sbin/vifs' to remove the nix line"
+}
+
+fstab_uninstall_prompt() {
+ cat </dev/null
+
+ # if the patch test edit, minus comment lines, is equal to empty (:)
+ if /usr/bin/diff -q <(:) <(/usr/bin/grep -v "^#" "$SCRATCH/fstab.edit") &>/dev/null; then
+ # this edit would leave it empty; propose deleting it
+ if confirm_rm "/etc/fstab"; then
+ return 0
+ else
+ echo "Remove nix from /etc/fstab (or remove the file)"
fi
- if ! test_nix; then
- echo "error: failed to bootstrap /nix; if a reboot doesn't help," >&2
- suggest_report_error
- exit 1
+ else
+ echo "I might be able to help you make this edit. Here's the diff:"
+ if ! _diff "/etc/fstab" "$SCRATCH/fstab.edit" && ui_confirm "Does the change above look right?"; then
+ delete_nix_vol_fstab_line /usr/sbin/vifs
+ else
+ echo "Remove nix from /etc/fstab (or remove the file)"
fi
fi
+}
- disk="$(root_disk_identifier)"
- volume=$(find_nix_volume "$disk")
- if [ -z "$volume" ]; then
- echo "Creating a Nix Store volume..." >&2
+remove_volume() {
+ local volume_special="$1" # (i.e., disk1s7)
+ _sudo "to unmount the Nix volume" \
+ /usr/sbin/diskutil unmount force "$volume_special" || true # might not be mounted
+ _sudo "to delete the Nix volume" \
+ /usr/sbin/diskutil apfs deleteVolume "$volume_special"
+}
- if test_filevault_in_use; then
- # TODO: Not sure if it's in-scope now, but `diskutil apfs list`
- # shows both filevault and encrypted at rest status, and it
- # may be the more semantic way to test for this? It'll show
- # `FileVault: No (Encrypted at rest)`
- # `FileVault: No`
- # `FileVault: Yes (Unlocked)`
- # and so on.
- if test_t2_chip_present; then
- echo "warning: boot volume is FileVault-encrypted, but the Nix store volume" >&2
- echo " is only encrypted at rest." >&2
- echo " See https://nixos.org/nix/manual/#sect-macos-installation" >&2
+# aspiration: robust enough to both fix problems
+# *and* update older darwin volumes
+cure_volume() {
+ local volume_special="$1" # (i.e., disk1s7)
+ local volume_uuid="$2"
+ header "Found existing Nix volume"
+ row " special" "$volume_special"
+ row " uuid" "$volume_uuid"
+
+ if volume_encrypted "$volume_special"; then
+ row "encrypted" "yes"
+ if volume_pass_works "$volume_special" "$volume_uuid"; then
+ NIX_VOLUME_DO_ENCRYPT=0
+ ok "Found a working decryption password in keychain :)"
+ echo ""
+ else
+ # - this is a volume we made, and
+ # - the user encrypted it on their own
+ # - something deleted the credential
+ # - this is an old or BYO volume and the pw
+ # just isn't somewhere we can find it.
+ #
+ # We're going to explain why we're freaking out
+ # and prompt them to either delete the volume
+ # (requiring a sudo auth), or abort to fix
+ warning <&2
- echo " FileVault encrypted, but encryption-at-rest is not available." >&2
- echo " Manually create a volume for the store and re-run this script." >&2
- echo " See https://nixos.org/nix/manual/#sect-macos-installation" >&2
- exit 1
+ # TODO: this is a good design case for a warn-and
+ # remind idiom...
+ failure <&2
- fi
-
- if ! test_fstab; then
- echo "Configuring /etc/fstab..." >&2
- label=$(echo "$volume" | sed 's/ /\\040/g')
- # shellcheck disable=SC2209
- printf "\$a\nLABEL=%s /nix apfs rw,nobrowse\n.\nwq\n" "$label" | EDITOR=ed sudo vifs
+ row "encrypted" "no"
fi
}
-main "$@"
+remove_volume_artifacts() {
+ if test_synthetic_conf_either; then
+ # NIX_ROOT is in synthetic.conf
+ if synthetic_conf_uninstall_prompt; then
+ # TODO: moot until we tackle uninstall, but when we're
+ # actually uninstalling, we should issue:
+ # reminder "macOS will clean up the empty mount-point directory at $NIX_ROOT on reboot."
+ :
+ fi
+ fi
+ if test_fstab; then
+ fstab_uninstall_prompt
+ fi
+
+ if test_nix_volume_mountd_installed; then
+ nix_volume_mountd_uninstall_prompt
+ fi
+}
+
+setup_synthetic_conf() {
+ if test_nix_root_is_symlink; then
+ if ! test_synthetic_conf_symlinked; then
+ failure >&2 < $(readlink "$NIX_ROOT")).
+Please remove it. If nix is in /etc/synthetic.conf, remove it and reboot.
+EOF
+ fi
+ fi
+ if ! test_synthetic_conf_mountable; then
+ task "Configuring /etc/synthetic.conf to make a mount-point at $NIX_ROOT" >&2
+ # technically /etc/synthetic.d/nix is supported in Big Sur+
+ # but handling both takes even more code...
+ _sudo "to add Nix to /etc/synthetic.conf" \
+ /usr/bin/ex /etc/synthetic.conf <&2
+ fi
+ create_synthetic_objects
+ if ! test_nix; then
+ failure >&2 <&2
+ add_nix_vol_fstab_line "$volume_uuid" /usr/sbin/vifs
+ fi
+}
+
+encrypt_volume() {
+ local volume_uuid="$1"
+ local volume_label="$2"
+ local password
+ # Note: mount/unmount are late additions to support the right order
+ # of operations for creating the volume and then baking its uuid into
+ # other artifacts; not as well-trod wrt to potential errors, race
+ # conditions, etc.
+
+ /usr/sbin/diskutil mount "$volume_label"
+
+ password="$(/usr/bin/xxd -l 32 -p -c 256 /dev/random)"
+ _sudo "to add your Nix volume's password to Keychain" \
+ /usr/bin/security -i </dev/null; do
+ :
+ done
+}
+
+setup_volume() {
+ local use_special use_uuid profile_packages
+ task "Creating a Nix volume" >&2
+ # DOING: I'm tempted to wrap this call in a grep to get the new disk special without doing anything too complex, but this sudo wrapper *is* a little complex, so it'll be a PITA unless maybe we can skip sudo on this. Let's just try it without.
+
+ use_special="${NIX_VOLUME_USE_SPECIAL:-$(create_volume)}"
+
+ use_uuid=${NIX_VOLUME_USE_UUID:-$(volume_uuid_from_special "$use_special")}
+
+ setup_fstab "$use_uuid"
+
+ if should_encrypt_volume; then
+ encrypt_volume "$use_uuid" "$NIX_VOLUME_LABEL"
+ setup_volume_daemon "encrypted" "$use_uuid"
+ # TODO: might be able to save ~60ms by caching or setting
+ # this somewhere rather than re-checking here.
+ elif volume_encrypted "$use_special"; then
+ setup_volume_daemon "encrypted" "$use_uuid"
+ else
+ setup_volume_daemon "unencrypted" "$use_uuid"
+ fi
+
+ await_volume
+
+ # TODO: below is a vague kludge for now; I just don't know
+ # what if any safe action there is to take here. Also, the
+ # reminder isn't very helpful.
+ # I'm less sure where this belongs, but it also wants mounted, pre-install
+ if type -p nix-env; then
+ profile_packages="$(nix-env --query --installed)"
+ # TODO: can probably do below faster w/ read
+ # intentionally unquoted string to eat whitespace in wc output
+ # shellcheck disable=SC2046,SC2059
+ if ! [ $(printf "$profile_packages" | /usr/bin/wc -l) = "0" ]; then
+ reminder <&2
+ _sudo "to install the Nix volume mounter" /usr/bin/ex "$NIX_VOLUME_MOUNTD_DEST" <&2
+
+ setup_darwin_volume
+ }
+
+ main "$@"
+fi
diff --git a/scripts/install-darwin-multi-user.sh b/scripts/install-darwin-multi-user.sh
index f6575ae2f..f8d6c5e8f 100644
--- a/scripts/install-darwin-multi-user.sh
+++ b/scripts/install-darwin-multi-user.sh
@@ -3,59 +3,99 @@
set -eu
set -o pipefail
-readonly PLIST_DEST=/Library/LaunchDaemons/org.nixos.nix-daemon.plist
+readonly NIX_DAEMON_DEST=/Library/LaunchDaemons/org.nixos.nix-daemon.plist
+# create by default; set 0 to DIY, use a symlink, etc.
+readonly NIX_VOLUME_CREATE=${NIX_VOLUME_CREATE:-1} # now default
NIX_FIRST_BUILD_UID="301"
NIX_BUILD_USER_NAME_TEMPLATE="_nixbld%d"
-dsclattr() {
- /usr/bin/dscl . -read "$1" \
- | awk "/$2/ { print \$2 }"
+# caution: may update times on / if not run as normal non-root user
+read_only_root() {
+ # this touch command ~should~ always produce an error
+ # as of this change I confirmed /usr/bin/touch emits:
+ # "touch: /: Read-only file system" Catalina+ and Big Sur
+ # "touch: /: Permission denied" Mojave
+ # (not matching prefix for compat w/ coreutils touch in case using
+ # an explicit path causes problems; its prefix differs)
+ [[ "$(/usr/bin/touch / 2>&1)" = *"Read-only file system" ]]
+
+ # Avoiding the slow semantic way to get this information (~330ms vs ~8ms)
+ # unless using touch causes problems. Just in case, that approach is:
+ # diskutil info -plist / | , i.e.
+ # diskutil info -plist / | xmllint --xpath "name(/plist/dict/key[text()='Writable']/following-sibling::*[1])" -
}
-poly_validate_assumptions() {
- if [ "$(uname -s)" != "Darwin" ]; then
- failure "This script is for use with macOS!"
+if read_only_root && [ "$NIX_VOLUME_CREATE" = 1 ]; then
+ should_create_volume() { return 0; }
+else
+ should_create_volume() { return 1; }
+fi
+
+# shellcheck source=./create-darwin-volume.sh
+. "$EXTRACTED_NIX_PATH/create-darwin-volume.sh" "no-main"
+
+dsclattr() {
+ /usr/bin/dscl . -read "$1" \
+ | /usr/bin/awk "/$2/ { print \$2 }"
+}
+
+test_nix_daemon_installed() {
+ test -e "$NIX_DAEMON_DEST"
+}
+
+poly_cure_artifacts() {
+ if should_create_volume; then
+ task "Fixing any leftover Nix volume state"
+ cat < /dev/null 2>&1
+ /usr/sbin/dseditgroup -o checkmember -m "$username" "$group" > /dev/null 2>&1
}
poly_user_in_group_set() {
@@ -151,3 +193,17 @@ poly_create_build_user() {
/usr/bin/dscl . create "/Users/$username" \
UniqueID "${uid}"
}
+
+poly_prepare_to_install() {
+ if should_create_volume; then
+ header "Preparing a Nix volume"
+ # intentional indent below to match task indent
+ cat < 1 )); then
+ header "Reminders"
+ for line in "${_reminders[@]}"; do
+ echo "$line"
+ if ! headless && [ "${#line}" = 0 ]; then
+ if read -r -p "Press enter/return to acknowledge."; then
+ printf $'\033[A\33[2K\r'
+ fi
+ fi
+ done
+ fi
+}
+
+reminder() {
+ printf -v label "${BLUE}[ %d ]${ESC}" "$_remind_num"
+ _reminders+=("$label")
+ if [[ "$*" = "" ]]; then
+ while read -r line; do
+ _reminders+=("$line")
+ done
+ else
+ # this expands each arg to an array entry (and each entry will
+ # ultimately be a separate line in the output)
+ _reminders+=("$@")
+ fi
+ _reminders+=("")
+ ((_remind_num++))
+}
+
__sudo() {
local expl="$1"
local cmd="$2"
@@ -221,18 +313,18 @@ _sudo() {
local expl="$1"
shift
if ! headless; then
- __sudo "$expl" "$*"
+ __sudo "$expl" "$*" >&2
fi
sudo "$@"
}
-readonly SCRATCH=$(mktemp -d -t tmp.XXXXXXXXXX)
-function finish_cleanup {
+readonly SCRATCH=$(mktemp -d "${TMPDIR:-/tmp/}tmp.XXXXXXXXXX")
+finish_cleanup() {
rm -rf "$SCRATCH"
}
-function finish_fail {
+finish_fail() {
finish_cleanup
failure < /dev/null >&2; then
warning <&2
-elif [ "$(uname -s)" = "Linux" ]; then
+if [ "$(uname -s)" = "Linux" ]; then
echo "Note: a multi-user installation is possible. See https://nixos.org/nix/manual/#sect-multi-user-installation" >&2
fi
-INSTALL_MODE=no-daemon
-CREATE_DARWIN_VOLUME=0
+case "$(uname -s)" in
+ "Darwin")
+ INSTALL_MODE=daemon;;
+ *)
+ INSTALL_MODE=no-daemon;;
+esac
+
+# space-separated string
+ACTIONS=
+
# handle the command line flags
while [ $# -gt 0 ]; do
case $1 in
--daemon)
- INSTALL_MODE=daemon;;
+ INSTALL_MODE=daemon
+ ACTIONS="${ACTIONS}install "
+ ;;
--no-daemon)
- INSTALL_MODE=no-daemon;;
+ if [ "$(uname -s)" = "Darwin" ]; then
+ printf '\e[1;31mError: --no-daemon installs are no-longer supported on Darwin/macOS!\e[0m\n' >&2
+ exit 1
+ fi
+ INSTALL_MODE=no-daemon
+ # intentional tail space
+ ACTIONS="${ACTIONS}install "
+ ;;
+ # --uninstall)
+ # # intentional tail space
+ # ACTIONS="${ACTIONS}uninstall "
+ # ;;
--no-channel-add)
export NIX_INSTALLER_NO_CHANNEL_ADD=1;;
--daemon-user-count)
@@ -69,13 +79,18 @@ while [ $# -gt 0 ]; do
--no-modify-profile)
NIX_INSTALLER_NO_MODIFY_PROFILE=1;;
--darwin-use-unencrypted-nix-store-volume)
- CREATE_DARWIN_VOLUME=1;;
+ {
+ echo "Warning: the flag --darwin-use-unencrypted-nix-store-volume"
+ echo " is no longer needed and will be removed in the future."
+ echo ""
+ } >&2;;
--nix-extra-conf-file)
- export NIX_EXTRA_CONF="$(cat $2)"
+ # shellcheck disable=SC2155
+ export NIX_EXTRA_CONF="$(cat "$2")"
shift;;
*)
- (
- echo "Nix Installer [--daemon|--no-daemon] [--daemon-user-count INT] [--no-channel-add] [--no-modify-profile] [--darwin-use-unencrypted-nix-store-volume] [--nix-extra-conf-file FILE]"
+ {
+ echo "Nix Installer [--daemon|--no-daemon] [--daemon-user-count INT] [--no-channel-add] [--no-modify-profile] [--nix-extra-conf-file FILE]"
echo "Choose installation method."
echo ""
@@ -101,45 +116,16 @@ while [ $# -gt 0 ]; do
if [ -n "${INVOKED_FROM_INSTALL_IN:-}" ]; then
echo " --tarball-url-prefix URL: Base URL to download the Nix tarball from."
fi
- ) >&2
+ } >&2
- # darwin and Catalina+
- if [ "$(uname -s)" = "Darwin" ] && { [ "$macos_major" -gt 10 ] || { [ "$macos_major" -eq 10 ] && [ "$macos_minor" -gt 14 ]; }; }; then
- (
- echo " --darwin-use-unencrypted-nix-store-volume: Create an APFS volume for the Nix"
- echo " store and mount it at /nix. This is the recommended way to create"
- echo " /nix with a read-only / on macOS >=10.15."
- echo " See: https://nixos.org/nix/manual/#sect-macos-installation"
- echo ""
- ) >&2
- fi
exit;;
esac
shift
done
-if [ "$(uname -s)" = "Darwin" ]; then
- if [ "$CREATE_DARWIN_VOLUME" = 1 ]; then
- printf '\e[1;31mCreating volume and mountpoint /nix.\e[0m\n'
- "$self/create-darwin-volume.sh"
- fi
-
- writable="$(diskutil info -plist / | xmllint --xpath "name(/plist/dict/key[text()='Writable']/following-sibling::*[1])" -)"
- if ! [ -e $dest ] && [ "$writable" = "false" ]; then
- (
- echo ""
- echo "Installing on macOS >=10.15 requires relocating the store to an apfs volume."
- echo "Use sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume or run the preparation steps manually."
- echo "See https://nixos.org/nix/manual/#sect-macos-installation"
- echo ""
- ) >&2
- exit 1
- fi
-fi
-
if [ "$INSTALL_MODE" = "daemon" ]; then
printf '\e[1;31mSwitching to the Multi-user Installer\e[0m\n'
- exec "$self/install-multi-user"
+ exec "$self/install-multi-user" $ACTIONS # let ACTIONS split
exit 0
fi
@@ -194,6 +180,7 @@ if ! "$nix/bin/nix-store" --load-db < "$self/.reginfo"; then
exit 1
fi
+# shellcheck source=./nix-profile.sh.in
. "$nix/etc/profile.d/nix.sh"
if ! "$nix/bin/nix-env" -i "$nix"; then
diff --git a/scripts/install-systemd-multi-user.sh b/scripts/install-systemd-multi-user.sh
index fda5ef600..81c61b2a0 100755
--- a/scripts/install-systemd-multi-user.sh
+++ b/scripts/install-systemd-multi-user.sh
@@ -41,10 +41,8 @@ handle_network_proxy() {
fi
}
-poly_validate_assumptions() {
- if [ "$(uname -s)" != "Linux" ]; then
- failure "This script is for use with Linux!"
- fi
+poly_cure_artifacts() {
+ :
}
poly_service_installed_check() {
@@ -72,7 +70,7 @@ poly_service_setup_note() {
EOF
}
-poly_extra_try_me_commands(){
+poly_extra_try_me_commands() {
if [ -e /run/systemd/system ]; then
:
else
@@ -81,19 +79,10 @@ poly_extra_try_me_commands(){
EOF
fi
}
-poly_extra_setup_instructions(){
- if [ -e /run/systemd/system ]; then
- :
- else
- cat <