diff --git a/Library/Homebrew/cask/lib/hbc/artifact/base.rb b/Library/Homebrew/cask/lib/hbc/artifact/base.rb
index d925ff340e..a8a17c0814 100644
--- a/Library/Homebrew/cask/lib/hbc/artifact/base.rb
+++ b/Library/Homebrew/cask/lib/hbc/artifact/base.rb
@@ -34,7 +34,6 @@ module Hbc
# stanza may not be needed as an explicit argument
description = stanza.to_s
if key
- arguments = arguments[key]
description.concat(" #{key.inspect}")
end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb
index 75d210931f..d438fc0264 100644
--- a/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb
+++ b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb
@@ -6,11 +6,6 @@ require "hbc/artifact/base"
module Hbc
module Artifact
class UninstallBase < Base
- # TODO: 500 is also hardcoded in cask/pkg.rb, but much of
- # that logic is probably in the wrong location
-
- PATH_ARG_SLICE_SIZE = 500
-
ORDERED_DIRECTIVES = [
:early_script,
:launchctl,
@@ -25,47 +20,7 @@ module Hbc
:rmdir,
].freeze
- # TODO: these methods were consolidated here from separate
- # sources and should be refactored for consistency
-
- def self.expand_path_strings(path_strings)
- path_strings.map do |path_string|
- path_string.start_with?("~") ? Pathname.new(path_string).expand_path : Pathname.new(path_string)
- end
- end
-
- def self.expand_glob(path_strings)
- path_strings.flat_map(&Pathname.method(:glob))
- end
-
- def self.remove_relative_path_strings(action, path_strings)
- relative = path_strings.map do |path_string|
- path_string if %r{/\.\.(?:/|\Z)}.match(path_string) || !%r{\A/}.match(path_string)
- end.compact
- relative.each do |path_string|
- opoo "Skipping #{action} for relative path #{path_string}"
- end
- path_strings - relative
- end
-
- def self.remove_undeletable_path_strings(action, path_strings)
- undeletable = path_strings.map do |path_string|
- path_string if MacOS.undeletable?(Pathname.new(path_string))
- end.compact
- undeletable.each do |path_string|
- opoo "Skipping #{action} for undeletable path #{path_string}"
- end
- path_strings - undeletable
- end
-
- def self.prepare_path_strings(action, path_strings, expand_tilde)
- path_strings = expand_path_strings(path_strings) if expand_tilde
- path_strings = remove_relative_path_strings(action, path_strings)
- path_strings = expand_glob(path_strings)
- remove_undeletable_path_strings(action, path_strings)
- end
-
- def dispatch_uninstall_directives(expand_tilde: true)
+ def dispatch_uninstall_directives
directives_set = @cask.artifacts[stanza]
ohai "Running #{stanza} process for #{@cask}; your password may be necessary"
@@ -75,9 +30,8 @@ module Hbc
ORDERED_DIRECTIVES.each do |directive_sym|
directives_set.select { |h| h.key?(directive_sym) }.each do |directives|
- args = [directives]
- args << expand_tilde if [:delete, :trash, :rmdir].include?(directive_sym)
- send("uninstall_#{directive_sym}", *args)
+ args = directives[directive_sym]
+ send("uninstall_#{directive_sym}", *(args.is_a?(Hash) ? [args] : args))
end
end
end
@@ -102,8 +56,8 @@ module Hbc
end
# :launchctl must come before :quit/:signal for cases where app would instantly re-launch
- def uninstall_launchctl(directives)
- Array(directives[:launchctl]).each do |service|
+ def uninstall_launchctl(*services)
+ services.each do |service|
ohai "Removing launchctl service #{service}"
[false, true].each do |with_sudo|
plist_status = @command.run("/bin/launchctl", args: ["list", service], sudo: with_sudo, print_stderr: false).stdout
@@ -127,45 +81,6 @@ module Hbc
end
end
- # :quit/:signal must come before :kext so the kext will not be in use by a running process
- def uninstall_quit(directives)
- Array(directives[:quit]).each do |id|
- ohai "Quitting application ID #{id}"
- next if running_processes(id).empty?
- @command.run!("/usr/bin/osascript", args: ["-e", %Q(tell application id "#{id}" to quit)], sudo: true)
-
- begin
- Timeout.timeout(3) do
- Kernel.loop do
- break if running_processes(id).empty?
- end
- end
- rescue Timeout::Error
- next
- end
- end
- end
-
- # :signal should come after :quit so it can be used as a backup when :quit fails
- def uninstall_signal(directives)
- Array(directives[:signal]).flatten.each_slice(2) do |pair|
- raise CaskInvalidError.new(@cask, "Each #{stanza} :signal must have 2 elements.") unless pair.length == 2
- signal, bundle_id = pair
- ohai "Signalling '#{signal}' to application ID '#{bundle_id}'"
- pids = running_processes(bundle_id).map(&:first)
- next unless pids.any?
- # Note that unlike :quit, signals are sent from the current user (not
- # upgraded to the superuser). This is a todo item for the future, but
- # there should be some additional thought/safety checks about that, as a
- # misapplied "kill" by root could bring down the system. The fact that we
- # learned the pid from AppleScript is already some degree of protection,
- # though indirect.
- odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}"
- Process.kill(signal, *pids)
- sleep 3
- end
- end
-
def running_processes(bundle_id)
@command.run!("/bin/launchctl", args: ["list"]).stdout.lines
.map { |line| line.chomp.split("\t") }
@@ -176,8 +91,50 @@ module Hbc
end
end
- def uninstall_login_item(directives)
- Array(directives[:login_item]).each do |name|
+ # :quit/:signal must come before :kext so the kext will not be in use by a running process
+ def uninstall_quit(*bundle_ids)
+ bundle_ids.each do |bundle_id|
+ ohai "Quitting application ID #{bundle_id}"
+ next if running_processes(bundle_id).empty?
+ @command.run!("/usr/bin/osascript", args: ["-e", %Q(tell application id "#{bundle_id}" to quit)], sudo: true)
+
+ begin
+ Timeout.timeout(3) do
+ Kernel.loop do
+ break if running_processes(bundle_id).empty?
+ end
+ end
+ rescue Timeout::Error
+ next
+ end
+ end
+ end
+
+ # :signal should come after :quit so it can be used as a backup when :quit fails
+ def uninstall_signal(*signals)
+ signals.flatten.each_slice(2) do |pair|
+ unless pair.size == 2
+ raise CaskInvalidError.new(@cask, "Each #{stanza} :signal must consist of 2 elements.")
+ end
+
+ signal, bundle_id = pair
+ ohai "Signalling '#{signal}' to application ID '#{bundle_id}'"
+ pids = running_processes(bundle_id).map(&:first)
+ next unless pids.any?
+ # Note that unlike :quit, signals are sent from the current user (not
+ # upgraded to the superuser). This is a todo item for the future, but
+ # there should be some additional thought/safety checks about that, as a
+ # misapplied "kill" by root could bring down the system. The fact that we
+ # learned the pid from AppleScript is already some degree of protection,
+ # though indirect.
+ odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{bundle_id}"
+ Process.kill(signal, *pids)
+ sleep 3
+ end
+ end
+
+ def uninstall_login_item(*login_items)
+ login_items.each do |name|
ohai "Removing login item #{name}"
@command.run!("/usr/bin/osascript",
args: ["-e", %Q(tell application "System Events" to delete every login item whose name is "#{name}")],
@@ -187,8 +144,8 @@ module Hbc
end
# :kext should be unloaded before attempting to delete the relevant file
- def uninstall_kext(directives)
- Array(directives[:kext]).each do |kext|
+ def uninstall_kext(*kexts)
+ kexts.each do |kext|
ohai "Unloading kernel extension #{kext}"
is_loaded = @command.run!("/usr/sbin/kextstat", args: ["-l", "-b", kext], sudo: true).stdout
if is_loaded.length > 1
@@ -209,6 +166,7 @@ module Hbc
{ must_succeed: true, sudo: true },
{ print_stdout: true },
directive_name)
+
ohai "Running uninstall script #{executable}"
raise CaskInvalidError.new(@cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil?
executable_path = @cask.staged_path.join(executable)
@@ -225,43 +183,67 @@ module Hbc
sleep 1
end
- def uninstall_pkgutil(directives)
- ohai "Removing files from pkgutil Bill-of-Materials"
- Array(directives[:pkgutil]).each do |regexp|
- pkgs = Hbc::Pkg.all_matching(regexp, @command)
- pkgs.each(&:uninstall)
+ def uninstall_pkgutil(*pkgs)
+ ohai "Uninstalling packages:"
+ pkgs.each do |regex|
+ Hbc::Pkg.all_matching(regex, @command).each do |pkg|
+ puts pkg.package_id
+ pkg.uninstall
+ end
end
end
- def uninstall_delete(directives, expand_tilde = true)
- Array(directives[:delete]).concat(Array(directives[:trash])).flatten.each_slice(PATH_ARG_SLICE_SIZE) do |path_slice|
- ohai "Removing files: #{path_slice.utf8_inspect}"
- path_slice = self.class.prepare_path_strings(:delete, path_slice, expand_tilde)
- @command.run!("/bin/rm", args: path_slice.unshift("-rf", "--"), sudo: true)
+ def each_resolved_path(action, paths)
+ paths.each do |path|
+ resolved_path = Pathname.new(path)
+
+ if path.start_with?("~")
+ resolved_path = resolved_path.expand_path
+ end
+
+ if resolved_path.relative? || resolved_path.split.any? { |part| part.to_s == ".." }
+ opoo "Skipping #{Formatter.identifier(action)} for relative path '#{path}'."
+ next
+ end
+
+ if MacOS.undeletable?(resolved_path)
+ opoo "Skipping #{Formatter.identifier(action)} for undeletable path '#{path}'."
+ next
+ end
+
+ yield path, Pathname.glob(resolved_path)
end
end
- # :trash functionality is stubbed as a synonym for :delete
- # TODO: make :trash work differently, moving files to the Trash
- def uninstall_trash(directives, expand_tilde = true)
- uninstall_delete(directives, expand_tilde)
+ def uninstall_delete(*paths)
+ return if paths.empty?
+
+ ohai "Removing files:"
+ each_resolved_path(:delete, paths) do |path, resolved_paths|
+ puts path
+ @command.run!("/usr/bin/xargs", args: ["-0", "--", "/bin/rm", "-r", "-f", "--"], input: resolved_paths.join("\0"), sudo: true)
+ end
end
- def uninstall_rmdir(directories, expand_tilde = true)
- action = :rmdir
- self.class.prepare_path_strings(action, Array(directories[action]).flatten, expand_tilde).each do |directory|
- next if directory.to_s.empty?
- ohai "Removing directory if empty: #{directory.to_s.utf8_inspect}"
- directory = Pathname.new(directory)
- next unless directory.exist?
- @command.run!("/bin/rm",
- args: ["-f", "--", directory.join(".DS_Store")],
- sudo: true,
- print_stderr: false)
- @command.run("/bin/rmdir",
- args: ["--", directory],
- sudo: true,
- print_stderr: false)
+ def uninstall_trash(*paths)
+ # :trash functionality is stubbed as a synonym for :delete
+ # TODO: make :trash work differently, moving files to the Trash
+ uninstall_delete(*paths)
+ end
+
+ def uninstall_rmdir(*directories)
+ return if directories.empty?
+
+ ohai "Removing directories if empty:"
+ each_resolved_path(:rmdir, directories) do |path, resolved_paths|
+ puts path
+ resolved_paths.select(&:directory?).each do |resolved_path|
+ if (ds_store = resolved_path.join(".DS_Store")).exist?
+ @command.run!("/bin/rm", args: ["-f", "--", ds_store], sudo: true, print_stderr: false)
+ end
+
+ @command.run("/bin/rmdir", args: ["--", resolved_path], sudo: true, print_stderr: false)
+ end
end
end
end
diff --git a/Library/Homebrew/cask/lib/hbc/artifact/zap.rb b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb
index 7793e57728..cdfe2531da 100644
--- a/Library/Homebrew/cask/lib/hbc/artifact/zap.rb
+++ b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb
@@ -4,7 +4,7 @@ module Hbc
module Artifact
class Zap < UninstallBase
def zap_phase
- dispatch_uninstall_directives(expand_tilde: true)
+ dispatch_uninstall_directives
end
end
end
diff --git a/Library/Homebrew/test/cask/artifact/uninstall_spec.rb b/Library/Homebrew/test/cask/artifact/uninstall_spec.rb
index b7deb4575b..49a6dce9fe 100644
--- a/Library/Homebrew/test/cask/artifact/uninstall_spec.rb
+++ b/Library/Homebrew/test/cask/artifact/uninstall_spec.rb
@@ -1,351 +1,7 @@
+require_relative "uninstall_zap_shared_examples"
+
describe Hbc::Artifact::Uninstall, :cask do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-installable.rb") }
-
- let(:uninstall_artifact) {
- Hbc::Artifact::Uninstall.new(cask, command: Hbc::FakeSystemCommand)
- }
-
- let(:dir) { TEST_TMPDIR }
- let(:absolute_path) { Pathname.new("#{dir}/absolute_path") }
- let(:path_with_tilde) { Pathname.new("#{dir}/path_with_tilde") }
- let(:glob_path1) { Pathname.new("#{dir}/glob_path1") }
- let(:glob_path2) { Pathname.new("#{dir}/glob_path2") }
-
- around(:each) do |example|
- begin
- ENV["HOME"] = dir
-
- paths = [
- absolute_path,
- path_with_tilde,
- glob_path1,
- glob_path2,
- ]
-
- FileUtils.touch paths
-
- shutup do
- InstallHelper.install_without_artifacts(cask)
- end
-
- example.run
- ensure
- FileUtils.rm_f paths
- end
- end
-
- describe "uninstall_phase" do
- subject {
- shutup do
- uninstall_artifact.uninstall_phase
- end
- }
-
- context "when using launchctl" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-launchctl.rb") }
- let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] }
- let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] }
- let(:unknown_response) { "launchctl list returned unknown response\n" }
- let(:service_info) {
- <<-EOS.undent
- {
- "LimitLoadToSessionType" = "Aqua";
- "Label" = "my.fancy.package.service";
- "TimeOut" = 30;
- "OnDemand" = true;
- "LastExitStatus" = 0;
- "ProgramArguments" = (
- "argument";
- );
- };
- EOS
- }
-
- context "when launchctl job is owned by user" do
- it "can uninstall" do
- Hbc::FakeSystemCommand.stubs_command(
- launchctl_list_cmd,
- service_info,
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- sudo(launchctl_list_cmd),
- unknown_response,
- )
-
- Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd)
-
- subject
- end
- end
-
- context "when launchctl job is owned by system" do
- it "can uninstall" do
- Hbc::FakeSystemCommand.stubs_command(
- launchctl_list_cmd,
- unknown_response,
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- sudo(launchctl_list_cmd),
- service_info,
- )
-
- Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd))
-
- subject
- end
- end
- end
-
- context "when using pkgutil" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-pkgutil.rb") }
- let(:main_pkg_id) { "my.fancy.package.main" }
- let(:agent_pkg_id) { "my.fancy.package.agent" }
- let(:main_files) {
- %w[
- fancy/bin/fancy.exe
- fancy/var/fancy.data
- ]
- }
- let(:main_dirs) {
- %w[
- fancy
- fancy/bin
- fancy/var
- ]
- }
- let(:agent_files) {
- %w[
- fancy/agent/fancy-agent.exe
- fancy/agent/fancy-agent.pid
- fancy/agent/fancy-agent.log
- ]
- }
- let(:agent_dirs) {
- %w[
- fancy
- fancy/agent
- ]
- }
- let(:pkg_info_plist) {
- <<-EOS.undent
-
-
-
-
- install-location
- tmp
- volume
- /
-
-
- EOS
- }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.stubs_command(
- %w[/usr/sbin/pkgutil --pkgs=my.fancy.package.*],
- "#{main_pkg_id}\n#{agent_pkg_id}",
- )
-
- [
- [main_pkg_id, main_files, main_dirs],
- [agent_pkg_id, agent_files, agent_dirs],
- ].each do |pkg_id, pkg_files, pkg_dirs|
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --only-files --files #{pkg_id}],
- pkg_files.join("\n"),
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --only-dirs --files #{pkg_id}],
- pkg_dirs.join("\n"),
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --files #{pkg_id}],
- (pkg_files + pkg_dirs).join("\n"),
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --pkg-info-plist #{pkg_id}],
- pkg_info_plist,
- )
-
- Hbc::FakeSystemCommand.expects_command(sudo(%W[/usr/sbin/pkgutil --forget #{pkg_id}]))
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -f --] + pkg_files.map { |path| Pathname("/tmp/#{path}") }),
- )
- end
-
- subject
- end
- end
-
- context "when using kext" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-kext.rb") }
- let(:kext_id) { "my.fancy.package.kernelextension" }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.stubs_command(
- sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded"
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%W[/sbin/kextunload -b #{kext_id}]),
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%W[/usr/sbin/kextfind -b #{kext_id}]), "/Library/Extensions/FancyPackage.kext\n"
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(["/bin/rm", "-rf", "/Library/Extensions/FancyPackage.kext"]),
- )
-
- subject
- end
- end
-
- context "when using quit" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-quit.rb") }
- let(:bundle_id) { "my.fancy.package.app" }
- let(:quit_application_script) {
- %Q(tell application id "#{bundle_id}" to quit)
- }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.stubs_command(
- %w[/bin/launchctl list], "999\t0\t#{bundle_id}\n"
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %w[/bin/launchctl list],
- )
-
- subject
- end
- end
-
- context "when using signal" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-signal.rb") }
- let(:bundle_id) { "my.fancy.package.app" }
- let(:signals) { %w[TERM KILL] }
- let(:unix_pids) { [12_345, 67_890] }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.stubs_command(
- %w[/bin/launchctl list], unix_pids.map { |pid| [pid, 0, bundle_id].join("\t") }.join("\n")
- )
-
- signals.each do |signal|
- expect(Process).to receive(:kill).with(signal, *unix_pids)
- end
-
- subject
- end
- end
-
- context "when using delete" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-delete.rb") }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -rf --],
- absolute_path,
- path_with_tilde,
- glob_path1,
- glob_path2),
- )
-
- subject
- end
- end
-
- context "when using trash" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-trash.rb") }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -rf --],
- absolute_path,
- path_with_tilde,
- glob_path1,
- glob_path2),
- )
-
- subject
- end
- end
-
- context "when using rmdir" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-rmdir.rb") }
- let(:empty_directory_path) { Pathname.new("#{TEST_TMPDIR}/empty_directory_path") }
-
- before(:each) do
- empty_directory_path.mkdir
- end
-
- after(:each) do
- empty_directory_path.rmdir
- end
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -f --], empty_directory_path/".DS_Store"),
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rmdir --], empty_directory_path),
- )
-
- subject
- end
- end
-
- context "when using script" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-script.rb") }
- let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname])
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"),
- )
-
- subject
- end
- end
-
- context "when using early_script" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-early-script.rb") }
- let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname])
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"),
- )
-
- subject
- end
- end
-
- context "when using login_item" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-uninstall-login-item.rb") }
-
- it "can uninstall" do
- Hbc::FakeSystemCommand.expects_command(
- ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \
- 'item whose name is "Fancy"'],
- )
-
- subject
- end
- end
+ describe "#uninstall_phase" do
+ include_examples "#uninstall_phase or #zap_phase"
end
end
diff --git a/Library/Homebrew/test/cask/artifact/uninstall_zap_shared_examples.rb b/Library/Homebrew/test/cask/artifact/uninstall_zap_shared_examples.rb
new file mode 100644
index 0000000000..2b69a06a83
--- /dev/null
+++ b/Library/Homebrew/test/cask/artifact/uninstall_zap_shared_examples.rb
@@ -0,0 +1,334 @@
+shared_examples "#uninstall_phase or #zap_phase" do
+ let(:artifact_name) { described_class.artifact_name }
+ let(:artifact) { described_class.new(cask, command: fake_system_command) }
+ let(:fake_system_command) { Hbc::FakeSystemCommand }
+
+ subject do
+ shutup do
+ artifact.public_send(:"#{artifact_name}_phase")
+ end
+ end
+
+ context "using :launchctl" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-launchctl.rb") }
+ let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] }
+ let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] }
+ let(:unknown_response) { "launchctl list returned unknown response\n" }
+ let(:service_info) do
+ <<-EOS.undent
+ {
+ "LimitLoadToSessionType" = "Aqua";
+ "Label" = "my.fancy.package.service";
+ "TimeOut" = 30;
+ "OnDemand" = true;
+ "LastExitStatus" = 0;
+ "ProgramArguments" = (
+ "argument";
+ );
+ };
+ EOS
+ end
+
+ it "works when job is owned by user" do
+ Hbc::FakeSystemCommand.stubs_command(
+ launchctl_list_cmd,
+ service_info,
+ )
+
+ Hbc::FakeSystemCommand.stubs_command(
+ sudo(launchctl_list_cmd),
+ unknown_response,
+ )
+
+ Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd)
+
+ subject
+ end
+
+ it "works when job is owned by system" do
+ Hbc::FakeSystemCommand.stubs_command(
+ launchctl_list_cmd,
+ unknown_response,
+ )
+
+ Hbc::FakeSystemCommand.stubs_command(
+ sudo(launchctl_list_cmd),
+ service_info,
+ )
+
+ Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd))
+
+ subject
+ end
+ end
+
+ context "using :pkgutil" do
+ let(:fake_system_command) { class_double(Hbc::SystemCommand) }
+
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-pkgutil.rb") }
+ let(:main_pkg_id) { "my.fancy.package.main" }
+ let(:agent_pkg_id) { "my.fancy.package.agent" }
+ let(:main_files) do
+ %w[
+ fancy/bin/fancy.exe
+ fancy/var/fancy.data
+ ]
+ end
+ let(:main_dirs) do
+ %w[
+ fancy
+ fancy/bin
+ fancy/var
+ ]
+ end
+ let(:agent_files) do
+ %w[
+ fancy/agent/fancy-agent.exe
+ fancy/agent/fancy-agent.pid
+ fancy/agent/fancy-agent.log
+ ]
+ end
+ let(:agent_dirs) do
+ %w[
+ fancy
+ fancy/agent
+ ]
+ end
+ let(:pkg_info_plist) do
+ <<-EOS.undent
+
+
+
+
+ install-location
+ tmp
+ volume
+ /
+
+
+ EOS
+ end
+
+ it "is supported" do
+ allow(fake_system_command).to receive(:run).with(
+ "/usr/sbin/pkgutil",
+ args: ["--pkgs=my.fancy.package.*"],
+ ).and_return(double(stdout: "#{main_pkg_id}\n#{agent_pkg_id}"))
+
+ [
+ [main_pkg_id, main_files, main_dirs],
+ [agent_pkg_id, agent_files, agent_dirs],
+ ].each do |pkg_id, pkg_files, pkg_dirs|
+
+ allow(fake_system_command).to receive(:run!).with(
+ "/usr/sbin/pkgutil",
+ args: ["--only-files", "--files", pkg_id.to_s],
+ ).and_return(double(stdout: pkg_files.join("\n")))
+
+ allow(fake_system_command).to receive(:run!).with(
+ "/usr/sbin/pkgutil",
+ args: ["--only-dirs", "--files", pkg_id.to_s],
+ ).and_return(double(stdout: pkg_dirs.join("\n")))
+
+ allow(fake_system_command).to receive(:run!).with(
+ "/usr/sbin/pkgutil",
+ args: ["--files", pkg_id.to_s],
+ ).and_return(double(stdout: (pkg_files + pkg_dirs).join("\n")))
+
+ result = Hbc::SystemCommand::Result.new(nil, pkg_info_plist, nil, 0)
+ allow(fake_system_command).to receive(:run!).with(
+ "/usr/sbin/pkgutil",
+ args: ["--pkg-info-plist", pkg_id.to_s],
+ ).and_return(result)
+
+ expect(fake_system_command).to receive(:run).with(
+ "/usr/bin/xargs",
+ args: ["-0", "--", "/bin/rm", "-f", "--"],
+ input: pkg_files.map { |path| "/tmp/#{path}" }.join("\0"),
+ sudo: true,
+ )
+
+ expect(fake_system_command).to receive(:run!).with(
+ "/usr/sbin/pkgutil",
+ args: ["--forget", pkg_id.to_s],
+ sudo: true,
+ )
+ end
+
+ subject
+ end
+ end
+
+ context "using :kext" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-kext.rb") }
+ let(:kext_id) { "my.fancy.package.kernelextension" }
+
+ it "is supported" do
+ Hbc::FakeSystemCommand.stubs_command(
+ sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded"
+ )
+
+ Hbc::FakeSystemCommand.expects_command(
+ sudo(%W[/sbin/kextunload -b #{kext_id}]),
+ )
+
+ Hbc::FakeSystemCommand.expects_command(
+ sudo(%W[/usr/sbin/kextfind -b #{kext_id}]), "/Library/Extensions/FancyPackage.kext\n"
+ )
+
+ Hbc::FakeSystemCommand.expects_command(
+ sudo(["/bin/rm", "-rf", "/Library/Extensions/FancyPackage.kext"]),
+ )
+
+ subject
+ end
+ end
+
+ context "using :quit" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-quit.rb") }
+ let(:bundle_id) { "my.fancy.package.app" }
+ let(:quit_application_script) do
+ %Q(tell application id "#{bundle_id}" to quit)
+ end
+
+ it "is supported" do
+ Hbc::FakeSystemCommand.stubs_command(
+ %w[/bin/launchctl list], "999\t0\t#{bundle_id}\n"
+ )
+
+ Hbc::FakeSystemCommand.stubs_command(
+ %w[/bin/launchctl list],
+ )
+
+ subject
+ end
+ end
+
+ context "using :signal" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-signal.rb") }
+ let(:bundle_id) { "my.fancy.package.app" }
+ let(:signals) { %w[TERM KILL] }
+ let(:unix_pids) { [12_345, 67_890] }
+
+ it "is supported" do
+ Hbc::FakeSystemCommand.stubs_command(
+ %w[/bin/launchctl list], unix_pids.map { |pid| [pid, 0, bundle_id].join("\t") }.join("\n")
+ )
+
+ signals.each do |signal|
+ expect(Process).to receive(:kill).with(signal, *unix_pids)
+ end
+
+ subject
+ end
+ end
+
+ [:delete, :trash].each do |directive|
+ context "using :#{directive}" do
+ let(:dir) { TEST_TMPDIR }
+ let(:absolute_path) { Pathname.new("#{dir}/absolute_path") }
+ let(:path_with_tilde) { Pathname.new("#{dir}/path_with_tilde") }
+ let(:glob_path1) { Pathname.new("#{dir}/glob_path1") }
+ let(:glob_path2) { Pathname.new("#{dir}/glob_path2") }
+ let(:paths) { [absolute_path, path_with_tilde, glob_path1, glob_path2] }
+
+ around(:each) do |example|
+ begin
+ ENV["HOME"] = dir
+
+ FileUtils.touch paths
+
+ example.run
+ ensure
+ FileUtils.rm_f paths
+ end
+ end
+
+ let(:fake_system_command) { Hbc::NeverSudoSystemCommand }
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-#{directive}.rb") }
+
+ it "is supported" do
+ paths.each do |path|
+ expect(path).to exist
+ end
+
+ subject
+
+ paths.each do |path|
+ expect(path).not_to exist
+ end
+ end
+ end
+ end
+
+ context "using :rmdir" do
+ let(:fake_system_command) { Hbc::NeverSudoSystemCommand }
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-rmdir.rb") }
+ let(:empty_directory) { Pathname.new("#{TEST_TMPDIR}/empty_directory_path") }
+ let(:ds_store) { empty_directory.join(".DS_Store") }
+
+ before(:each) do
+ empty_directory.mkdir
+ FileUtils.touch ds_store
+ end
+
+ after(:each) do
+ FileUtils.rm_rf empty_directory
+ end
+
+ it "is supported" do
+ expect(empty_directory).to exist
+ expect(ds_store).to exist
+
+ subject
+
+ expect(ds_store).not_to exist
+ expect(empty_directory).not_to exist
+ end
+ end
+
+ context "using :script" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-script.rb") }
+ let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") }
+
+ it "is supported" do
+ Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname])
+
+ Hbc::FakeSystemCommand.expects_command(
+ sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"),
+ )
+
+ InstallHelper.install_without_artifacts(cask)
+ subject
+ end
+ end
+
+ context "using :early_script" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-early-script.rb") }
+ let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") }
+
+ it "is supported" do
+ Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname])
+
+ Hbc::FakeSystemCommand.expects_command(
+ sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"),
+ )
+
+ InstallHelper.install_without_artifacts(cask)
+ subject
+ end
+ end
+
+ context "using :login_item" do
+ let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-#{artifact_name}-login-item.rb") }
+
+ it "is supported" do
+ Hbc::FakeSystemCommand.expects_command(
+ ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \
+ 'item whose name is "Fancy"'],
+ )
+
+ subject
+ end
+ end
+end
diff --git a/Library/Homebrew/test/cask/artifact/zap_spec.rb b/Library/Homebrew/test/cask/artifact/zap_spec.rb
index fdf2e4f9db..0a09d9710e 100644
--- a/Library/Homebrew/test/cask/artifact/zap_spec.rb
+++ b/Library/Homebrew/test/cask/artifact/zap_spec.rb
@@ -1,352 +1,7 @@
-# TODO: test that zap removes an alternate version of the same Cask
+require_relative "uninstall_zap_shared_examples"
+
describe Hbc::Artifact::Zap, :cask do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-installable.rb") }
-
- let(:zap_artifact) {
- Hbc::Artifact::Zap.new(cask, command: Hbc::FakeSystemCommand)
- }
-
- let(:dir) { TEST_TMPDIR }
- let(:absolute_path) { Pathname.new("#{dir}/absolute_path") }
- let(:path_with_tilde) { Pathname.new("#{dir}/path_with_tilde") }
- let(:glob_path1) { Pathname.new("#{dir}/glob_path1") }
- let(:glob_path2) { Pathname.new("#{dir}/glob_path2") }
-
- around(:each) do |example|
- begin
- ENV["HOME"] = dir
-
- paths = [
- absolute_path,
- path_with_tilde,
- glob_path1,
- glob_path2,
- ]
-
- FileUtils.touch paths
-
- shutup do
- InstallHelper.install_without_artifacts(cask)
- end
-
- example.run
- ensure
- FileUtils.rm_f paths
- end
- end
-
describe "#zap_phase" do
- subject {
- shutup do
- zap_artifact.zap_phase
- end
- }
-
- context "when using launchctl" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-launchctl.rb") }
- let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] }
- let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] }
- let(:unknown_response) { "launchctl list returned unknown response\n" }
- let(:service_info) {
- <<-EOS.undent
- {
- "LimitLoadToSessionType" = "Aqua";
- "Label" = "my.fancy.package.service";
- "TimeOut" = 30;
- "OnDemand" = true;
- "LastExitStatus" = 0;
- "ProgramArguments" = (
- "argument";
- );
- };
- EOS
- }
-
- context "when launchctl job is owned by user" do
- it "can zap" do
- Hbc::FakeSystemCommand.stubs_command(
- launchctl_list_cmd,
- service_info,
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- sudo(launchctl_list_cmd),
- unknown_response,
- )
-
- Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd)
-
- subject
- end
- end
-
- describe "when launchctl job is owned by system" do
- it "can zap" do
- Hbc::FakeSystemCommand.stubs_command(
- launchctl_list_cmd,
- unknown_response,
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- sudo(launchctl_list_cmd),
- service_info,
- )
-
- Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd))
-
- subject
- end
- end
- end
-
- context "when using pkgutil" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-pkgutil.rb") }
- let(:main_pkg_id) { "my.fancy.package.main" }
- let(:agent_pkg_id) { "my.fancy.package.agent" }
- let(:main_files) {
- %w[
- fancy/bin/fancy.exe
- fancy/var/fancy.data
- ]
- }
- let(:main_dirs) {
- %w[
- fancy
- fancy/bin
- fancy/var
- ]
- }
- let(:agent_files) {
- %w[
- fancy/agent/fancy-agent.exe
- fancy/agent/fancy-agent.pid
- fancy/agent/fancy-agent.log
- ]
- }
- let(:agent_dirs) {
- %w[
- fancy
- fancy/agent
- ]
- }
- let(:pkg_info_plist) {
- <<-EOS.undent
-
-
-
-
- install-location
- tmp
- volume
- /
-
-
- EOS
- }
-
- it "can zap" do
- Hbc::FakeSystemCommand.stubs_command(
- %w[/usr/sbin/pkgutil --pkgs=my.fancy.package.*],
- "#{main_pkg_id}\n#{agent_pkg_id}",
- )
-
- [
- [main_pkg_id, main_files, main_dirs],
- [agent_pkg_id, agent_files, agent_dirs],
- ].each do |pkg_id, pkg_files, pkg_dirs|
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --only-files --files #{pkg_id}],
- pkg_files.join("\n"),
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --only-dirs --files #{pkg_id}],
- pkg_dirs.join("\n"),
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --files #{pkg_id}],
- (pkg_files + pkg_dirs).join("\n"),
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %W[/usr/sbin/pkgutil --pkg-info-plist #{pkg_id}],
- pkg_info_plist,
- )
-
- Hbc::FakeSystemCommand.expects_command(sudo(%W[/usr/sbin/pkgutil --forget #{pkg_id}]))
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -f --] + pkg_files.map { |path| Pathname("/tmp/#{path}") }),
- )
- end
-
- subject
- end
- end
-
- context "when using kext" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-kext.rb") }
- let(:kext_id) { "my.fancy.package.kernelextension" }
-
- it "can zap" do
- Hbc::FakeSystemCommand.stubs_command(
- sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded"
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%W[/sbin/kextunload -b #{kext_id}]),
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%W[/usr/sbin/kextfind -b #{kext_id}]), "/Library/Extensions/FancyPackage.kext\n"
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(["/bin/rm", "-rf", "/Library/Extensions/FancyPackage.kext"]),
- )
-
- subject
- end
- end
-
- context "when using quit" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-quit.rb") }
- let(:bundle_id) { "my.fancy.package.app" }
- let(:quit_application_script) {
- %Q(tell application id "#{bundle_id}" to quit)
- }
-
- it "can zap" do
- Hbc::FakeSystemCommand.stubs_command(
- %w[/bin/launchctl list], "999\t0\t#{bundle_id}\n"
- )
-
- Hbc::FakeSystemCommand.stubs_command(
- %w[/bin/launchctl list],
- )
-
- subject
- end
- end
-
- context "when using signal" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-signal.rb") }
- let(:bundle_id) { "my.fancy.package.app" }
- let(:signals) { %w[TERM KILL] }
- let(:unix_pids) { [12_345, 67_890] }
-
- it "can zap" do
- Hbc::FakeSystemCommand.stubs_command(
- %w[/bin/launchctl list], unix_pids.map { |pid| [pid, 0, bundle_id].join("\t") }.join("\n")
- )
-
- signals.each do |signal|
- expect(Process).to receive(:kill).with(signal, *unix_pids)
- end
-
- subject
- end
- end
-
- context "when using delete" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-delete.rb") }
-
- it "can zap" do
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -rf --],
- absolute_path,
- path_with_tilde,
- glob_path1,
- glob_path2),
- )
-
- subject
- end
- end
-
- context "when using trash" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-trash.rb") }
-
- it "can zap" do
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -rf --],
- absolute_path,
- path_with_tilde,
- glob_path1,
- glob_path2),
- )
-
- subject
- end
- end
-
- context "when using rmdir" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-rmdir.rb") }
- let(:empty_directory_path) { Pathname.new("#{TEST_TMPDIR}/empty_directory_path") }
-
- before(:each) do
- empty_directory_path.mkdir
- end
-
- after(:each) do
- empty_directory_path.rmdir
- end
-
- it "can zap" do
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rm -f --], empty_directory_path/".DS_Store"),
- )
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(%w[/bin/rmdir --], empty_directory_path),
- )
-
- subject
- end
- end
-
- context "when using script" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-script.rb") }
- let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") }
-
- it "can zap" do
- Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname])
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"),
- )
-
- subject
- end
- end
-
- context "when using early_script" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-early-script.rb") }
- let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") }
-
- it "can zap" do
- Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname])
-
- Hbc::FakeSystemCommand.expects_command(
- sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please"),
- )
-
- subject
- end
- end
-
- context "when using login_item" do
- let(:cask) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/with-zap-login-item.rb") }
-
- it "can zap" do
- Hbc::FakeSystemCommand.expects_command(
- ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \
- 'item whose name is "Fancy"'],
- )
-
- subject
- end
- end
+ include_examples "#uninstall_phase or #zap_phase"
end
end