aboutsummaryrefslogtreecommitdiff
path: root/scripts/create-darwin-volume.sh
diff options
context:
space:
mode:
authorJohn Ericson <John.Ericson@Obsidian.Systems>2021-10-14 20:07:20 +0000
committerJohn Ericson <John.Ericson@Obsidian.Systems>2021-10-14 20:07:20 +0000
commit7869be49c2735280ceabbd13c087b4a06444ae63 (patch)
treea93530cdfba9f3056f727de26548c71157de19e5 /scripts/create-darwin-volume.sh
parent2dd11f07808c8b86513260b85f35a70cbe9b1249 (diff)
parent4c0cde95ad8dc95f876e5cf32790e73e08f49b28 (diff)
Merge remote-tracking branch 'upstream/master' into trustless-remote-builder-simple
Diffstat (limited to 'scripts/create-darwin-volume.sh')
-rwxr-xr-xscripts/create-darwin-volume.sh923
1 files changed, 799 insertions, 124 deletions
diff --git a/scripts/create-darwin-volume.sh b/scripts/create-darwin-volume.sh
index 32fa577a8..b52232dd3 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}"
+
+ _sudo() {
+ shift # throw away the 'explanation'
+ /usr/bin/sudo "$@"
+ }
+ failure() {
+ if [ "$*" = "" ]; then
+ cat
+ else
+ echo "$@"
+ fi
+ exit 1
+ }
+ task() {
+ echo "$@"
+ }
+fi
-# i.e., "disk1"
+# 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]*}"
+}
+
+# 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 ))
}
-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
+substep() {
+ printf " %s\n" "" "- $1" "" "${@:2}"
+}
+
+
+volumes_labeled() {
+ local label="$1"
+ xsltproc --novalid --stringparam label "$label" - <(/usr/sbin/ioreg -ra -c "AppleAPFSVolume") <<'EOF'
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
+ <xsl:output method="text"/>
+ <xsl:template match="/">
+ <xsl:apply-templates select="/plist/array/dict/key[text()='IORegistryEntryName']/following-sibling::*[1][text()=$label]/.."/>
+ </xsl:template>
+ <xsl:template match="dict">
+ <xsl:apply-templates match="string" select="key[text()='BSD Name']/following-sibling::*[1]"/>
+ <xsl:text>=</xsl:text>
+ <xsl:apply-templates match="string" select="key[text()='UUID']/following-sibling::*[1]"/>
+ <xsl:text>&#xA;</xsl:text>
+ </xsl:template>
+</xsl:stylesheet>
+EOF
+ # I cut label out of the extracted values, but here it is for reference:
+ # <xsl:apply-templates match="string" select="key[text()='IORegistryEntryName']/following-sibling::*[1]"/>
+ # <xsl:text>=</xsl:text>
+}
+
+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 <<EOF
+Ignoring ${special} (${uuid}) because I am looking for:
+disk=${NIX_VOLUME_USE_DISK} special=${NIX_VOLUME_USE_SPECIAL:-${NIX_VOLUME_USE_DISK}sX} uuid=${NIX_VOLUME_USE_UUID:-any}
+EOF
+ # TODO: give chance to delete if ! headless?
+ fi
+ else
+ warning <<EOF
+Ignoring ${special} (${uuid}), already found target: $found
+EOF
+ # TODO reminder? I feel like I want one
+ # idiom that reminds some warnings, or warns
+ # some reminders?
+ # TODO: if ! headless, chance to delete?
+ fi
+ done
+ if [ -z "${found:-}" ]; then
+ readonly NIX_VOLUME_USE_SPECIAL NIX_VOLUME_USE_UUID
+ fi
+}
+
+volume_encrypted() {
+ local volume_special="$1" # (i.e., disk1s7)
+ # Trying to match the first line of output; known first lines:
+ # No cryptographic users for <special>
+ # Cryptographic user for <special> (1 found)
+ # Cryptographic users for <special> (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_root_is_symlink() {
+ [ -L "$NIX_ROOT" ]
+}
+
+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"
}
-test_nix_symlink() {
- [ -L "/nix" ] || grep -q "^nix." /etc/synthetic.conf 2>/dev/null
+# 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
}
-test_synthetic_conf() {
- grep -q "^nix$" /etc/synthetic.conf 2>/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,575 @@ create_synthetic_objects(){
}
test_nix() {
- test -d "/nix"
-}
-
-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_filevault_in_use() {
- fdesetup isactive >/dev/null
-}
-
-# 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
-}
-
-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
-
- 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
- fi
-
- if ! test_synthetic_conf; then
- echo "Configuring /etc/synthetic.conf..." >&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
+ test -d "$NIX_ROOT"
+}
+
+test_voldaemon() {
+ test -f "$NIX_VOLUME_MOUNTD_DEST"
+}
+
+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 " <string>%s</string>\n" "${cmd[@]}"
+}
+
+generate_mount_daemon() {
+ local cmd_type="$1" # encrypted|unencrypted
+ local volume_uuid="$2"
+ cat <<EOF
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>RunAtLoad</key>
+ <true/>
+ <key>Label</key>
+ <string>org.nixos.darwin-store</string>
+ <key>ProgramArguments</key>
+ <array>
+$(generate_mount_command "$cmd_type" "$volume_uuid")
+ </array>
+</dict>
+</plist>
+EOF
+}
+
+_eat_bootout_err() {
+ /usr/bin/grep -v "Boot-out failed: 36: Operation now in progress"
+}
+
+# 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 <<EOF
+
+The installer adds a LaunchDaemon to $reason_for_daemon: $daemon_label
+EOF
+ if ui_confirm "Can I remove it?"; then
+ _sudo "to terminate the daemon" \
+ launchctl bootout "system/$daemon_label" 2> >(_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 <<EOF
+
+During install, I add '${NIX_ROOT:1}' to /etc/synthetic.conf, which instructs
+macOS to create an empty root directory for mounting the Nix volume.
+EOF
+ # make the edit to a copy
+ /usr/bin/grep -vE "^${NIX_ROOT:1}($|\t.{3,}$)" /etc/synthetic.conf > "$SCRATCH/synthetic.conf.edit"
+
+ if test_synthetic_conf_symlinked; then
+ warning <<EOF
+
+/etc/synthetic.conf already contains a line instructing your system
+to make '${NIX_ROOT}' as a symlink:
+ $(/usr/bin/grep -nE "^${NIX_ROOT:1}\t.{3,}$" /etc/synthetic.conf)
+
+This may mean your system has/had a non-standard Nix install.
+
+The volume-creation process in this installer is *not* compatible
+with a symlinked store, so I'll have to remove this instruction to
+continue.
+
+If you want/need to keep this instruction, answer 'n' to abort.
+
+EOF
+ fi
+
+ # 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 <<EOF
+I removed /etc/synthetic.conf, but $NIX_ROOT is already a symlink
+(-> $(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 <<EOF
+I edited Nix out of /etc/synthetic.conf, but $NIX_ROOT is already a symlink
+(-> $(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" "$@" <<EOF
+:a
+UUID=$uuid $escaped_mountpoint apfs rw,noauto,nobrowse,suid,owners
+.
+:x
+EOF
+ # TODO: preserving my notes on suid,owners above until resolved
+ # There *may* be some issue regarding volume ownership, see nix#3156
+ #
+ # It seems like the cheapest fix is adding "suid,owners" to fstab, but:
+ # - We don't have much info on this condition yet
+ # - I'm not certain if these cause other problems?
+ # - There's a "chown" component some people claim to need to fix this
+ # that I don't understand yet
+ # (Note however that I've had to add a chown step to handle
+ # single->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 <<EOF
+During install, I add '${NIX_ROOT}' to /etc/fstab so that macOS knows what
+mount options to use for the Nix volume.
+EOF
+ cp /etc/fstab "$SCRATCH/fstab.edit"
+ # technically doesn't need the _sudo path, but throwing away the
+ # output is probably better than mostly-duplicating the code...
+ delete_nix_vol_fstab_line patch "$SCRATCH/fstab.edit" &>/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
-
- 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
+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"
+}
+
+# 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 <<EOF
+
+This volume is encrypted, but I don't see a password to decrypt it.
+The quick fix is to let me delete this volume and make you a new one.
+If that's okay, enter your (sudo) password to continue. If not, you
+can ensure the decryption password is in your system keychain with a
+"Where" (service) field set to this volume's UUID:
+ $volume_uuid
+EOF
+ if password_confirm "delete this volume"; then
+ remove_volume "$volume_special"
else
- echo "error: refusing to create Nix store volume because the boot volume is" >&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 <<EOF
+Your Nix volume is encrypted, but I couldn't find its password. Either:
+- Delete or rename the volume out of the way
+- Ensure its decryption password is in the system keychain with a
+ "Where" (service) field set to this volume's UUID:
+ $volume_uuid
+EOF
+ fi
+ fi
+ elif test_filevault_in_use; then
+ row "encrypted" "no"
+ warning <<EOF
+FileVault is on, but your $NIX_VOLUME_LABEL volume isn't encrypted.
+EOF
+ # if we're interactive, give them a chance to
+ # encrypt the volume. If not, /shrug
+ if ! headless && (( NIX_VOLUME_DO_ENCRYPT == 1 )); then
+ if ui_confirm "Should I encrypt it and add the decryption key to your keychain?"; then
+ encrypt_volume "$volume_uuid" "$NIX_VOLUME_LABEL"
+ NIX_VOLUME_DO_ENCRYPT=0
+ else
+ NIX_VOLUME_DO_ENCRYPT=0
+ reminder "FileVault is on, but your $NIX_VOLUME_LABEL volume isn't encrypted."
fi
fi
-
- sudo diskutil apfs addVolume "$disk" APFS 'Nix Store' -mountpoint /nix
- volume="Nix Store"
else
- echo "Using existing '$volume' volume" >&2
+ row "encrypted" "no"
+ fi
+}
+
+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 <<EOF
+error: $NIX_ROOT is a symlink (-> $(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 <<EOF
+:a
+${NIX_ROOT:1}
+.
+:x
+EOF
+ if ! test_synthetic_conf_mountable; then
+ failure "error: failed to configure synthetic.conf" >&2
+ fi
+ create_synthetic_objects
+ if ! test_nix; then
+ failure >&2 <<EOF
+error: failed to bootstrap $NIX_ROOT
+If you enabled FileVault after booting, this is likely a known issue
+with macOS that you'll have to reboot to fix. If you didn't enable FV,
+though, please open an issue describing how the system that you see
+this error on was set up.
+EOF
+ fi
fi
+}
+setup_fstab() {
+ local volume_uuid="$1"
+ # fstab used to be responsible for mounting the volume. Now the last
+ # step adds a LaunchDaemon responsible for mounting. This is technically
+ # redundant for mounting, but diskutil appears to pick up mount options
+ # from fstab (and diskutil's support for specifying them directly is not
+ # consistent across versions/subcommands).
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
+ task "Configuring /etc/fstab to specify volume mount options" >&2
+ add_nix_vol_fstab_line "$volume_uuid" /usr/sbin/vifs
fi
}
-main "$@"
+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 <<EOF
+add-generic-password -a "$volume_label" -s "$volume_uuid" -l "$volume_label encryption password" -D "Encrypted volume password" -j "Added automatically by the Nix installer for use by $NIX_VOLUME_MOUNTD_DEST" -w "$password" -T /System/Library/CoreServices/APFSUserAgent -T /System/Library/CoreServices/CSUserAgent -T /usr/bin/security "/Library/Keychains/System.keychain"
+EOF
+ builtin printf "%s" "$password" | _sudo "to encrypt your Nix volume" \
+ /usr/sbin/diskutil apfs encryptVolume "$volume_label" -user disk -stdinpassphrase
+
+ /usr/sbin/diskutil unmount force "$volume_label"
+}
+
+create_volume() {
+ # Notes:
+ # 1) using `-nomount` instead of `-mountpoint "$NIX_ROOT"` to get
+ # its UUID and set mount opts in fstab before first mount
+ #
+ # 2) system is in some sense less secure than user keychain... (it's
+ # possible to read the password for decrypting the keychain) but
+ # the user keychain appears to be available too late. As far as I
+ # can tell, the file with this password (/var/db/SystemKey) is
+ # inside the FileVault envelope. If that isn't true, it may make
+ # sense to store the password inside the envelope?
+ #
+ # 3) At some point it would be ideal to have a small binary to serve
+ # as the daemon itself, and for it to replace /usr/bin/security here.
+ #
+ # 4) *UserAgent exemptions should let the system seamlessly supply the
+ # password if noauto is removed from fstab entry. This is intentional;
+ # the user will hopefully look for help if the volume stops mounting,
+ # rather than failing over into subtle race-condition problems.
+ #
+ # 5) If we ever get users griping about not having space to do
+ # anything useful with Nix, it is possibly to specify
+ # `-reserve 10g` or something, which will fail w/o that much
+ #
+ # 6) getting special w/ awk may be fragile, but doing it to:
+ # - save time over running slow diskutil commands
+ # - skirt risk we grab wrong volume if multiple match
+ _sudo "to create a new APFS volume '$NIX_VOLUME_LABEL' on $NIX_VOLUME_USE_DISK" \
+ /usr/sbin/diskutil apfs addVolume "$NIX_VOLUME_USE_DISK" "$NIX_VOLUME_FS" "$NIX_VOLUME_LABEL" -nomount | /usr/bin/awk '/Created new APFS Volume/ {print $5}'
+}
+
+volume_uuid_from_special() {
+ local volume_special="$1" # (i.e., disk1s7)
+ # For reasons I won't pretend to fathom, this returns 253 when it works
+ /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -k "$volume_special" || true
+}
+
+# this sometimes clears immediately, and AFAIK clears
+# within about 1s. diskutil info on an unmounted path
+# fails in around 50-100ms and a match takes about
+# 250-300ms. I suspect it's usually ~250-750ms
+await_volume() {
+ # caution: this could, in theory, get stuck
+ until /usr/sbin/diskutil info "$NIX_ROOT" &>/dev/null; do
+ :
+ done
+}
+
+setup_volume() {
+ local use_special use_uuid profile_packages
+ task "Creating a Nix volume" >&2
+
+ 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
+
+ if [ "$(/usr/sbin/diskutil info -plist "$NIX_ROOT" | xmllint --xpath "(/plist/dict/key[text()='GlobalPermissionsEnabled'])/following-sibling::*[1]" -)" = "<false/>" ]; then
+ _sudo "to set enableOwnership (enabling users to own files)" \
+ /usr/sbin/diskutil enableOwnership "$NIX_ROOT"
+ fi
+
+ # 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 <<EOF
+Nix now supports only multi-user installs on Darwin/macOS, and your user's
+Nix profile has some packages in it. These packages may obscure those in the
+default profile, including the Nix this installer will add. You should
+review these packages:
+$profile_packages
+EOF
+ fi
+ fi
+
+}
+
+setup_volume_daemon() {
+ local cmd_type="$1" # encrypted|unencrypted
+ local volume_uuid="$2"
+ if ! test_voldaemon; then
+ task "Configuring LaunchDaemon to mount '$NIX_VOLUME_LABEL'" >&2
+ _sudo "to install the Nix volume mounter" /usr/bin/ex "$NIX_VOLUME_MOUNTD_DEST" <<EOF
+:a
+$(generate_mount_daemon "$cmd_type" "$volume_uuid")
+.
+:x
+EOF
+
+ # TODO: should probably alert the user if this is disabled?
+ _sudo "to launch the Nix volume mounter" \
+ launchctl bootstrap system "$NIX_VOLUME_MOUNTD_DEST" || true
+ # TODO: confirm whether kickstart is necessesary?
+ # I feel a little superstitous, but it can guard
+ # against multiple problems (doesn't start, old
+ # version still running for some reason...)
+ _sudo "to launch the Nix volume mounter" \
+ launchctl kickstart -k system/org.nixos.darwin-store
+ fi
+}
+
+setup_darwin_volume() {
+ setup_synthetic_conf
+ setup_volume
+}
+
+if [ "$_CREATE_VOLUME_NO_MAIN" = 1 ]; then
+ if [ -n "$*" ]; then
+ "$@" # expose functions in case we want multiple routines?
+ fi
+else
+ # no reason to pay for bash to process this
+ main() {
+ {
+ echo ""
+ echo " ------------------------------------------------------------------ "
+ echo " | This installer will create a volume for the nix store and |"
+ echo " | configure it to mount at $NIX_ROOT. Follow these steps to uninstall. |"
+ echo " ------------------------------------------------------------------ "
+ echo ""
+ echo " 1. Remove the entry from fstab using 'sudo /usr/sbin/vifs'"
+ echo " 2. Run 'sudo launchctl bootout system/org.nixos.darwin-store'"
+ echo " 3. Remove $NIX_VOLUME_MOUNTD_DEST"
+ echo " 4. Destroy the data volume using '/usr/sbin/diskutil apfs deleteVolume'"
+ echo " 5. Remove the 'nix' line from /etc/synthetic.conf (or the file)"
+ echo ""
+ } >&2
+
+ setup_darwin_volume
+ }
+
+ main "$@"
+fi