diff options
Diffstat (limited to 'scripts/create-darwin-volume.sh')
-rwxr-xr-x | scripts/create-darwin-volume.sh | 875 |
1 files changed, 0 insertions, 875 deletions
diff --git a/scripts/create-darwin-volume.sh b/scripts/create-darwin-volume.sh deleted file mode 100755 index 103e1e391..000000000 --- a/scripts/create-darwin-volume.sh +++ /dev/null @@ -1,875 +0,0 @@ -#!/usr/bin/env bash -set -eu -set -o pipefail - -# 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 - -# usually "disk1" -root_disk_identifier() { - # 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 )) -} - -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>
</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() { - /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" -} - -# 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" - _sudo "to confirm the password actually unlocks the volume" \ - /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() { - # 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). - # This feels a little dirty, but as far as I can tell the - # simplest way to get the right one is to just throw away stderr - # and call both... :] - { - /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -t || true # Big Sur - /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -B || true # Catalina - } >/dev/null 2>&1 -} - -test_nix() { - 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 -} - -add_nix_vol_fstab_line() { - local uuid="$1" - # shellcheck disable=SC1003,SC2026 - local escaped_mountpoint="${NIX_ROOT/ /'\\\'040}" - shift - - # wrap `ex` to work around problems w/ vim features breaking exit codes - # - plugins (see github.com/NixOS/nix/issues/5468): -u NONE - # - swap file: -n - # - # the first draft used `--noplugin`, but github.com/NixOS/nix/issues/6462 - # suggests we need the less-semantic `-u NONE` - # - # we'd prefer EDITOR="/usr/bin/ex -u NONE" but vifs doesn't word-split - # the EDITOR env. - # - # TODO: at some point we should switch to `--clean`, but it wasn't added - # until https://github.com/vim/vim/releases/tag/v8.0.1554 while the macOS - # minver 10.12.6 seems to have released with vim 7.4 - cat > "$SCRATCH/ex_cleanroom_wrapper" <<EOF -#!/bin/sh -/usr/bin/ex -u NONE -n "\$@" -EOF - chmod 755 "$SCRATCH/ex_cleanroom_wrapper" - - EDITOR="$SCRATCH/ex_cleanroom_wrapper" _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 - 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 -} - -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 - # 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 - else - 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... - # See earlier note; `-u NONE` disables vim plugins/rc, `-n` skips swapfile - _sudo "to add Nix to /etc/synthetic.conf" \ - /usr/bin/ex -u NONE -n /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 - task "Configuring /etc/fstab to specify volume mount options" >&2 - add_nix_vol_fstab_line "$volume_uuid" /usr/sbin/vifs - fi -} - -encrypt_volume() { - local volume_uuid="$1" - local volume_label="$2" - local password - - task "Encrypt the Nix volume" >&2 - - # 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. - - _sudo "to mount your Nix volume for encrypting" \ - /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 actually encrypt your Nix volume" \ - /usr/sbin/diskutil apfs encryptVolume "$volume_label" -user disk -stdinpassphrase - - _sudo "to unmount the encrypted volume" \ - /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)}" - - _sudo "to ensure the Nix volume is not mounted" \ - /usr/sbin/diskutil unmount force "$use_special" || true # might not be mounted - - 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 - # See earlier note; `-u NONE` disables vim plugins/rc, `-n` skips swapfile - _sudo "to install the Nix volume mounter" /usr/bin/ex -u NONE -n "$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 |