diff --git a/Library/Homebrew/cask/artifact/moved.rb b/Library/Homebrew/cask/artifact/moved.rb index 58ae0e9d95..065be564b9 100644 --- a/Library/Homebrew/cask/artifact/moved.rb +++ b/Library/Homebrew/cask/artifact/moved.rb @@ -34,8 +34,33 @@ module Cask private - def move(force: false, command: nil, **options) + def move(adopt: false, force: false, verbose: false, command: nil, **options) + unless source.exist? + raise CaskError, "It seems the #{self.class.english_name} source '#{source}' is not there." + end + if Utils.path_occupied?(target) + if adopt + ohai "Adopting existing #{self.class.english_name} at '#{target}'" + same = command.run( + "/usr/bin/diff", + args: ["-rq", source, target], + verbose: verbose, + print_stdout: verbose, + ).success? + + unless same + raise CaskError, + "It seems the existing #{self.class.english_name} is different from " \ + "the one being installed." + end + + # Simulate moving the source to the target location + source.rmtree + + return post_move(command) + end + message = "It seems there is already #{self.class.english_article} " \ "#{self.class.english_name} at '#{target}'" raise CaskError, "#{message}." unless force @@ -44,10 +69,6 @@ module Cask delete(target, force: force, command: command, **options) end - unless source.exist? - raise CaskError, "It seems the #{self.class.english_name} source '#{source}' is not there." - end - ohai "Moving #{self.class.english_name} '#{source.basename}' to '#{target}'" if target.dirname.ascend.find(&:directory?).writable? target.dirname.mkpath @@ -61,6 +82,11 @@ module Cask command.run!("/bin/mv", args: [source, target], sudo: true) end + post_move(command) + end + + # Performs any actions necessary after the source has been moved to the target location. + def post_move(command) FileUtils.ln_sf target, source add_altname_metadata(target, source.basename, command: command) diff --git a/Library/Homebrew/cask/cmd/install.rb b/Library/Homebrew/cask/cmd/install.rb index a52b412391..e19e64eb01 100644 --- a/Library/Homebrew/cask/cmd/install.rb +++ b/Library/Homebrew/cask/cmd/install.rb @@ -10,6 +10,10 @@ module Cask extend T::Sig OPTIONS = [ + [:switch, "--adopt", { + description: "Adopt existing artifacts in the destination that are identical to those being installed. " \ + "Cannot be combined with --force.", + }], [:switch, "--skip-cask-deps", { description: "Skip installing cask dependencies.", }], @@ -40,6 +44,7 @@ module Cask binaries: args.binaries?, verbose: args.verbose?, force: args.force?, + adopt: args.adopt?, skip_cask_deps: args.skip_cask_deps?, require_sha: args.require_sha?, quarantine: args.quarantine?, @@ -52,6 +57,7 @@ module Cask *casks, verbose: nil, force: nil, + adopt: nil, binaries: nil, skip_cask_deps: nil, require_sha: nil, @@ -65,6 +71,7 @@ module Cask options = { verbose: verbose, force: force, + adopt: adopt, binaries: binaries, skip_cask_deps: skip_cask_deps, require_sha: require_sha, diff --git a/Library/Homebrew/cask/cmd/reinstall.rb b/Library/Homebrew/cask/cmd/reinstall.rb index 9637805450..ee7040a411 100644 --- a/Library/Homebrew/cask/cmd/reinstall.rb +++ b/Library/Homebrew/cask/cmd/reinstall.rb @@ -16,6 +16,7 @@ module Cask binaries: args.binaries?, verbose: args.verbose?, force: args.force?, + adopt: args.adopt?, skip_cask_deps: args.skip_cask_deps?, require_sha: args.require_sha?, quarantine: args.quarantine?, @@ -27,6 +28,7 @@ module Cask *casks, verbose: nil, force: nil, + adopt: nil, skip_cask_deps: nil, binaries: nil, require_sha: nil, @@ -39,6 +41,7 @@ module Cask binaries: binaries, verbose: verbose, force: force, + adopt: adopt, skip_cask_deps: skip_cask_deps, require_sha: require_sha, quarantine: quarantine, diff --git a/Library/Homebrew/cask/cmd/upgrade.rb b/Library/Homebrew/cask/cmd/upgrade.rb index a96275cbe2..b1ca718c93 100644 --- a/Library/Homebrew/cask/cmd/upgrade.rb +++ b/Library/Homebrew/cask/cmd/upgrade.rb @@ -13,6 +13,10 @@ module Cask extend T::Sig OPTIONS = [ + [:switch, "--adopt", { + description: "Adopt existing artifacts in the destination that are identical to those being installed. " \ + "Cannot be combined with --force.", + }], [:switch, "--skip-cask-deps", { description: "Skip installing cask dependencies.", }], @@ -66,6 +70,7 @@ module Cask casks: Cask, args: Homebrew::CLI::Args, force: T.nilable(T::Boolean), + adopt: T.nilable(T::Boolean), greedy: T.nilable(T::Boolean), greedy_latest: T.nilable(T::Boolean), greedy_auto_updates: T.nilable(T::Boolean), @@ -81,6 +86,7 @@ module Cask *casks, args:, force: false, + adopt: false, greedy: false, greedy_latest: false, greedy_auto_updates: false, @@ -156,7 +162,7 @@ module Cask upgradable_casks.each do |(old_cask, new_cask)| upgrade_cask( old_cask, new_cask, - binaries: binaries, force: force, skip_cask_deps: skip_cask_deps, verbose: verbose, + binaries: binaries, force: force, adopt: adopt, skip_cask_deps: skip_cask_deps, verbose: verbose, quarantine: quarantine, require_sha: require_sha ) rescue => e @@ -171,7 +177,7 @@ module Cask def self.upgrade_cask( old_cask, new_cask, - binaries:, force:, quarantine:, require_sha:, skip_cask_deps:, verbose: + binaries:, force:, adopt:, quarantine:, require_sha:, skip_cask_deps:, verbose: ) require "cask/installer" @@ -195,6 +201,7 @@ module Cask binaries: binaries, verbose: verbose, force: force, + adopt: adopt, skip_cask_deps: skip_cask_deps, require_sha: require_sha, upgrade: true, diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index c0417e311d..5c92615e9e 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -20,7 +20,7 @@ module Cask extend Predicable - def initialize(cask, command: SystemCommand, force: false, + def initialize(cask, command: SystemCommand, force: false, adopt: false, skip_cask_deps: false, binaries: true, verbose: false, zap: false, require_sha: false, upgrade: false, installed_as_dependency: false, quarantine: true, @@ -28,6 +28,7 @@ module Cask @cask = cask @command = command @force = force + @adopt = adopt @skip_cask_deps = skip_cask_deps @binaries = binaries @verbose = verbose @@ -41,7 +42,7 @@ module Cask @quiet = quiet end - attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?, + attr_predicate :binaries?, :force?, :adopt?, :skip_cask_deps?, :require_sha?, :reinstall?, :upgrade?, :verbose?, :zap?, :installed_as_dependency?, :quarantine?, :quiet? @@ -237,7 +238,7 @@ module Cask next if artifact.is_a?(Artifact::Binary) && !binaries? - artifact.install_phase(command: @command, verbose: verbose?, force: force?) + artifact.install_phase(command: @command, verbose: verbose?, adopt: adopt?, force: force?) already_installed_artifacts.unshift(artifact) end @@ -348,6 +349,7 @@ module Cask Installer.new( cask_or_formula, + adopt: adopt?, binaries: binaries?, verbose: verbose?, installed_as_dependency: true, diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index f9eeb6a0b0..be9fb54dca 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -140,6 +140,7 @@ module Homebrew conflicts "--ignore-dependencies", "--only-dependencies" conflicts "--build-from-source", "--build-bottle", "--force-bottle" + conflicts "--adopt", "--force" named_args [:formula, :cask], min: 1 end @@ -193,6 +194,7 @@ module Homebrew binaries: args.binaries?, verbose: args.verbose?, force: args.force?, + adopt: args.adopt?, require_sha: args.require_sha?, skip_cask_deps: args.skip_cask_deps?, quarantine: args.quarantine?, diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index 863f8162db..b6c7ea9f0f 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -152,6 +152,7 @@ module Homebrew binaries: args.binaries?, verbose: args.verbose?, force: args.force?, + adopt: args.adopt?, require_sha: args.require_sha?, skip_cask_deps: args.skip_cask_deps?, quarantine: args.quarantine?, diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index e33b3cab0b..b79e52a8f8 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -97,6 +97,7 @@ module Homebrew cask_options conflicts "--build-from-source", "--force-bottle" + conflicts "--adopt", "--force" named_args [:outdated_formula, :outdated_cask] end @@ -226,6 +227,7 @@ module Homebrew Cask::Cmd::Upgrade.upgrade_casks( *casks, force: args.force?, + adopt: args.adopt?, greedy: args.greedy?, greedy_latest: args.greedy_latest?, greedy_auto_updates: args.greedy_auto_updates?, diff --git a/Library/Homebrew/test/cask/artifact/app_spec.rb b/Library/Homebrew/test/cask/artifact/app_spec.rb index 23a5cd944f..d9960b82e4 100644 --- a/Library/Homebrew/test/cask/artifact/app_spec.rb +++ b/Library/Homebrew/test/cask/artifact/app_spec.rb @@ -4,13 +4,14 @@ describe Cask::Artifact::App, :cask do let(:cask) { Cask::CaskLoader.load(cask_path("local-caffeine")) } let(:command) { SystemCommand } + let(:adopt) { false } let(:force) { false } let(:app) { cask.artifacts.find { |a| a.is_a?(described_class) } } let(:source_path) { cask.staged_path.join("Caffeine.app") } let(:target_path) { cask.config.appdir.join("Caffeine.app") } - let(:install_phase) { app.install_phase(command: command, force: force) } + let(:install_phase) { app.install_phase(command: command, adopt: adopt, force: force) } let(:uninstall_phase) { app.uninstall_phase(command: command, force: force) } before do @@ -79,6 +80,57 @@ describe Cask::Artifact::App, :cask do expect(contents_path).not_to exist end + describe "given the adopt option" do + let(:adopt) { true } + + describe "when the target compares different from the source" do + it "avoids clobbering the existing app" do + stdout = <<~EOS + ==> Adopting existing App at '#{target_path}' + EOS + + expect { install_phase } + .to output(stdout).to_stdout + .and raise_error( + Cask::CaskError, + "It seems the existing App is different from the one being installed.", + ) + + expect(source_path).to be_a_directory + expect(target_path).to be_a_directory + expect(File.identical?(source_path, target_path)).to be false + + contents_path = target_path.join("Contents/Info.plist") + expect(contents_path).not_to exist + end + end + + describe "when the target compares the same as the source" do + before do + target_path.delete + FileUtils.cp_r source_path, target_path + end + + it "adopts the existing app" do + stdout = <<~EOS + ==> Adopting existing App at '#{target_path}' + EOS + + stderr = "" + + expect { install_phase } + .to output(stdout).to_stdout + .and output(stderr).to_stderr + + expect(source_path).to be_a_symlink + expect(target_path).to be_a_directory + + contents_path = target_path.join("Contents/Info.plist") + expect(contents_path).to exist + end + end + end + describe "given the force option" do let(:force) { true }