diff --git a/Library/Homebrew/cask/lib/hbc/artifact/moved.rb b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb index ba1c8e9072003..11e019af2c4f7 100644 --- a/Library/Homebrew/cask/lib/hbc/artifact/moved.rb +++ b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb @@ -12,7 +12,7 @@ def install_phase(**options) end def uninstall_phase(**options) - delete(**options) + move_back(**options) end def summarize_installed @@ -30,7 +30,7 @@ def move(force: false, command: nil, **options) message = "It seems there is already #{self.class.english_article} #{self.class.english_name} at '#{target}'" raise CaskError, "#{message}." unless force opoo "#{message}; overwriting." - delete(force: force, command: command, **options) + delete(target, force: force, command: command, **options) end unless source.exist? @@ -49,7 +49,32 @@ def move(force: false, command: nil, **options) add_altname_metadata(target, source.basename, command: command) end - def delete(force: false, command: nil, **_) + def move_back(skip: false, force: false, command: nil, **options) + if Utils.path_occupied?(source) + message = "It seems there is already #{self.class.english_article} #{self.class.english_name} at '#{source}'" + raise CaskError, "#{message}." unless force + opoo "#{message}; overwriting." + delete(source, force: force, command: command, **options) + end + + unless target.exist? + return if skip + raise CaskError, "It seems the #{self.class.english_name} source '#{target}' is not there." + end + + ohai "Moving #{self.class.english_name} '#{target.basename}' back to '#{source}'." + source.dirname.mkpath + + if source.parent.writable? + FileUtils.move(target, source) + else + command.run("/bin/mv", args: [target, source], sudo: true) + end + + add_altname_metadata(target, source.basename, command: command) + end + + def delete(target, force: false, command: nil, **_) ohai "Removing #{self.class.english_name} '#{target}'." raise CaskError, "Cannot remove undeletable #{self.class.english_name}." if MacOS.undeletable?(target) diff --git a/Library/Homebrew/cask/lib/hbc/cli.rb b/Library/Homebrew/cask/lib/hbc/cli.rb index e147c82809c61..215b59843dc73 100644 --- a/Library/Homebrew/cask/lib/hbc/cli.rb +++ b/Library/Homebrew/cask/lib/hbc/cli.rb @@ -21,6 +21,7 @@ require "hbc/cli/search" require "hbc/cli/style" require "hbc/cli/uninstall" +require "hbc/cli/upgrade" require "hbc/cli/--version" require "hbc/cli/zap" diff --git a/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb b/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb new file mode 100644 index 0000000000000..bd80ad6905d63 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb @@ -0,0 +1,87 @@ +module Hbc + class CLI + class Upgrade < AbstractCommand + option "--greedy", :greedy, false + option "--quiet", :quiet, false + option "--force", :force, false + option "--skip-cask-deps", :skip_cask_deps, false + + def initialize(*) + super + self.verbose = ($stdout.tty? || verbose?) && !quiet? + end + + def run + outdated_casks = casks(alternative: lambda { + Hbc.installed.select do |cask| + cask.outdated?(greedy?) + end + }).select { |cask| cask.outdated?(true) } + + if outdated_casks.empty? + oh1 "No Casks to upgrade" + return + end + + oh1 "Upgrading #{Formatter.pluralize(outdated_casks.length, "outdated package")}, with result:" + puts outdated_casks.map { |f| "#{f.full_name} #{f.version}" } * ", " + + outdated_casks.each do |old_cask| + odebug "Started upgrade process for Cask #{old_cask}" + raise CaskNotInstalledError, old_cask unless old_cask.installed? || force? + + raise CaskUnavailableError.new(old_cask, "The Caskfile is missing!") if old_cask.installed_caskfile.nil? + + old_cask = CaskLoader.load(old_cask.installed_caskfile) + + old_cask_installer = Installer.new(old_cask, binaries: binaries?, verbose: verbose?, force: force?, upgrade: true) + + new_cask = CaskLoader.load(old_cask.to_s) + + new_cask_installer = + Installer.new(new_cask, binaries: binaries?, + verbose: verbose?, + force: force?, + skip_cask_deps: skip_cask_deps?, + require_sha: require_sha?, + upgrade: true) + + started_upgrade = false + new_artifacts_installed = false + + begin + # Start new Cask's installation steps + new_cask_installer.check_conflicts + + new_cask_installer.fetch + + new_cask_installer.stage + + # Move the old Cask's artifacts back to staging + old_cask_installer.start_upgrade + # And flag it so in case of error + started_upgrade = true + + # Install the new Cask + new_cask_installer.install_artifacts + new_artifacts_installed = true + + new_cask_installer.enable_accessibility_access + + # If successful, wipe the old Cask from staging + old_cask_installer.finalize_upgrade + rescue CaskError => e + new_cask_installer.uninstall_artifacts if new_artifacts_installed + new_cask_installer.purge_versioned_files + old_cask_installer.revert_upgrade if started_upgrade + raise e + end + end + end + + def self.help + "upgrades all outdated casks" + end + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/installer.rb b/Library/Homebrew/cask/lib/hbc/installer.rb index 1063f488bcf28..6056250fc05ce 100644 --- a/Library/Homebrew/cask/lib/hbc/installer.rb +++ b/Library/Homebrew/cask/lib/hbc/installer.rb @@ -19,7 +19,7 @@ class Installer PERSISTENT_METADATA_SUBDIRS = ["gpg"].freeze - def initialize(cask, command: SystemCommand, force: false, skip_cask_deps: false, binaries: true, verbose: false, require_sha: false) + def initialize(cask, command: SystemCommand, force: false, skip_cask_deps: false, binaries: true, verbose: false, require_sha: false, upgrade: false) @cask = cask @command = command @force = force @@ -28,9 +28,10 @@ def initialize(cask, command: SystemCommand, force: false, skip_cask_deps: false @verbose = verbose @require_sha = require_sha @reinstall = false + @upgrade = upgrade end - attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?, :verbose? + attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?, :upgrade?, :verbose? def self.print_caveats(cask) odebug "Printing caveats" @@ -82,7 +83,7 @@ def stage def install odebug "Hbc::Installer#install" - if @cask.installed? && !force? && !@reinstall + if @cask.installed? && !force? && !@reinstall && !upgrade? raise CaskAlreadyInstalledError, @cask end @@ -129,13 +130,13 @@ def uninstall_existing_cask installed_cask = installed_caskfile.exist? ? CaskLoader.load(installed_caskfile) : @cask # Always force uninstallation, ignore method parameter - Installer.new(installed_cask, binaries: binaries?, verbose: verbose?, force: true).uninstall + Installer.new(installed_cask, binaries: binaries?, verbose: verbose?, force: true, upgrade: upgrade?).uninstall end def summary s = "" s << "#{Emoji.install_badge} " if Emoji.enabled? - s << "#{@cask} was successfully installed!" + s << "#{@cask} was successfully #{upgrade? ? "upgraded" : "installed"}!" end def download @@ -367,12 +368,31 @@ def save_caskfile def uninstall oh1 "Uninstalling Cask #{@cask}" disable_accessibility_access - uninstall_artifacts + uninstall_artifacts(clear: true) purge_versioned_files purge_caskroom_path if force? end - def uninstall_artifacts + def start_upgrade + oh1 "Starting upgrade for Cask #{@cask}" + + disable_accessibility_access + uninstall_artifacts + end + + def revert_upgrade + opoo "Reverting upgrade for Cask #{@cask}" + install_artifacts + enable_accessibility_access + end + + def finalize_upgrade + purge_versioned_files + + puts summary + end + + def uninstall_artifacts(clear: false) odebug "Un-installing artifacts" artifacts = @cask.artifacts @@ -381,7 +401,7 @@ def uninstall_artifacts artifacts.each do |artifact| next unless artifact.respond_to?(:uninstall_phase) odebug "Un-installing artifact of class #{artifact.class}" - artifact.uninstall_phase(command: @command, verbose: verbose?, force: force?) + artifact.uninstall_phase(command: @command, verbose: verbose?, skip: clear, force: force?) end end @@ -405,7 +425,7 @@ def gain_permissions_remove(path) end def purge_versioned_files - odebug "Purging files for version #{@cask.version} of Cask #{@cask}" + ohai "Purging files for version #{@cask.version} of Cask #{@cask}" # versioned staged distribution gain_permissions_remove(@cask.staged_path) if !@cask.staged_path.nil? && @cask.staged_path.exist? @@ -420,10 +440,10 @@ def purge_versioned_files end end @cask.metadata_versioned_path.rmdir_if_possible - @cask.metadata_master_container_path.rmdir_if_possible + @cask.metadata_master_container_path.rmdir_if_possible unless upgrade? # toplevel staged distribution - @cask.caskroom_path.rmdir_if_possible + @cask.caskroom_path.rmdir_if_possible unless upgrade? end def purge_caskroom_path diff --git a/Library/Homebrew/test/cask/cli/reinstall_spec.rb b/Library/Homebrew/test/cask/cli/reinstall_spec.rb index 5e551e5b5a2fb..3737a7a701357 100644 --- a/Library/Homebrew/test/cask/cli/reinstall_spec.rb +++ b/Library/Homebrew/test/cask/cli/reinstall_spec.rb @@ -13,7 +13,8 @@ Already downloaded: .*local-caffeine--1.2.3.zip ==> Verifying checksum for Cask local-caffeine ==> Uninstalling Cask local-caffeine - ==> Removing App '.*Caffeine.app'. + ==> Moving App 'Caffeine.app' back to '.*Caffeine.app'. + ==> Purging files for version 1.2.3 of Cask local-caffeine ==> Installing Cask local-caffeine ==> Moving App 'Caffeine.app' to '.*Caffeine.app'. .*local-caffeine was successfully installed! diff --git a/Library/Homebrew/test/cask/cli/uninstall_spec.rb b/Library/Homebrew/test/cask/cli/uninstall_spec.rb index 1ab8f7e4da89a..345e1b9f230a6 100644 --- a/Library/Homebrew/test/cask/cli/uninstall_spec.rb +++ b/Library/Homebrew/test/cask/cli/uninstall_spec.rb @@ -12,7 +12,8 @@ output = Regexp.new <<~EOS ==> Uninstalling Cask local-caffeine - ==> Removing App '.*Caffeine.app'. + ==> Moving App 'Caffeine.app' back to '.*Caffeine.app'. + ==> Purging files for version 1.2.3 of Cask local-caffeine EOS expect { diff --git a/Library/Homebrew/test/cask/cli/upgrade_spec.rb b/Library/Homebrew/test/cask/cli/upgrade_spec.rb new file mode 100644 index 0000000000000..5f389d695019c --- /dev/null +++ b/Library/Homebrew/test/cask/cli/upgrade_spec.rb @@ -0,0 +1,211 @@ +require_relative "shared_examples/invalid_option" + +describe Hbc::CLI::Upgrade, :cask do + it_behaves_like "a command that handles invalid options" + + context "successful upgrade" do + let(:installed) { + [ + "outdated/local-caffeine", + "outdated/local-transmission", + "outdated/auto-updates", + ] + } + + before(:example) do + installed.each { |cask| Hbc::CLI::Install.run(cask) } + + allow_any_instance_of(described_class).to receive(:verbose?).and_return(true) + end + + describe 'without --greedy it ignores the Casks with "version latest" or "auto_updates true"' do + it "updates all the installed Casks when no token is provided" do + local_caffeine = Hbc::CaskLoader.load("local-caffeine") + local_caffeine_path = Hbc.appdir.join("Caffeine.app") + local_transmission = Hbc::CaskLoader.load("local-transmission") + local_transmission_path = Hbc.appdir.join("Transmission.app") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.2") + + expect(local_transmission).to be_installed + expect(local_transmission_path).to be_a_directory + expect(local_transmission.versions).to include("2.60") + + described_class.run + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.3") + + expect(local_transmission).to be_installed + expect(local_transmission_path).to be_a_directory + expect(local_transmission.versions).to include("2.61") + end + + it "updates only the Casks specified in the command line" do + local_caffeine = Hbc::CaskLoader.load("local-caffeine") + local_caffeine_path = Hbc.appdir.join("Caffeine.app") + local_transmission = Hbc::CaskLoader.load("local-transmission") + local_transmission_path = Hbc.appdir.join("Transmission.app") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.2") + + expect(local_transmission).to be_installed + expect(local_transmission_path).to be_a_directory + expect(local_transmission.versions).to include("2.60") + + described_class.run("local-caffeine") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.3") + + expect(local_transmission).to be_installed + expect(local_transmission_path).to be_a_directory + expect(local_transmission.versions).to include("2.60") + end + + it 'updates "auto_updates" and "latest" Casks when their tokens are provided in the command line' do + local_caffeine = Hbc::CaskLoader.load("local-caffeine") + local_caffeine_path = Hbc.appdir.join("Caffeine.app") + auto_updates = Hbc::CaskLoader.load("auto-updates") + auto_updates_path = Hbc.appdir.join("MyFancyApp.app") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.2") + + expect(auto_updates).to be_installed + expect(auto_updates_path).to be_a_directory + expect(auto_updates.versions).to include("2.57") + + described_class.run("local-caffeine", "auto-updates") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.3") + + expect(auto_updates).to be_installed + expect(auto_updates_path).to be_a_directory + expect(auto_updates.versions).to include("2.61") + end + end + + describe "with --greedy it checks additional Casks" do + it 'includes the Casks with "auto_updates true" or "version latest"' do + local_caffeine = Hbc::CaskLoader.load("local-caffeine") + local_caffeine_path = Hbc.appdir.join("Caffeine.app") + auto_updates = Hbc::CaskLoader.load("auto-updates") + auto_updates_path = Hbc.appdir.join("MyFancyApp.app") + local_transmission = Hbc::CaskLoader.load("local-transmission") + local_transmission_path = Hbc.appdir.join("Transmission.app") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.2") + + expect(auto_updates).to be_installed + expect(auto_updates_path).to be_a_directory + expect(auto_updates.versions).to include("2.57") + + expect(local_transmission).to be_installed + expect(local_transmission_path).to be_a_directory + expect(local_transmission.versions).to include("2.60") + + described_class.run("--greedy") + + expect(local_caffeine).to be_installed + expect(local_caffeine_path).to be_a_directory + expect(local_caffeine.versions).to include("1.2.3") + + expect(auto_updates).to be_installed + expect(auto_updates_path).to be_a_directory + expect(auto_updates.versions).to include("2.61") + + expect(local_transmission).to be_installed + expect(local_transmission_path).to be_a_directory + expect(local_transmission.versions).to include("2.61") + end + + it 'does not include the Casks with "auto_updates true" when the version did not change' do + cask = Hbc::CaskLoader.load("auto-updates") + cask_path = Hbc.appdir.join("MyFancyApp.app") + + expect(cask).to be_installed + expect(cask_path).to be_a_directory + expect(cask.versions).to include("2.57") + + described_class.run("auto-updates", "--greedy") + + expect(cask).to be_installed + expect(cask_path).to be_a_directory + expect(cask.versions).to include("2.61") + + described_class.run("auto-updates", "--greedy") + + expect(cask).to be_installed + expect(cask_path).to be_a_directory + expect(cask.versions).to include("2.61") + end + end + end + + context "failed upgrade" do + let(:installed) { + [ + "outdated/bad-checksum", + "outdated/will-fail-if-upgraded", + ] + } + + before(:example) do + installed.each { |cask| Hbc::CLI::Install.run(cask) } + + allow_any_instance_of(described_class).to receive(:verbose?).and_return(true) + end + + output_reverted = Regexp.new <<~EOS + Warning: Reverting upgrade for Cask .* + EOS + + it "restores the old Cask if the upgrade failed" do + will_fail_if_upgraded = Hbc::CaskLoader.load("will-fail-if-upgraded") + will_fail_if_upgraded_path = Hbc.appdir.join("container") + + expect(will_fail_if_upgraded).to be_installed + expect(will_fail_if_upgraded_path).to be_a_file + expect(will_fail_if_upgraded.versions).to include("1.2.2") + + expect { + described_class.run("will-fail-if-upgraded") + }.to raise_error(Hbc::CaskError).and output(output_reverted).to_stderr + + expect(will_fail_if_upgraded).to be_installed + expect(will_fail_if_upgraded_path).to be_a_file + expect(will_fail_if_upgraded.versions).to include("1.2.2") + expect(will_fail_if_upgraded.staged_path).to_not exist + end + + it "does not restore the old Cask if the upgrade failed pre-install" do + bad_checksum = Hbc::CaskLoader.load("bad-checksum") + bad_checksum_path = Hbc.appdir.join("Caffeine.app") + + expect(bad_checksum).to be_installed + expect(bad_checksum_path).to be_a_directory + expect(bad_checksum.versions).to include("1.2.2") + + expect { + described_class.run("bad-checksum") + }.to raise_error(Hbc::CaskSha256MismatchError).and(not_to_output(output_reverted).to_stderr) + + expect(bad_checksum).to be_installed + expect(bad_checksum_path).to be_a_directory + expect(bad_checksum.versions).to include("1.2.2") + expect(bad_checksum.staged_path).to_not exist + end + end +end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/auto-updates.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/auto-updates.rb index 1cada2561cfb5..4f61455bb1bb6 100644 --- a/Library/Homebrew/test/support/fixtures/cask/Casks/auto-updates.rb +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/auto-updates.rb @@ -1,11 +1,11 @@ cask 'auto-updates' do version '2.61' - sha256 'e44ffa103fbf83f55c8d0b1bea309a43b2880798dae8620b1ee8da5e1095ec68' + sha256 '5633c3a0f2e572cbf021507dec78c50998b398c343232bdfc7e26221d0a5db4d' - url "file://#{TEST_FIXTURE_DIR}/cask/transmission-2.61.dmg" - homepage 'http://example.com/auto-updates' + url "file://#{TEST_FIXTURE_DIR}/cask/MyFancyApp.zip" + homepage 'http://example.com/MyFancyApp' auto_updates true - app 'Transmission.app' + app 'MyFancyApp/MyFancyApp.app' end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/auto-updates.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/auto-updates.rb index e202f5a1632e7..5844a0762cc7f 100644 --- a/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/auto-updates.rb +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/auto-updates.rb @@ -1,11 +1,11 @@ cask 'auto-updates' do version '2.57' - sha256 'e44ffa103fbf83f55c8d0b1bea309a43b2880798dae8620b1ee8da5e1095ec68' + sha256 '5633c3a0f2e572cbf021507dec78c50998b398c343232bdfc7e26221d0a5db4d' - url "file://#{TEST_FIXTURE_DIR}/cask/transmission-2.61.dmg" - homepage 'http://example.com/auto-updates' + url "file://#{TEST_FIXTURE_DIR}/cask/MyFancyApp.zip" + homepage 'http://example.com/MyFancyApp' auto_updates true - app 'Transmission.app' + app 'MyFancyApp/MyFancyApp.app' end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/bad-checksum.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/bad-checksum.rb new file mode 100644 index 0000000000000..5e7e744830225 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/bad-checksum.rb @@ -0,0 +1,9 @@ +cask 'bad-checksum' do + version '1.2.2' + sha256 '67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94' + + url "file://#{TEST_FIXTURE_DIR}/cask/caffeine.zip" + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/will-fail-if-upgraded.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/will-fail-if-upgraded.rb new file mode 100644 index 0000000000000..7735ffa848a8a --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/will-fail-if-upgraded.rb @@ -0,0 +1,9 @@ +cask 'will-fail-if-upgraded' do + version '1.2.2' + sha256 'fab685fabf73d5a9382581ce8698fce9408f5feaa49fa10d9bc6c510493300f5' + + url "file://#{TEST_FIXTURE_DIR}/cask/container.tar.gz" + homepage 'https://example.com/container-tar-gz' + + app 'container' +end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/will-fail-if-upgraded.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/will-fail-if-upgraded.rb new file mode 100644 index 0000000000000..99ed4b87cfd08 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/will-fail-if-upgraded.rb @@ -0,0 +1,9 @@ +cask 'will-fail-if-upgraded' do + version '1.2.3' + sha256 'e44ffa103fbf83f55c8d0b1bea309a43b2880798dae8620b1ee8da5e1095ec68' + + url "file://#{TEST_FIXTURE_DIR}/cask/transmission-2.61.dmg" + homepage 'http://example.com/local-transmission' + + app 'container' +end