diff --git a/Library/Homebrew/cask/artifact/moved.rb b/Library/Homebrew/cask/artifact/moved.rb index 3b19f5849d..d41e62cec0 100644 --- a/Library/Homebrew/cask/artifact/moved.rb +++ b/Library/Homebrew/cask/artifact/moved.rb @@ -84,8 +84,7 @@ module Cask Utils.gain_permissions_mkpath(target.dirname, command: command) unless target.dirname.exist? - if target.directory? - Quarantine.ensure_app_management_permissions_granted(app: target, command: command) + if target.directory? && Quarantine.app_management_permissions_granted?(app: target, command: command) if target.writable? source.children.each { |child| FileUtils.move(child, target/child.basename) } else @@ -154,10 +153,11 @@ module Cask return unless Utils.path_occupied?(target) - if target.directory? && matching_artifact?(successor) + if target.directory? && matching_artifact?(successor) && Quarantine.app_management_permissions_granted?( + app: target, command: command, + ) # If an app folder is deleted, macOS considers the app uninstalled and removes some data. # Remove only the contents to handle this case. - Quarantine.ensure_app_management_permissions_granted(app: target, command: command) target.children.each do |child| if target.writable? && !force child.rmtree diff --git a/Library/Homebrew/cask/quarantine.rb b/Library/Homebrew/cask/quarantine.rb index cb402e6955..194c794990 100644 --- a/Library/Homebrew/cask/quarantine.rb +++ b/Library/Homebrew/cask/quarantine.rb @@ -192,9 +192,9 @@ module Cask # Ensures that Homebrew has permission to update apps on macOS Ventura. # This may be granted either through the App Management toggle or the Full Disk Access toggle. # The system will only show a prompt for App Management, so we ask the user to grant that. - sig { params(app: Pathname, command: T.class_of(SystemCommand)).void } - def self.ensure_app_management_permissions_granted(app:, command:) - return unless app.directory? + sig { params(app: Pathname, command: T.class_of(SystemCommand)).returns(T::Boolean) } + def self.app_management_permissions_granted?(app:, command:) + return true unless app.directory? # To get macOS to prompt the user for permissions, we need to actually attempt to # modify a file in the app. @@ -215,7 +215,7 @@ module Cask begin File.write(test_file, "") test_file.delete - return + return true rescue Errno::EACCES # Using error handler below end @@ -237,17 +237,19 @@ module Cask print_stderr: false, sudo: true, ) - return + return true rescue ErrorDuringExecution => e # We only want to handle "touch" errors here; propagate "sudo" errors up raise e unless e.stderr.include?("touch: #{test_file}: Operation not permitted") end end - odie <<~EOF - Your terminal needs permission to update apps. - Go to Settings > Security and Privacy > App Management, or look for a notification saying your terminal was prevented from modifying apps. + opoo <<~EOF + Your terminal does not have App Management permissions, so Homebrew will delete and reinstall the app. + This may result in some configurations (like notification settings or location in the Dock/Launchpad) being lost. + To fix this, go to Settings > Security and Privacy > App Management and turn on the switch for your terminal. EOF + false end end end diff --git a/Library/Homebrew/test/cask/artifact/app_spec.rb b/Library/Homebrew/test/cask/artifact/app_spec.rb index 3dc19a2cc4..ed94017311 100644 --- a/Library/Homebrew/test/cask/artifact/app_spec.rb +++ b/Library/Homebrew/test/cask/artifact/app_spec.rb @@ -329,13 +329,16 @@ describe Cask::Artifact::App, :cask do end describe "when the system blocks modifying apps" do - it "does not delete files" do + it "uninstalls and reinstalls the app" do target_contents_path = target_path.join("Contents") expect(File).to receive(:write).with(target_path / ".homebrew-write-test", instance_of(String)).and_raise(Errno::EACCES) - expect { app.uninstall_phase(command: command, force: force, successor: cask) }.to raise_error(SystemExit) + app.uninstall_phase(command: command, force: force, successor: cask) + expect(target_path).not_to exist + + app.install_phase(command: command, adopt: adopt, force: force, predecessor: cask) expect(target_contents_path).to exist end end @@ -368,7 +371,7 @@ describe Cask::Artifact::App, :cask do end describe "when the system blocks modifying apps" do - it "does not delete files" do + it "uninstalls and reinstalls the app" do target_contents_path = target_path.join("Contents") allow(command).to receive(:run!).with(any_args).and_call_original @@ -380,7 +383,10 @@ describe Cask::Artifact::App, :cask do .and_raise(ErrorDuringExecution.new([], status: 1, output: [[:stderr, "touch: #{target_path}/.homebrew-write-test: Operation not permitted\n"]], secrets: [])) - expect { app.uninstall_phase(command: command, force: force, successor: cask) }.to raise_error(SystemExit) + app.uninstall_phase(command: command, force: force, successor: cask) + expect(target_path).not_to exist + + app.install_phase(command: command, adopt: adopt, force: force, predecessor: cask) expect(target_contents_path).to exist end end