diff --git a/Library/Homebrew/cask/lib/hbc/artifact/moved.rb b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb index 43061178c1..03d5768079 100644 --- a/Library/Homebrew/cask/lib/hbc/artifact/moved.rb +++ b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb @@ -65,11 +65,8 @@ module Hbc ohai "Backing #{self.class.english_name} '#{target.basename}' up to '#{source}'." source.dirname.mkpath - if target.parent.writable? - FileUtils.cp_r(target, source) - else - command.run!("/bin/cp", args: ["-r", target, source], sudo: true) - end + # We need to preserve extended attributes between copies. + command.run!("/bin/cp", args: ["-pR", target, source], sudo: !target.parent.writable?) delete(target, force: force, command: command, **options) end diff --git a/Library/Homebrew/cask/lib/hbc/auditor.rb b/Library/Homebrew/cask/lib/hbc/auditor.rb index b5dd2dd82e..7eb8715008 100644 --- a/Library/Homebrew/cask/lib/hbc/auditor.rb +++ b/Library/Homebrew/cask/lib/hbc/auditor.rb @@ -2,15 +2,16 @@ require "hbc/download" module Hbc class Auditor - def self.audit(cask, audit_download: false, check_token_conflicts: false, commit_range: nil) - new(cask, audit_download, check_token_conflicts, commit_range).audit + def self.audit(cask, audit_download: false, check_token_conflicts: false, quarantine: true, commit_range: nil) + new(cask, audit_download, check_token_conflicts, quarantine, commit_range).audit end attr_reader :cask, :commit_range - def initialize(cask, audit_download, check_token_conflicts, commit_range) + def initialize(cask, audit_download, check_token_conflicts, quarantine, commit_range) @cask = cask @audit_download = audit_download + @quarantine = quarantine @commit_range = commit_range @check_token_conflicts = check_token_conflicts end @@ -19,6 +20,10 @@ module Hbc @audit_download end + def quarantine? + @quarantine + end + def check_token_conflicts? @check_token_conflicts end @@ -52,7 +57,7 @@ module Hbc end def audit_cask_instance(cask) - download = audit_download? && Download.new(cask) + download = audit_download? && Download.new(cask, quarantine: quarantine?) audit = Audit.new(cask, download: download, check_token_conflicts: check_token_conflicts?, commit_range: commit_range) diff --git a/Library/Homebrew/cask/lib/hbc/cli/abstract_command.rb b/Library/Homebrew/cask/lib/hbc/cli/abstract_command.rb index 59268715fa..59e7de6f4e 100644 --- a/Library/Homebrew/cask/lib/hbc/cli/abstract_command.rb +++ b/Library/Homebrew/cask/lib/hbc/cli/abstract_command.rb @@ -7,11 +7,12 @@ module Hbc include Options include Homebrew::Search - option "--[no-]binaries", :binaries, true - option "--debug", :debug, false - option "--verbose", :verbose, false - option "--outdated", :outdated_only, false - option "--require-sha", :require_sha, false + option "--[no-]binaries", :binaries, true + option "--debug", :debug, false + option "--verbose", :verbose, false + option "--outdated", :outdated_only, false + option "--require-sha", :require_sha, false + option "--[no-]quarantine", :quarantine, true def self.command_name @command_name ||= name.sub(/^.*:/, "").gsub(/(.)([A-Z])/, '\1_\2').downcase diff --git a/Library/Homebrew/cask/lib/hbc/cli/audit.rb b/Library/Homebrew/cask/lib/hbc/cli/audit.rb index c1bb5bcd8c..4f215826d2 100644 --- a/Library/Homebrew/cask/lib/hbc/cli/audit.rb +++ b/Library/Homebrew/cask/lib/hbc/cli/audit.rb @@ -18,7 +18,9 @@ module Hbc def audit(cask) odebug "Auditing Cask #{cask}" - Auditor.audit(cask, audit_download: download?, check_token_conflicts: token_conflicts?) + Auditor.audit(cask, audit_download: download?, + check_token_conflicts: token_conflicts?, + quarantine: quarantine?) end end end diff --git a/Library/Homebrew/cask/lib/hbc/cli/fetch.rb b/Library/Homebrew/cask/lib/hbc/cli/fetch.rb index 73a7f067b3..927a3d7139 100644 --- a/Library/Homebrew/cask/lib/hbc/cli/fetch.rb +++ b/Library/Homebrew/cask/lib/hbc/cli/fetch.rb @@ -14,7 +14,7 @@ module Hbc casks.each do |cask| Installer.print_caveats(cask) ohai "Downloading external files for Cask #{cask}" - downloaded_path = Download.new(cask, force: force?).perform + downloaded_path = Download.new(cask, force: force?, quarantine: quarantine?).perform Verify.all(cask, downloaded_path) ohai "Success! Downloaded to -> #{downloaded_path}" end diff --git a/Library/Homebrew/cask/lib/hbc/cli/install.rb b/Library/Homebrew/cask/lib/hbc/cli/install.rb index 31d90f60ef..f8a8929367 100644 --- a/Library/Homebrew/cask/lib/hbc/cli/install.rb +++ b/Library/Homebrew/cask/lib/hbc/cli/install.rb @@ -17,7 +17,8 @@ module Hbc verbose: verbose?, force: force?, skip_cask_deps: skip_cask_deps?, - require_sha: require_sha?).install + require_sha: require_sha?, + quarantine: quarantine?).install rescue CaskAlreadyInstalledError => e opoo e.message end diff --git a/Library/Homebrew/cask/lib/hbc/cli/reinstall.rb b/Library/Homebrew/cask/lib/hbc/cli/reinstall.rb index 408be134da..95d419991c 100644 --- a/Library/Homebrew/cask/lib/hbc/cli/reinstall.rb +++ b/Library/Homebrew/cask/lib/hbc/cli/reinstall.rb @@ -7,7 +7,8 @@ module Hbc verbose: verbose?, force: force?, skip_cask_deps: skip_cask_deps?, - require_sha: require_sha?).reinstall + require_sha: require_sha?, + quarantine: quarantine?).reinstall end end diff --git a/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb b/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb index fc4f24ac09..01ff0d36fc 100644 --- a/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb +++ b/Library/Homebrew/cask/lib/hbc/cli/upgrade.rb @@ -36,7 +36,11 @@ module Hbc old_cask = CaskLoader.load(old_cask.installed_caskfile) - old_cask_installer = Installer.new(old_cask, binaries: binaries?, verbose: verbose?, force: force?, upgrade: true) + old_cask_installer = + Installer.new(old_cask, binaries: binaries?, + verbose: verbose?, + force: force?, + upgrade: true) new_cask = CaskLoader.load(old_cask.to_s) @@ -46,7 +50,8 @@ module Hbc force: force?, skip_cask_deps: skip_cask_deps?, require_sha: require_sha?, - upgrade: true) + upgrade: true, + quarantine: quarantine?) started_upgrade = false new_artifacts_installed = false diff --git a/Library/Homebrew/cask/lib/hbc/download.rb b/Library/Homebrew/cask/lib/hbc/download.rb index 1eb7eb8250..d16e0ebe5d 100644 --- a/Library/Homebrew/cask/lib/hbc/download.rb +++ b/Library/Homebrew/cask/lib/hbc/download.rb @@ -1,18 +1,21 @@ require "fileutils" +require "hbc/quarantine" require "hbc/verify" module Hbc class Download attr_reader :cask - def initialize(cask, force: false) + def initialize(cask, force: false, quarantine: true) @cask = cask @force = force + @quarantine = quarantine end def perform clear_cache fetch + quarantine downloaded_path end @@ -38,5 +41,13 @@ module Hbc rescue StandardError => e raise CaskError, "Download failed on Cask '#{cask}' with message: #{e}" end + + def quarantine + return unless @quarantine + return unless Quarantine.available? + return if Quarantine.detect(@downloaded_path) + + Quarantine.cask(cask: @cask, download_path: @downloaded_path) + end end end diff --git a/Library/Homebrew/cask/lib/hbc/exceptions.rb b/Library/Homebrew/cask/lib/hbc/exceptions.rb index f5e6649e0e..7fc171586d 100644 --- a/Library/Homebrew/cask/lib/hbc/exceptions.rb +++ b/Library/Homebrew/cask/lib/hbc/exceptions.rb @@ -32,13 +32,13 @@ module Hbc class CaskUnavailableError < AbstractCaskErrorWithToken def to_s - "Cask '#{token}' is unavailable" << (reason.empty? ? "." : ": #{reason}") + "Cask '#{token}' is unavailable#{reason.empty? ? "." : ": #{reason}"}" end end class CaskUnreadableError < CaskUnavailableError def to_s - "Cask '#{token}' is unreadable" << (reason.empty? ? "." : ": #{reason}") + "Cask '#{token}' is unreadable#{reason.empty? ? "." : ": #{reason}"}" end end @@ -73,7 +73,7 @@ module Hbc class CaskCyclicDependencyError < AbstractCaskErrorWithToken def to_s - "Cask '#{token}' includes cyclic dependencies on other Casks" << (reason.empty? ? "." : ": #{reason}") + "Cask '#{token}' includes cyclic dependencies on other Casks#{reason.empty? ? "." : ": #{reason}"}" end end @@ -91,7 +91,7 @@ module Hbc class CaskInvalidError < AbstractCaskErrorWithToken def to_s - "Cask '#{token}' definition is invalid" << (reason.empty? ? "." : ": #{reason}") + "Cask '#{token}' definition is invalid#{reason.empty? ? "." : ": #{reason}"}" end end @@ -149,4 +149,39 @@ module Hbc EOS end end + + class CaskQuarantineError < CaskError + attr_reader :path, :reason + + def initialize(path, reason) + @path = path + @reason = reason + end + + def to_s + s = "Failed to quarantine #{path}." + + unless reason.empty? + s << " Here's the reason:\n" + s << Formatter.error(reason) + s << "\n" unless reason.end_with?("\n") + end + + s + end + end + + class CaskQuarantinePropagationError < CaskQuarantineError + def to_s + s = "Failed to quarantine one or more files within #{path}." + + unless reason.empty? + s << " Here's the reason:\n" + s << Formatter.error(reason) + s << "\n" unless reason.end_with?("\n") + end + + s + end + end end diff --git a/Library/Homebrew/cask/lib/hbc/installer.rb b/Library/Homebrew/cask/lib/hbc/installer.rb index a0fe425141..51259ce7b0 100644 --- a/Library/Homebrew/cask/lib/hbc/installer.rb +++ b/Library/Homebrew/cask/lib/hbc/installer.rb @@ -7,6 +7,7 @@ require "hbc/cask_dependencies" require "hbc/download" require "hbc/staged" require "hbc/verify" +require "hbc/quarantine" require "cgi" @@ -23,7 +24,10 @@ module Hbc PERSISTENT_METADATA_SUBDIRS = ["gpg"].freeze - def initialize(cask, command: SystemCommand, force: false, skip_cask_deps: false, binaries: true, verbose: false, require_sha: false, upgrade: false, installed_as_dependency: false) + def initialize(cask, command: SystemCommand, force: false, + skip_cask_deps: false, binaries: true, verbose: false, + require_sha: false, upgrade: false, + installed_as_dependency: false, quarantine: true) @cask = cask @command = command @force = force @@ -34,9 +38,12 @@ module Hbc @reinstall = false @upgrade = upgrade @installed_as_dependency = installed_as_dependency + @quarantine = quarantine end - attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?, :upgrade?, :verbose?, :installed_as_dependency? + attr_predicate :binaries?, :force?, :skip_cask_deps?, :require_sha?, + :upgrade?, :verbose?, :installed_as_dependency?, + :quarantine? def self.print_caveats(cask) odebug "Printing caveats" @@ -86,6 +93,7 @@ module Hbc uninstall_existing_cask if @reinstall oh1 "Installing Cask #{Formatter.identifier(@cask)}" + opoo "macOS's Gatekeeper has been disabled for this Cask" unless quarantine? stage install_artifacts enable_accessibility_access @@ -137,7 +145,7 @@ module Hbc def download odebug "Downloading" - @downloaded_path = Download.new(@cask, force: false).perform + @downloaded_path = Download.new(@cask, force: false, quarantine: quarantine?).perform odebug "Downloaded to -> #{@downloaded_path}" @downloaded_path end @@ -176,6 +184,11 @@ module Hbc else primary_container.extract_nestedly(to: @cask.staged_path, basename: basename, verbose: verbose?) end + + return unless quarantine? + return unless Quarantine.available? + + Quarantine.propagate(from: @downloaded_path, to: @cask.staged_path, command: @command) end def install_artifacts diff --git a/Library/Homebrew/cask/lib/hbc/quarantine.rb b/Library/Homebrew/cask/lib/hbc/quarantine.rb new file mode 100644 index 0000000000..4212fd231b --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/quarantine.rb @@ -0,0 +1,80 @@ +require "development_tools" +module Hbc + module Quarantine + module_function + + QUARANTINE_ATTRIBUTE = "com.apple.quarantine".freeze + + QUARANTINE_SCRIPT = (HOMEBREW_LIBRARY_PATH/"cask/lib/hbc/utils/quarantine.swift").freeze + + # @private + def swift + @swift ||= DevelopmentTools.locate("swift") + end + + def available? + status = !swift.nil? + odebug "Quarantine is #{status ? "available" : "not available"}." + status + end + + def detect(file) + return if file.nil? + + odebug "Verifying Gatekeeper status of #{file}" + + quarantine_status = !status(file).empty? + + odebug "#{file} is #{quarantine_status ? "quarantined" : "not quarantined"}" + + quarantine_status + end + + def status(file, command: SystemCommand) + command.run("/usr/bin/xattr", + args: ["-p", QUARANTINE_ATTRIBUTE, file], + print_stderr: false).stdout.rstrip + end + + def cask(cask: nil, download_path: nil, command: SystemCommand) + return if cask.nil? || download_path.nil? + + odebug "Quarantining #{download_path}" + + quarantiner = command.run(swift, + args: [ + QUARANTINE_SCRIPT, + download_path, + cask.url.to_s, + cask.homepage.to_s, + ]) + + return if quarantiner.success? + + case quarantiner.exit_status + when 2 + raise CaskQuarantineError.new(download_path, "Insufficient parameters") + else + raise CaskQuarantineError.new(download_path, quarantiner.stderr) + end + end + + def propagate(from: nil, to: nil, command: SystemCommand) + return if from.nil? || to.nil? + + raise CaskError, "#{from} was not quarantined properly." unless detect(from) + + odebug "Propagating quarantine from #{from} to #{to}" + + quarantine_status = status(from, command: command) + + quarantiner = command.run("/usr/bin/xattr", + args: ["-w", "-r", QUARANTINE_ATTRIBUTE, quarantine_status, to], + print_stderr: false) + + return if quarantiner.success? + + raise CaskQuarantinePropagationError.new(to, quarantiner.stderr) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/utils/quarantine.swift b/Library/Homebrew/cask/lib/hbc/utils/quarantine.swift new file mode 100644 index 0000000000..af89e2ce15 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/utils/quarantine.swift @@ -0,0 +1,42 @@ +#!/usr/bin/swift + +import Foundation + +struct swifterr: TextOutputStream { + public static var stream = swifterr() + mutating func write(_ string: String) { fputs(string, stderr) } +} + +if (CommandLine.arguments.count < 4) { + exit(2) +} + +let dataLocationUrl: NSURL = NSURL.init(fileURLWithPath: CommandLine.arguments[1]) + +var errorBag: NSError? + +let quarantineProperties: [String: Any] = [ + kLSQuarantineAgentNameKey as String: "Homebrew-Cask", + kLSQuarantineTypeKey as String: kLSQuarantineTypeWebDownload, + kLSQuarantineDataURLKey as String: CommandLine.arguments[2], + kLSQuarantineOriginURLKey as String: CommandLine.arguments[3] +] + +if (dataLocationUrl.checkResourceIsReachableAndReturnError(&errorBag)) { + do { + try dataLocationUrl.setResourceValue( + quarantineProperties as NSDictionary, + forKey: URLResourceKey.quarantinePropertiesKey + ) + } + catch { + print(error.localizedDescription, to: &swifterr.stream) + exit(1) + } +} +else { + print(errorBag!.localizedDescription, to: &swifterr.stream) + exit(3) +} + +exit(0) diff --git a/Library/Homebrew/manpages/brew-cask.1.md b/Library/Homebrew/manpages/brew-cask.1.md index cb579cfb95..914f686a77 100644 --- a/Library/Homebrew/manpages/brew-cask.1.md +++ b/Library/Homebrew/manpages/brew-cask.1.md @@ -19,7 +19,7 @@ names, and other aspects of this manual are still subject to change. ## FREQUENTLY USED COMMANDS - * `install` [--force] [--skip-cask-deps] [--require-sha] [--language=[, ... ]] [ ... ]: + * `install` [--force] [--skip-cask-deps] [--require-sha] [--no-quarantine] [--language=[, ... ]] [ ... ]: Install Cask identified by . * `uninstall` [--force] [ ... ]: @@ -56,10 +56,11 @@ names, and other aspects of this manual are still subject to change. * `edit` : Open the given Cask definition file for editing. - * `fetch` [--force] [ ... ]: + * `fetch` [--force] [--no-quarantine] [ ... ]: Download remote application files for the given Cask to the local cache. With `--force`, force re-download even if the files are already - cached. + cached. `--no-quarantine` will prevent Gatekeeper from + enforcing its security restrictions on the Cask. * `home` or `homepage` [ ... ]: Display the homepage associated with a given Cask in a browser. @@ -69,11 +70,12 @@ names, and other aspects of this manual are still subject to change. * `info` or `abv` [ ... ]: Display information about the given Cask. - * `install` [--force] [--skip-cask-deps] [--require-sha] [ ... ]: + * `install` [--force] [--skip-cask-deps] [--require-sha] [--no-quarantine] [ ... ]: Install the given Cask. With `--force`, re-install even if the Cask appears to be already present. With `--skip-cask-deps`, skip any Cask dependencies. `--require-sha` will abort installation if the Cask does not - have a checksum defined. + have a checksum defined. `--no-quarantine` will prevent Gatekeeper from + enforcing its security restrictions on the Cask. is usually the ID of a Cask, but see [OTHER WAYS TO SPECIFY A CASK][] for variations. @@ -97,7 +99,7 @@ names, and other aspects of this manual are still subject to change. `--verbose` forces the display of the outdated and latest version.
`--quiet` suppresses the display of versions. - * `reinstall` [ ... ]: + * `reinstall` [--no-quarantine] [ ... ]: Reinstall the given Cask. * `search` or `-S` [ | //]: @@ -162,6 +164,10 @@ in a future version. * `--require-sha`: Abort Cask installation if the Cask does not have a checksum defined. + * `--no-quarantine`: + Prevent Gatekeeper from enforcing its security restrictions on the Cask. + This will let you run it straightaway. + * `--verbose`: Give additional feedback during installation. diff --git a/Library/Homebrew/test/cask/cli/audit_spec.rb b/Library/Homebrew/test/cask/cli/audit_spec.rb index 2ed4ea9f11..4619e38a67 100644 --- a/Library/Homebrew/test/cask/cli/audit_spec.rb +++ b/Library/Homebrew/test/cask/cli/audit_spec.rb @@ -19,7 +19,7 @@ describe Hbc::CLI::Audit, :cask do expect(Hbc::CaskLoader).to receive(:load).with(cask_token).and_return(cask) expect(Hbc::Auditor).to receive(:audit) - .with(cask, audit_download: false, check_token_conflicts: false) + .with(cask, audit_download: false, check_token_conflicts: false, quarantine: true) .and_return(true) described_class.run(cask_token) @@ -30,7 +30,7 @@ describe Hbc::CLI::Audit, :cask do it "does not download the Cask per default" do allow(Hbc::CaskLoader).to receive(:load).and_return(cask) expect(Hbc::Auditor).to receive(:audit) - .with(cask, audit_download: false, check_token_conflicts: false) + .with(cask, audit_download: false, check_token_conflicts: false, quarantine: true) .and_return(true) described_class.run("casktoken") @@ -39,7 +39,7 @@ describe Hbc::CLI::Audit, :cask do it "download a Cask if --download flag is set" do allow(Hbc::CaskLoader).to receive(:load).and_return(cask) expect(Hbc::Auditor).to receive(:audit) - .with(cask, audit_download: true, check_token_conflicts: false) + .with(cask, audit_download: true, check_token_conflicts: false, quarantine: true) .and_return(true) described_class.run("casktoken", "--download") @@ -50,7 +50,7 @@ describe Hbc::CLI::Audit, :cask do it "does not check for token conflicts per default" do allow(Hbc::CaskLoader).to receive(:load).and_return(cask) expect(Hbc::Auditor).to receive(:audit) - .with(cask, audit_download: false, check_token_conflicts: false) + .with(cask, audit_download: false, check_token_conflicts: false, quarantine: true) .and_return(true) described_class.run("casktoken") @@ -59,7 +59,7 @@ describe Hbc::CLI::Audit, :cask do it "checks for token conflicts if --token-conflicts flag is set" do allow(Hbc::CaskLoader).to receive(:load).and_return(cask) expect(Hbc::Auditor).to receive(:audit) - .with(cask, audit_download: false, check_token_conflicts: true) + .with(cask, audit_download: false, check_token_conflicts: true, quarantine: true) .and_return(true) described_class.run("casktoken", "--token-conflicts") diff --git a/Library/Homebrew/test/cask/cli/info_spec.rb b/Library/Homebrew/test/cask/cli/info_spec.rb index 3a8a5cd15f..0fb5efe804 100644 --- a/Library/Homebrew/test/cask/cli/info_spec.rb +++ b/Library/Homebrew/test/cask/cli/info_spec.rb @@ -51,7 +51,7 @@ describe Hbc::CLI::Info, :cask do Not installed From: https://github.com/Homebrew/homebrew-cask/blob/master/Casks/local-transmission.rb ==> Name - None + Transmission ==> Artifacts Transmission.app (App) EOS diff --git a/Library/Homebrew/test/cask/cli/quarantine_spec.rb b/Library/Homebrew/test/cask/cli/quarantine_spec.rb new file mode 100644 index 0000000000..21c1754221 --- /dev/null +++ b/Library/Homebrew/test/cask/cli/quarantine_spec.rb @@ -0,0 +1,213 @@ +describe Hbc::Quarantine, :cask do + matcher :be_quarantined do + match do |path| + expect( + described_class.detect(path), + ).to be true + end + end + + describe "by default" do + it "quarantines a nice fresh Cask" do + Hbc::CLI::Install.run("local-transmission") + + expect( + Hbc::CaskLoader.load(cask_path("local-transmission")), + ).to be_installed + + expect( + Hbc::Config.global.appdir.join("Transmission.app"), + ).to be_quarantined + end + + it "quarantines Cask fetches" do + Hbc::CLI::Fetch.run("local-transmission") + local_transmission = Hbc::CaskLoader.load(cask_path("local-transmission")) + cached_location = Hbc::Download.new(local_transmission, force: false, quarantine: false).perform + + expect(cached_location).to be_quarantined + end + + it "quarantines Cask audits" do + Hbc::CLI::Audit.run("local-transmission", "--download") + + local_transmission = Hbc::CaskLoader.load(cask_path("local-transmission")) + cached_location = Hbc::Download.new(local_transmission, force: false, quarantine: false).perform + + expect(cached_location).to be_quarantined + end + + it "quarantines dmg-based Casks" do + Hbc::CLI::Install.run("container-dmg") + + expect( + Hbc::CaskLoader.load(cask_path("container-dmg")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to be_quarantined + end + + it "quarantines tar-gz-based Casks" do + Hbc::CLI::Install.run("container-tar-gz") + + expect( + Hbc::CaskLoader.load(cask_path("container-tar-gz")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to be_quarantined + end + + it "quarantines xar-based Casks" do + Hbc::CLI::Install.run("container-xar") + + expect( + Hbc::CaskLoader.load(cask_path("container-xar")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to be_quarantined + end + + it "quarantines pure bzip2-based Casks" do + Hbc::CLI::Install.run("container-bzip2") + + expect( + Hbc::CaskLoader.load(cask_path("container-bzip2")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to be_quarantined + end + + it "quarantines pure gzip-based Casks" do + Hbc::CLI::Install.run("container-gzip") + + expect( + Hbc::CaskLoader.load(cask_path("container-gzip")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to be_quarantined + end + + it "quarantines the pkg in naked-pkg-based Casks" do + Hbc::CLI::Install.run("container-pkg") + + naked_pkg = Hbc::CaskLoader.load(cask_path("container-pkg")) + + expect(naked_pkg).to be_installed + + expect( + Hbc::Caskroom.path.join("container-pkg", naked_pkg.version, "container.pkg"), + ).to be_quarantined + end + + it "quarantines a nested container" do + Hbc::CLI::Install.run("nested-app") + + expect( + Hbc::CaskLoader.load(cask_path("nested-app")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("MyNestedApp.app")).to be_quarantined + end + end + + describe "when disabled" do + it "does not quarantine even a nice, fresh Cask" do + Hbc::CLI::Install.run("local-transmission", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("local-transmission")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("Transmission.app")).to_not be_quarantined + end + + it "does not quarantine Cask fetches" do + Hbc::CLI::Fetch.run("local-transmission", "--no-quarantine") + local_transmission = Hbc::CaskLoader.load(cask_path("local-transmission")) + cached_location = Hbc::Download.new(local_transmission, force: false, quarantine: false).perform + + expect(cached_location).to_not be_quarantined + end + + it "does not quarantine dmg-based Casks" do + Hbc::CLI::Install.run("container-dmg", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("container-dmg")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to_not be_quarantined + end + + it "does not quarantine tar-gz-based Casks" do + Hbc::CLI::Install.run("container-tar-gz", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("container-tar-gz")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to_not be_quarantined + end + + it "does not quarantine xar-based Casks" do + Hbc::CLI::Install.run("container-xar", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("container-xar")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to_not be_quarantined + end + + it "does not quarantine pure bzip2-based Casks" do + Hbc::CLI::Install.run("container-bzip2", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("container-bzip2")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to_not be_quarantined + end + + it "does not quarantine pure gzip-based Casks" do + Hbc::CLI::Install.run("container-gzip", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("container-gzip")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("container")).to_not be_quarantined + end + + it "does not quarantine the pkg in naked-pkg-based Casks" do + Hbc::CLI::Install.run("container-pkg", "--no-quarantine") + + naked_pkg = Hbc::CaskLoader.load(cask_path("container-pkg")) + + expect(naked_pkg).to be_installed + + expect( + Hbc::Caskroom.path.join("container-pkg", naked_pkg.version, "container.pkg"), + ).to_not be_quarantined + end + + it "does not quarantine a nested container" do + Hbc::CLI::Install.run("nested-app", "--no-quarantine") + + expect( + Hbc::CaskLoader.load(cask_path("nested-app")), + ).to be_installed + + expect(Hbc::Config.global.appdir.join("MyNestedApp.app")).to_not be_quarantined + end + + it "does not quarantine Cask audits" do + Hbc::CLI::Audit.run("local-transmission", "--download", "--no-quarantine") + + local_transmission = Hbc::CaskLoader.load(cask_path("local-transmission")) + cached_location = Hbc::Download.new(local_transmission, force: false, quarantine: false).perform + + expect(cached_location).to_not be_quarantined + end + end +end diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/local-transmission.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/local-transmission.rb index c3762af8fd..6cc69624d8 100644 --- a/Library/Homebrew/test/support/fixtures/cask/Casks/local-transmission.rb +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/local-transmission.rb @@ -1,4 +1,5 @@ cask 'local-transmission' do + name 'Transmission' version '2.61' sha256 'e44ffa103fbf83f55c8d0b1bea309a43b2880798dae8620b1ee8da5e1095ec68' diff --git a/Library/Homebrew/unpack_strategy/dmg.rb b/Library/Homebrew/unpack_strategy/dmg.rb index 55730bc02b..8348324cb1 100644 --- a/Library/Homebrew/unpack_strategy/dmg.rb +++ b/Library/Homebrew/unpack_strategy/dmg.rb @@ -90,7 +90,7 @@ module UnpackStrategy args: ["--bom", bomfile.path, "--", path, unpack_dir], verbose: verbose - FileUtils.chmod "u+w", Pathname.glob(unpack_dir/"**/*").reject(&:symlink?) + FileUtils.chmod "u+w", Pathname.glob(unpack_dir/"**/*", File::FNM_DOTMATCH).reject(&:symlink?) end end end diff --git a/manpages/brew-cask.1 b/manpages/brew-cask.1 index 8404f0ce57..ed0dca508d 100644 --- a/manpages/brew-cask.1 +++ b/manpages/brew-cask.1 @@ -18,7 +18,7 @@ Homebrew\-Cask works robustly enough that we welcome new users, but the project .SH "FREQUENTLY USED COMMANDS" . .TP -\fBinstall\fR [\-\-force] [\-\-skip\-cask\-deps] [\-\-require\-sha] [\-\-language=\fIiso\-language\fR[,\fIiso\-language\fR \.\.\. ]] \fItoken\fR [ \fItoken\fR \.\.\. ] +\fBinstall\fR [\-\-force] [\-\-skip\-cask\-deps] [\-\-require\-sha] [\-\-no\-quarantine] [\-\-language=\fIiso\-language\fR[,\fIiso\-language\fR \.\.\. ]] \fItoken\fR [ \fItoken\fR \.\.\. ] Install Cask identified by \fItoken\fR\. . .TP @@ -59,8 +59,8 @@ Check for configuration issues\. Can be useful to upload as a gist for developer Open the given Cask definition file for editing\. . .TP -\fBfetch\fR [\-\-force] \fItoken\fR [ \fItoken\fR \.\.\. ] -Download remote application files for the given Cask to the local cache\. With \fB\-\-force\fR, force re\-download even if the files are already cached\. +\fBfetch\fR [\-\-force] [\-\-no\-quarantine] \fItoken\fR [ \fItoken\fR \.\.\. ] +Download remote application files for the given Cask to the local cache\. With \fB\-\-force\fR, force re\-download even if the files are already cached\. \fB\-\-no\-quarantine\fR will prevent Gatekeeper from enforcing its security restrictions on the Cask\. . .TP \fBhome\fR or \fBhomepage\fR [ \fItoken\fR \.\.\. ] @@ -74,8 +74,8 @@ With no arguments, display the project page \fIhttps://caskroom\.github\.io/\fR\ Display information about the given Cask\. . .TP -\fBinstall\fR [\-\-force] [\-\-skip\-cask\-deps] [\-\-require\-sha] \fItoken\fR [ \fItoken\fR \.\.\. ] -Install the given Cask\. With \fB\-\-force\fR, re\-install even if the Cask appears to be already present\. With \fB\-\-skip\-cask\-deps\fR, skip any Cask dependencies\. \fB\-\-require\-sha\fR will abort installation if the Cask does not have a checksum defined\. +\fBinstall\fR [\-\-force] [\-\-skip\-cask\-deps] [\-\-require\-sha] [\-\-no\-quarantine] \fItoken\fR [ \fItoken\fR \.\.\. ] +Install the given Cask\. With \fB\-\-force\fR, re\-install even if the Cask appears to be already present\. With \fB\-\-skip\-cask\-deps\fR, skip any Cask dependencies\. \fB\-\-require\-sha\fR will abort installation if the Cask does not have a checksum defined\. \fB\-\-no\-quarantine\fR will prevent Gatekeeper from enforcing its security restrictions on the Cask\. . .IP \fItoken\fR is usually the ID of a Cask, but see \fIOTHER WAYS TO SPECIFY A CASK\fR for variations\. @@ -98,7 +98,7 @@ Without token arguments, display all the installed Casks that have newer version \fB\-\-quiet\fR suppresses the display of versions\. . .TP -\fBreinstall\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +\fBreinstall\fR [\-\-no\-quarantine] \fItoken\fR [ \fItoken\fR \.\.\. ] Reinstall the given Cask\. . .TP @@ -158,6 +158,10 @@ Skip Cask dependencies when installing\. Abort Cask installation if the Cask does not have a checksum defined\. . .TP +\fB\-\-no\-quarantine\fR +Prevent Gatekeeper from enforcing its security restrictions on the Cask\. This will let you run it straightaway\. +. +.TP \fB\-\-verbose\fR Give additional feedback during installation\. .