diff --git a/Library/Homebrew/cask/artifact/abstract_uninstall.rb b/Library/Homebrew/cask/artifact/abstract_uninstall.rb index c23be839d5..525c5c7d0c 100644 --- a/Library/Homebrew/cask/artifact/abstract_uninstall.rb +++ b/Library/Homebrew/cask/artifact/abstract_uninstall.rb @@ -285,8 +285,8 @@ module Cask end end - def uninstall_login_item(*login_items, command: nil, upgrade: false, **_) - return if upgrade + def uninstall_login_item(*login_items, command: nil, successor: nil, **_) + return if successor apps = cask.artifacts.select { |a| a.class.dsl_key == :app } derived_login_items = apps.map { |a| { path: a.target } } diff --git a/Library/Homebrew/cask/artifact/moved.rb b/Library/Homebrew/cask/artifact/moved.rb index 0c117d2611..48861a0f61 100644 --- a/Library/Homebrew/cask/artifact/moved.rb +++ b/Library/Homebrew/cask/artifact/moved.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "cask/artifact/relocated" +require "cask/quarantine" module Cask module Artifact @@ -32,39 +33,51 @@ module Cask private - def move(adopt: false, force: false, verbose: false, command: nil, **options) + def move(adopt: false, force: false, verbose: false, predecessor: nil, reinstall: 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: ["--recursive", "--brief", source, target], - verbose: verbose, - print_stdout: verbose, - ).success? + if target.directory? && target.children.empty? && matching_artifact?(predecessor) + # An upgrade removed the directory contents but left the directory itself (see below). + unless source.directory? + if target.parent.writable? && !force + target.rmdir + else + Utils.gain_permissions_remove(target, command: command) + end + end + else + if adopt + ohai "Adopting existing #{self.class.english_name} at '#{target}'" + same = command.run( + "/usr/bin/diff", + args: ["--recursive", "--brief", 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." + unless same + raise CaskError, + "It seems the existing #{self.class.english_name} is different from " \ + "the one being installed." + end + + # Remove the source as we don't need to move it to the target location + source.rmtree + + return post_move(command) end - # Remove the source as we don't need to move it to the target location - source.rmtree + message = "It seems there is already #{self.class.english_article} " \ + "#{self.class.english_name} at '#{target}'" + raise CaskError, "#{message}." unless force - return post_move(command) + opoo "#{message}; overwriting." + delete(target, force: force, command: command, **options) end - - 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(target, force: force, command: command, **options) end ohai "Moving #{self.class.english_name} '#{source.basename}' to '#{target}'" @@ -77,7 +90,16 @@ module Cask end end - if target.dirname.writable? + if target.directory? + if target.writable? + source.children.each { |child| FileUtils.move(child, target + child.basename) } + else + command.run!("/bin/cp", args: ["-pR", "#{source}/*", "#{source}/.*", "#{target}/"], + sudo: true) + end + Quarantine.copy_xattrs(source, target) + source.rmtree + elsif target.dirname.writable? FileUtils.move(source, target) else # default sudo user isn't necessarily able to write to Homebrew's locations @@ -96,6 +118,14 @@ module Cask add_altname_metadata(target, source.basename, command: command) end + def matching_artifact?(cask) + return false unless cask + + cask.artifacts.any? do |a| + a.instance_of?(self.class) && instance_of?(a.class) && a.target == target + end + end + def move_back(skip: false, force: false, command: nil, **options) FileUtils.rm source if source.symlink? && source.dirname.join(source.readlink) == target @@ -123,13 +153,23 @@ module Cask delete(target, force: force, command: command, **options) end - def delete(target, force: false, command: nil, **_) + def delete(target, force: false, successor: nil, command: nil, **_) ohai "Removing #{self.class.english_name} '#{target}'" raise CaskError, "Cannot remove undeletable #{self.class.english_name}." if MacOS.undeletable?(target) return unless Utils.path_occupied?(target) - if target.parent.writable? && !force + if target.directory? && matching_artifact?(successor) + # If an app folder is deleted, macOS considers the app uninstalled and removes some data. + # Remove only the contents to handle this case. + target.children.each do |child| + if target.writable? && !force + child.rmtree + else + Utils.gain_permissions_remove(child, command: command) + end + end + elsif target.parent.writable? && !force target.rmtree else Utils.gain_permissions_remove(target, command: command) diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index d5664b6b6e..16c3d5791d 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -20,7 +20,7 @@ module Cask def initialize(cask, command: SystemCommand, force: false, adopt: false, skip_cask_deps: false, binaries: true, verbose: false, - zap: false, require_sha: false, upgrade: false, + zap: false, require_sha: false, upgrade: false, reinstall: false, installed_as_dependency: false, quarantine: true, verify_download_integrity: true, quiet: false) @cask = cask @@ -32,7 +32,7 @@ module Cask @verbose = verbose @zap = zap @require_sha = require_sha - @reinstall = false + @reinstall = reinstall @upgrade = upgrade @installed_as_dependency = installed_as_dependency @quarantine = quarantine @@ -93,6 +93,7 @@ module Cask raise CaskAlreadyInstalledError, @cask end + predecessor = @cask if reinstall? && @cask.installed? check_conflicts @@ -108,7 +109,7 @@ module Cask @cask.config = @cask.default_config.merge(old_config) - install_artifacts + install_artifacts(predecessor: predecessor) if (tap = @cask.tap) && tap.should_report_analytics? ::Utils::Analytics.report_event(:cask_install, package_name: @cask.token, tap_name: tap.name, @@ -141,18 +142,12 @@ on_request: true) end end - def reinstall - odebug "Cask::Installer#reinstall" - @reinstall = true - install - end - def uninstall_existing_cask return unless @cask.installed? # Always force uninstallation, ignore method parameter - cask_installer = Installer.new(@cask, verbose: verbose?, force: true, upgrade: upgrade?) - zap? ? cask_installer.zap : cask_installer.uninstall + cask_installer = Installer.new(@cask, verbose: verbose?, force: true, upgrade: upgrade?, reinstall: true) + zap? ? cask_installer.zap : cask_installer.uninstall(successor: @cask) end sig { returns(String) } @@ -219,7 +214,7 @@ on_request: true) Quarantine.propagate(from: primary_container.path, to: to) end - def install_artifacts + def install_artifacts(predecessor: nil) artifacts = @cask.artifacts already_installed_artifacts = [] @@ -232,7 +227,8 @@ on_request: true) next if artifact.is_a?(Artifact::Binary) && !binaries? - artifact.install_phase(command: @command, verbose: verbose?, adopt: adopt?, force: force?) + artifact.install_phase(command: @command, verbose: verbose?, adopt: adopt?, force: force?, + predecessor: predecessor) already_installed_artifacts.unshift(artifact) end @@ -394,10 +390,10 @@ on_request: true) @cask.download_sha_path.atomic_write(@cask.new_download_sha) if @cask.checksumable? end - def uninstall + def uninstall(successor: nil) load_installed_caskfile! oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}" - uninstall_artifacts(clear: true) + uninstall_artifacts(clear: true, successor: successor) if !reinstall? && !upgrade? remove_download_sha remove_config_file @@ -415,8 +411,8 @@ on_request: true) FileUtils.rm_f @cask.download_sha_path if @cask.download_sha_path.exist? end - def start_upgrade - uninstall_artifacts + def start_upgrade(successor:) + uninstall_artifacts(successor: successor) backup end @@ -435,10 +431,10 @@ on_request: true) backup_metadata_path.rename @cask.metadata_versioned_path end - def revert_upgrade + def revert_upgrade(predecessor) opoo "Reverting upgrade for Cask #{@cask}" restore_backup - install_artifacts + install_artifacts(predecessor: predecessor) end def finalize_upgrade @@ -449,7 +445,7 @@ on_request: true) puts summary end - def uninstall_artifacts(clear: false) + def uninstall_artifacts(clear: false, successor: nil) artifacts = @cask.artifacts odebug "Uninstalling artifacts" @@ -459,7 +455,11 @@ on_request: true) if artifact.respond_to?(:uninstall_phase) odebug "Uninstalling artifact of class #{artifact.class}" artifact.uninstall_phase( - command: @command, verbose: verbose?, skip: clear, force: force?, upgrade: upgrade?, + command: @command, + verbose: verbose?, + skip: clear, + force: force?, + successor: successor, ) end @@ -467,7 +467,11 @@ on_request: true) odebug "Post-uninstalling artifact of class #{artifact.class}" artifact.post_uninstall_phase( - command: @command, verbose: verbose?, skip: clear, force: force?, upgrade: upgrade?, + command: @command, + verbose: verbose?, + skip: clear, + force: force?, + successor: successor, ) end end diff --git a/Library/Homebrew/cask/quarantine.rb b/Library/Homebrew/cask/quarantine.rb index d006d096ff..66e5f6bfe8 100644 --- a/Library/Homebrew/cask/quarantine.rb +++ b/Library/Homebrew/cask/quarantine.rb @@ -12,6 +12,7 @@ module Cask QUARANTINE_ATTRIBUTE = "com.apple.quarantine" QUARANTINE_SCRIPT = (HOMEBREW_LIBRARY_PATH/"cask/utils/quarantine.swift").freeze + COPY_XATTRS_SCRIPT = (HOMEBREW_LIBRARY_PATH/"cask/utils/copy-xattrs.swift").freeze def self.swift @swift ||= DevelopmentTools.locate("swift") @@ -172,5 +173,19 @@ module Cask raise CaskQuarantinePropagationError.new(to, quarantiner.stderr) end + + def self.copy_xattrs(from, to) + odebug "Copying xattrs from #{from} to #{to}" + + system_command!( + swift, + args: [ + *swift_target_args, + COPY_XATTRS_SCRIPT, + from, + to, + ], + ) + end end end diff --git a/Library/Homebrew/cask/reinstall.rb b/Library/Homebrew/cask/reinstall.rb index aa4c361542..d756aa3c56 100644 --- a/Library/Homebrew/cask/reinstall.rb +++ b/Library/Homebrew/cask/reinstall.rb @@ -26,8 +26,9 @@ module Cask force: force, skip_cask_deps: skip_cask_deps, require_sha: require_sha, + reinstall: true, quarantine: quarantine, - zap: zap).reinstall + zap: zap).install end end end diff --git a/Library/Homebrew/cask/upgrade.rb b/Library/Homebrew/cask/upgrade.rb index ac7e53c9f4..6e350d96f9 100644 --- a/Library/Homebrew/cask/upgrade.rb +++ b/Library/Homebrew/cask/upgrade.rb @@ -180,22 +180,22 @@ module Cask new_cask_installer.fetch # Move the old cask's artifacts back to staging - old_cask_installer.start_upgrade + old_cask_installer.start_upgrade(successor: new_cask) # And flag it so in case of error started_upgrade = true # Install the new cask new_cask_installer.stage - new_cask_installer.install_artifacts + new_cask_installer.install_artifacts(predecessor: old_cask) new_artifacts_installed = true # If successful, wipe the old cask from staging old_cask_installer.finalize_upgrade rescue => e - new_cask_installer.uninstall_artifacts if new_artifacts_installed + new_cask_installer.uninstall_artifacts(successor: old_cask) if new_artifacts_installed new_cask_installer.purge_versioned_files - old_cask_installer.revert_upgrade if started_upgrade + old_cask_installer.revert_upgrade(predecessor: new_cask) if started_upgrade raise e end diff --git a/Library/Homebrew/cask/utils/copy-xattrs.swift b/Library/Homebrew/cask/utils/copy-xattrs.swift new file mode 100755 index 0000000000..794242ed13 --- /dev/null +++ b/Library/Homebrew/cask/utils/copy-xattrs.swift @@ -0,0 +1,80 @@ +#!/usr/bin/swift + +import Foundation + +struct SwiftErr: TextOutputStream { + public static var stream = SwiftErr() + + mutating func write(_ string: String) { + fputs(string, stderr) + } +} + +guard CommandLine.arguments.count >= 3 else { + print("Usage: swift copy-xattrs.swift ") + exit(2) +} + +CommandLine.arguments[2].withCString { destinationPath in + let destinationNamesLength = listxattr(destinationPath, nil, 0, 0) + if destinationNamesLength == -1 { + print("listxattr for destination failed: \(errno)", to: &SwiftErr.stream) + exit(1) + } + let destinationNamesBuffer = UnsafeMutablePointer.allocate(capacity: destinationNamesLength) + if listxattr(destinationPath, destinationNamesBuffer, destinationNamesLength, 0) != destinationNamesLength { + print("Attributes changed during system call", to: &SwiftErr.stream) + exit(1) + } + + var destinationNamesIndex = 0 + while destinationNamesIndex < destinationNamesLength { + let attribute = destinationNamesBuffer + destinationNamesIndex + + if removexattr(destinationPath, attribute, 0) != 0 { + print("removexattr for \(String(cString: attribute)) failed: \(errno)", to: &SwiftErr.stream) + exit(1) + } + + destinationNamesIndex += strlen(attribute) + 1 + } + destinationNamesBuffer.deallocate() + + CommandLine.arguments[1].withCString { sourcePath in + let sourceNamesLength = listxattr(sourcePath, nil, 0, 0) + if sourceNamesLength == -1 { + print("listxattr for source failed: \(errno)", to: &SwiftErr.stream) + exit(1) + } + let sourceNamesBuffer = UnsafeMutablePointer.allocate(capacity: sourceNamesLength) + if listxattr(sourcePath, sourceNamesBuffer, sourceNamesLength, 0) != sourceNamesLength { + print("Attributes changed during system call", to: &SwiftErr.stream) + exit(1) + } + + var sourceNamesIndex = 0 + while sourceNamesIndex < sourceNamesLength { + let attribute = sourceNamesBuffer + sourceNamesIndex + + let valueLength = getxattr(sourcePath, attribute, nil, 0, 0, 0) + if valueLength == -1 { + print("getxattr for \(String(cString: attribute)) failed: \(errno)", to: &SwiftErr.stream) + exit(1) + } + let valueBuffer = UnsafeMutablePointer.allocate(capacity: valueLength) + if getxattr(sourcePath, attribute, valueBuffer, valueLength, 0, 0) != valueLength { + print("Attributes changed during system call", to: &SwiftErr.stream) + exit(1) + } + + if setxattr(destinationPath, attribute, valueBuffer, valueLength, 0, 0) != 0 { + print("setxattr for \(String(cString: attribute)) failed: \(errno)", to: &SwiftErr.stream) + exit(1) + } + + valueBuffer.deallocate() + sourceNamesIndex += strlen(attribute) + 1 + } + sourceNamesBuffer.deallocate() + } +} diff --git a/Library/Homebrew/test/cask/artifact/app_spec.rb b/Library/Homebrew/test/cask/artifact/app_spec.rb index 4e45f09ba0..796f03f19f 100644 --- a/Library/Homebrew/test/cask/artifact/app_spec.rb +++ b/Library/Homebrew/test/cask/artifact/app_spec.rb @@ -310,4 +310,29 @@ describe Cask::Artifact::App, :cask do end end end + + describe "upgrade" do + # Fix for https://github.com/Homebrew/homebrew-cask/issues/102721 + it "reuses the same directory" do + install_phase + + contents_path = target_path.join("Contents/Info.plist") + + expect(target_path).to exist + inode = target_path.stat.ino + expect(contents_path).to exist + + app.uninstall_phase(command: command, force: force, successor: cask) + + expect(target_path).to exist + expect(target_path.children).to be_empty + expect(contents_path).not_to exist + + app.install_phase(command: command, adopt: adopt, force: force, predecessor: cask) + expect(target_path).to exist + expect(target_path.stat.ino).to eq(inode) + + expect(contents_path).to exist + end + end end diff --git a/Library/Homebrew/test/cask/upgrade_spec.rb b/Library/Homebrew/test/cask/upgrade_spec.rb index c690c030f1..d9c9471256 100644 --- a/Library/Homebrew/test/cask/upgrade_spec.rb +++ b/Library/Homebrew/test/cask/upgrade_spec.rb @@ -12,6 +12,9 @@ describe Cask::Upgrade, :cask do let(:local_transmission) { Cask::CaskLoader.load("local-transmission") } let(:local_caffeine_path) { local_caffeine.config.appdir.join("Caffeine.app") } let(:local_caffeine) { Cask::CaskLoader.load("local-caffeine") } + let(:renamed_app) { Cask::CaskLoader.load("renamed-app") } + let(:renamed_app_old_path) { renamed_app.config.appdir.join("OldApp.app") } + let(:renamed_app_new_path) { renamed_app.config.appdir.join("NewApp.app") } let(:args) { Homebrew::CLI::Args.new } context "when the upgrade is successful" do @@ -21,6 +24,7 @@ describe Cask::Upgrade, :cask do "outdated/local-transmission", "outdated/auto-updates", "outdated/version-latest", + "outdated/renamed-app", ] end @@ -40,6 +44,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + described_class.upgrade_casks(args: args) expect(local_caffeine).to be_installed @@ -49,6 +58,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission).to be_installed expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.61") + + expect(renamed_app).to be_installed + expect(renamed_app_old_path).not_to be_a_directory + expect(renamed_app_new_path).to be_a_directory + expect(renamed_app.versions).to include("2.0.0") end it "updates only the Casks specified in the command line" do @@ -60,6 +74,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + described_class.upgrade_casks(local_caffeine, args: args) expect(local_caffeine).to be_installed @@ -69,6 +88,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission).to be_installed expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") + + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") end it 'updates "auto_updates" and "latest" Casks when their tokens are provided in the command line' do @@ -106,6 +130,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + expect(version_latest).to be_installed expect(version_latest_path_first).to be_a_directory expect(version_latest.versions).to include("latest") @@ -127,6 +156,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.61") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).not_to be_a_directory + expect(renamed_app_new_path).to be_a_directory + expect(renamed_app.versions).to include("2.0.0") + expect(version_latest).to be_installed expect(version_latest_path_second).to be_a_directory expect(version_latest.versions).to include("latest") @@ -186,6 +220,7 @@ describe Cask::Upgrade, :cask do "outdated/local-transmission", "outdated/auto-updates", "outdated/version-latest", + "outdated/renamed-app", ] end @@ -207,6 +242,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + described_class.upgrade_casks(dry_run: true, args: args) expect(local_caffeine).to be_installed @@ -218,6 +258,12 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") expect(local_transmission.versions).not_to include("2.61") + + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + expect(renamed_app.versions).not_to include("2.0.0") end it "would update only the Casks specified in the command line" do @@ -255,6 +301,11 @@ describe Cask::Upgrade, :cask do expect(auto_updates_path).to be_a_directory expect(auto_updates.versions).to include("2.57") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + described_class.upgrade_casks(local_caffeine, auto_updates, dry_run: true, args: args) expect(local_caffeine).to be_installed @@ -266,6 +317,12 @@ describe Cask::Upgrade, :cask do expect(auto_updates_path).to be_a_directory expect(auto_updates.versions).to include("2.57") expect(auto_updates.versions).not_to include("2.61") + + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + expect(renamed_app.versions).not_to include("2.0.0") end end @@ -285,6 +342,11 @@ describe Cask::Upgrade, :cask do expect(local_transmission_path).to be_a_directory expect(local_transmission.versions).to include("2.60") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + expect(version_latest).to be_installed # Change download sha so that :latest cask decides to update itself version_latest.download_sha_path.write("fake download sha") @@ -307,6 +369,12 @@ describe Cask::Upgrade, :cask do expect(local_transmission.versions).to include("2.60") expect(local_transmission.versions).not_to include("2.61") + expect(renamed_app).to be_installed + expect(renamed_app_old_path).to be_a_directory + expect(renamed_app_new_path).not_to be_a_directory + expect(renamed_app.versions).to include("1.0.0") + expect(renamed_app.versions).not_to include("2.0.0") + expect(version_latest).to be_installed expect(version_latest.outdated_download_sha?).to be(true) end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/renamed-app.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/renamed-app.rb new file mode 100644 index 0000000000..e93b90cb8e --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/outdated/renamed-app.rb @@ -0,0 +1,9 @@ +cask "renamed-app" do + version "1.0.0" + sha256 "cf001ed6c81820e049dc7a353957dab8936b91f1956ee74ff0b3eb59791f1ad9" + + url "file://#{TEST_FIXTURE_DIR}/cask/old-app.tar.gz" + homepage "https://brew.sh/" + + app "OldApp.app" +end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/renamed-app.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/renamed-app.rb new file mode 100644 index 0000000000..3e1f367fa5 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/renamed-app.rb @@ -0,0 +1,9 @@ +cask "renamed-app" do + version "2.0.0" + sha256 "9f88a6f3d8a7977cd3c116c56ee7a20a3c69e838a1d4946f815a926a57883299" + + url "file://#{TEST_FIXTURE_DIR}/cask/new-app.tar.gz" + homepage "https://brew.sh/" + + app "NewApp.app" +end diff --git a/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/Info.plist b/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/Info.plist new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/MacOS/NewApp b/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/MacOS/NewApp new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/PkgInfo b/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/PkgInfo new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/Resources/Caffeine.icns b/Library/Homebrew/test/support/fixtures/cask/NewApp.app/Contents/Resources/Caffeine.icns new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Library/Homebrew/test/support/fixtures/cask/new-app.tar.gz b/Library/Homebrew/test/support/fixtures/cask/new-app.tar.gz new file mode 100644 index 0000000000..a07fc7c8fe Binary files /dev/null and b/Library/Homebrew/test/support/fixtures/cask/new-app.tar.gz differ diff --git a/Library/Homebrew/test/support/fixtures/cask/old-app.tar.gz b/Library/Homebrew/test/support/fixtures/cask/old-app.tar.gz new file mode 100644 index 0000000000..d02fd7e111 Binary files /dev/null and b/Library/Homebrew/test/support/fixtures/cask/old-app.tar.gz differ