From 4b846fa3aa3a5dc792c86651901aa3bd3068e5b6 Mon Sep 17 00:00:00 2001 From: Thiago Kenji Okada Date: Mon, 20 Oct 2025 10:58:40 +0100 Subject: [PATCH] targets/darwin: init copyapps This is basically a better version of `targets.darwin.linkApps` that copy apps instead of linking (hence the name). While this is a convoluted approach, it works with Spotlight, where the previous approach doesn't. This is also the approach adopted by nix-darwin, see PR: https://github.com/nix-darwin/nix-darwin/pull/1396. There are a few particularities about this implementation, for one the flags we use in rsync are different since we are not using root to copy the apps to `~/Applications`. This may or may not cause some issues with specific applications so further testing will be needed. Also the check for App Management permission needs root (via sudo), so this check is gated behind a flag that can be disabled if needed. Fix: #1341. --- modules/targets/darwin/copyapps.nix | 145 ++++++++++++++++++++++++++++ modules/targets/darwin/default.nix | 8 +- modules/targets/darwin/linkapps.nix | 7 +- 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 modules/targets/darwin/copyapps.nix diff --git a/modules/targets/darwin/copyapps.nix b/modules/targets/darwin/copyapps.nix new file mode 100644 index 00000000..d0539668 --- /dev/null +++ b/modules/targets/darwin/copyapps.nix @@ -0,0 +1,145 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.targets.darwin.copyApps; +in +{ + options.targets.darwin.copyApps = { + enable = + lib.mkEnableOption "copying macOS applications to the user environment (works with Spotlight)" + // { + default = + pkgs.stdenv.hostPlatform.isDarwin && (lib.versionAtLeast config.home.stateVersion "25.11"); + defaultText = lib.literalExpression ''pkgs.stdenv.hostPlatform.isDarwin && (lib.versionAtLeast config.home.stateVersion "25.11")''; + }; + + enableChecks = + lib.mkEnableOption "enable App Management checks (needs sudo; may ask sudo twice with nix-darwin)" + // { + default = true; + }; + + directory = lib.mkOption { + type = lib.types.str; + default = "Applications/Home Manager Apps"; + description = "Path to link apps relative to the home directory."; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !config.targets.darwin.linkApps.enable; + message = "This modules conflicts with `targets.darwin.linkApps`."; + } + (lib.hm.assertions.assertPlatform "targets.darwin.copyApps" pkgs lib.platforms.darwin) + ]; + + home.activation = { + checkAppManagementPermission = lib.mkIf cfg.enableChecks ( + lib.hm.dag.entryBefore [ "copyApps" ] + # bash + '' + ensureAppManagement() { + for appBundle in '${cfg.directory}/'*.app; do + if [[ -d "$appBundle" ]]; then + if ! run /usr/bin/sudo /usr/bin/touch "$appBundle/.DS_Store" &> /dev/null; then + return 1 + fi + fi + done + + return 0 + } + + if ! ensureAppManagement; then + if [[ "$(/bin/launchctl managername)" != Aqua ]]; then + # It is possible to grant the App Management permission to `sshd-keygen-wrapper`, however + # there are many pitfalls like requiring the primary user to grant the permission and to + # be logged in when `darwin-rebuild` is run over SSH and it will still fail sometimes... + printf >&2 '\e[1;31merror: permission denied when trying to update apps over SSH, aborting activation\e[0m\n' + printf >&2 'Apps could not be updated as `darwin-rebuild` requires Full Disk Access to work over SSH.\n' + printf >&2 'You can either:\n' + printf >&2 '\n' + printf >&2 ' grant Full Disk Access to all programs run over SSH\n' + printf >&2 '\n' + printf >&2 'or\n' + printf >&2 '\n' + printf >&2 ' run `darwin-rebuild` in a graphical session.\n' + printf >&2 '\n' + printf >&2 'The option "Allow full disk access for remote users" can be found by\n' + printf >&2 'navigating to System Settings > General > Sharing > Remote Login\n' + printf >&2 'and then pressing on the i icon next to the switch.\n' + exit 1 + else + # The TCC service required to modify notarised app bundles is `kTCCServiceSystemPolicyAppBundles` + # and we can reset it to ensure the user gets another prompt + run /usr/bin/tccutil reset SystemPolicyAppBundles > /dev/null + + if ! ensureAppManagement; then + printf >&2 '\e[1;31merror: permission denied when trying to update apps, aborting activation\e[0m\n' + printf >&2 '`darwin-rebuild` requires permission to update your apps, please accept the notification\n' + printf >&2 'and grant the permission for your terminal emulator in System Settings.\n' + printf >&2 '\n' + printf >&2 'If you did not get a notification, you can navigate to System Settings > Privacy & Security > App Management.\n' + exit 1 + fi + fi + fi + '' + ); + + copyApps = lib.hm.dag.entryAfter [ "installPackages" ] ( + let + applications = pkgs.buildEnv { + name = "home-manager-applications"; + paths = config.home.packages; + pathsToLink = "/Applications"; + }; + in + # bash + '' + targetFolder='${cfg.directory}' + + echo "setting up ~/$targetFolder..." >&2 + + ourLink () { + local link + link=$(readlink "$1") + [ -L "$1" ] && [ "''${link#*-}" = 'home-manager-applications/Applications' ] + } + + if [ -e "$targetFolder" ] && ourLink "$targetFolder"; then + run rm "$targetFolder" + fi + + run mkdir -p "$targetFolder" + + rsyncFlags=( + # mtime is standardized in the nix store, which would leave only file size to distinguish files. + # Thus we need checksums, despite the speed penalty. + --checksum + # Converts all symlinks pointing outside of the copied tree (thus unsafe) into real files and directories. + # This neatly converts all the symlinks pointing to application bundles in the nix store into + # real directories, without breaking any relative symlinks inside of application bundles. + # This is good enough, because the make-symlinks-relative.sh setup hook converts all $out internal + # symlinks to relative ones. + --copy-unsafe-links + --archive + --delete + --chmod=+w + --no-group + --no-owner + ) + + run ${lib.getExe pkgs.rsync} "''${rsyncFlags[@]}" ${applications}/Applications/ "$targetFolder" + '' + ); + }; + }; +} diff --git a/modules/targets/darwin/default.nix b/modules/targets/darwin/default.nix index 360a3f3d..6f551e04 100644 --- a/modules/targets/darwin/default.nix +++ b/modules/targets/darwin/default.nix @@ -1,4 +1,9 @@ -{ lib, ... }: +{ + config, + lib, + pkgs, + ... +}: { meta.maintainers = with lib.maintainers; [ midchildan ]; @@ -7,6 +12,7 @@ ./user-defaults ./fonts.nix ./keybindings.nix + ./copyapps.nix ./linkapps.nix ./search.nix ]; diff --git a/modules/targets/darwin/linkapps.nix b/modules/targets/darwin/linkapps.nix index d5352134..bcc9f5b8 100644 --- a/modules/targets/darwin/linkapps.nix +++ b/modules/targets/darwin/linkapps.nix @@ -11,7 +11,8 @@ in { options.targets.darwin.linkApps = { enable = lib.mkEnableOption "linking macOS applications to the user environment" // { - default = pkgs.stdenv.hostPlatform.isDarwin; + default = pkgs.stdenv.hostPlatform.isDarwin && (lib.versionOlder config.home.stateVersion "25.11"); + defaultText = lib.literalExpression ''pkgs.stdenv.hostPlatform.isDarwin && (lib.versionOlder config.home.stateVersion "25.11")''; }; directory = lib.mkOption { @@ -23,6 +24,10 @@ in config = lib.mkIf cfg.enable { assertions = [ + { + assertion = !config.targets.darwin.copyApps.enable; + message = "This modules conflicts with `targets.darwin.copyApps`."; + } (lib.hm.assertions.assertPlatform "targets.darwin.linkApps" pkgs lib.platforms.darwin) ];