Merge remote-tracking branch 'origin/master' into metacollin/master

This commit is contained in:
Mike McQuaid 2020-11-20 12:45:21 +00:00
commit f42cbebd48
No known key found for this signature in database
GPG Key ID: 48A898132FD8EE70
36 changed files with 399 additions and 344 deletions

View File

@ -23,4 +23,3 @@ require "cask/staged"
require "cask/topological_hash"
require "cask/url"
require "cask/utils"
require "cask/verify"

View File

@ -255,20 +255,20 @@ module Cask
add_error "you should use sha256 :no_check when version is :latest"
end
def check_sha256_actually_256(sha256: cask.sha256, stanza: "sha256")
odebug "Verifying #{stanza} string is a legal SHA-256 digest"
return unless sha256.is_a?(String)
return if sha256.length == 64 && sha256[/^[0-9a-f]+$/i]
def check_sha256_actually_256
odebug "Verifying sha256 string is a legal SHA-256 digest"
return unless cask.sha256.is_a?(Checksum)
return if cask.sha256.length == 64 && cask.sha256[/^[0-9a-f]+$/i]
add_error "#{stanza} string must be of 64 hexadecimal characters"
add_error "sha256 string must be of 64 hexadecimal characters"
end
def check_sha256_invalid(sha256: cask.sha256, stanza: "sha256")
odebug "Verifying #{stanza} is not a known invalid value"
def check_sha256_invalid
odebug "Verifying sha256 is not a known invalid value"
empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
return unless sha256 == empty_sha256
return unless cask.sha256 == empty_sha256
add_error "cannot use the sha256 for an empty string in #{stanza}: #{empty_sha256}"
add_error "cannot use the sha256 for an empty string: #{empty_sha256}"
end
def check_latest_with_appcast
@ -428,8 +428,7 @@ module Cask
return unless download && cask.url
odebug "Auditing download"
downloaded_path = download.perform
Verify.all(cask, downloaded_path)
download.fetch
rescue => e
add_error "download not possible: #{e}"
end

View File

@ -32,7 +32,6 @@ module Cask
require "cask/installer"
options = {
force: args.force?,
quarantine: args.quarantine?,
}.compact
@ -41,8 +40,9 @@ module Cask
casks.each do |cask|
puts Installer.caveats(cask)
ohai "Downloading external files for Cask #{cask}"
downloaded_path = Download.new(cask, **options).perform
Verify.all(cask, downloaded_path)
download = Download.new(cask, **options)
download.clear_cache if args.force?
downloaded_path = download.fetch
ohai "Success! Downloaded to -> #{downloaded_path}"
end
end

View File

@ -32,6 +32,15 @@ module Cask
sig { void }
def run
self.class.zap_casks(*casks, verbose: args.verbose?, force: args.force?)
end
sig { params(casks: Cask, force: T.nilable(T::Boolean), verbose: T.nilable(T::Boolean)).void }
def self.zap_casks(
*casks,
force: nil,
verbose: nil
)
require "cask/installer"
casks.each do |cask|
@ -43,10 +52,10 @@ module Cask
cask = CaskLoader.load(installed_caskfile)
end
else
raise CaskNotInstalledError, cask unless args.force?
raise CaskNotInstalledError, cask unless force
end
Installer.new(cask, verbose: args.verbose?, force: args.force?).zap
Installer.new(cask, verbose: verbose, force: force).zap
end
end
end

View File

@ -4,25 +4,32 @@
require "fileutils"
require "cask/cache"
require "cask/quarantine"
require "cask/verify"
module Cask
# A download corresponding to a {Cask}.
#
# @api private
class Download
include Context
attr_reader :cask
def initialize(cask, force: false, quarantine: nil)
def initialize(cask, quarantine: nil)
@cask = cask
@force = force
@quarantine = quarantine
end
def perform
clear_cache
fetch
quarantine
def fetch(verify_download_integrity: true)
downloaded_path = begin
downloader.fetch
downloader.cached_location
rescue => e
error = CaskError.new("Download failed on Cask '#{cask}' with message: #{e}")
error.set_backtrace e.backtrace
raise error
end
quarantine(downloaded_path)
self.verify_download_integrity(downloaded_path) if verify_download_integrity
downloaded_path
end
@ -33,32 +40,43 @@ module Cask
end
end
def clear_cache
downloader.clear_cache
end
def cached_download
downloader.cached_location
end
def verify_download_integrity(fn)
if @cask.sha256 == :no_check
opoo "No checksum defined for cask '#{@cask}', skipping verification."
return
end
begin
ohai "Verifying checksum for cask '#{@cask}'." if verbose?
fn.verify_checksum(@cask.sha256)
rescue ChecksumMissingError
opoo <<~EOS
Cannot verify integrity of '#{fn.basename}'.
No checksum was provided for this cask.
For your reference, the checksum is:
sha256 "#{fn.sha256}"
EOS
end
end
private
attr_reader :force
attr_accessor :downloaded_path
def clear_cache
downloader.clear_cache if force
end
def fetch
downloader.fetch
@downloaded_path = downloader.cached_location
rescue => e
error = CaskError.new("Download failed on Cask '#{cask}' with message: #{e}")
error.set_backtrace e.backtrace
raise error
end
def quarantine
def quarantine(path)
return if @quarantine.nil?
return unless Quarantine.available?
if @quarantine
Quarantine.cask!(cask: @cask, download_path: @downloaded_path)
Quarantine.cask!(cask: @cask, download_path: path)
else
Quarantine.release!(download_path: @downloaded_path)
Quarantine.release!(download_path: path)
end
end
end

View File

@ -205,11 +205,14 @@ module Cask
def sha256(arg = nil)
set_unique_stanza(:sha256, arg.nil?) do
if !arg.is_a?(String) && arg != :no_check
case arg
when :no_check
arg
when String
Checksum.new(:sha256, arg)
else
raise CaskInvalidError.new(cask, "invalid 'sha256' value: '#{arg.inspect}'")
end
arg
end
end

View File

@ -207,76 +207,6 @@ module Cask
end
end
# Error with a cask's checksum.
#
# @api private
class CaskSha256Error < AbstractCaskErrorWithToken
attr_reader :expected, :actual
def initialize(token, expected = nil, actual = nil)
super(token)
@expected = expected
@actual = actual
end
end
# Error when a cask's checksum is missing.
#
# @api private
class CaskSha256MissingError < CaskSha256Error
extend T::Sig
sig { returns(String) }
def to_s
<<~EOS
Cask '#{token}' requires a checksum:
#{Formatter.identifier("sha256 \"#{actual}\"")}
EOS
end
end
# Error when a cask's checksum does not match.
#
# @api private
class CaskSha256MismatchError < CaskSha256Error
extend T::Sig
attr_reader :path
def initialize(token, expected, actual, path)
super(token, expected, actual)
@path = path
end
sig { returns(String) }
def to_s
<<~EOS
Checksum for Cask '#{token}' does not match.
Expected: #{Formatter.success(expected.to_s)}
Actual: #{Formatter.error(actual.to_s)}
File: #{path}
To retry an incomplete download, remove the file above.
If the issue persists, visit:
#{Formatter.url("https://github.com/Homebrew/homebrew-cask/blob/HEAD/doc/reporting_bugs/checksum_does_not_match_error.md")}
EOS
end
end
# Error when a cask has no checksum and the `--require-sha` flag is passed.
#
# @api private
class CaskNoShasumError < CaskSha256Error
extend T::Sig
sig { returns(String) }
def to_s
<<~EOS
Cask '#{token}' does not have a sha256 checksum defined and was not installed.
This means you have the #{Formatter.identifier("--require-sha")} option set, perhaps in your HOMEBREW_CASK_OPTS.
EOS
end
end
# Error during quarantining of a file.
#
# @api private

View File

@ -8,7 +8,6 @@ require "cask/topological_hash"
require "cask/config"
require "cask/download"
require "cask/staged"
require "cask/verify"
require "cask/quarantine"
require "cgi"
@ -68,7 +67,6 @@ module Cask
satisfy_dependencies
download
verify
end
def stage
@ -156,7 +154,7 @@ module Cask
return @downloaded_path if @downloaded_path
odebug "Downloading"
@downloaded_path = Download.new(@cask, force: false, quarantine: quarantine?).perform
@downloaded_path = Download.new(@cask, quarantine: quarantine?).fetch
odebug "Downloaded to -> #{@downloaded_path}"
@downloaded_path
end
@ -165,11 +163,10 @@ module Cask
odebug "Checking cask has checksum"
return unless @cask.sha256 == :no_check
raise CaskNoShasumError, @cask.token
end
def verify
Verify.all(@cask, @downloaded_path)
raise CaskError, <<~EOS
Cask '#{@cask}' does not have a sha256 checksum defined and was not installed.
This means you have the #{Formatter.identifier("--require-sha")} option set, perhaps in your HOMEBREW_CASK_OPTS.
EOS
end
def primary_container

View File

@ -1,30 +0,0 @@
# typed: false
# frozen_string_literal: true
module Cask
# Helper module for verifying a cask's checksum.
#
# @api private
module Verify
module_function
def all(cask, downloaded_path)
if cask.sha256 == :no_check
ohai "No SHA-256 checksum defined for Cask '#{cask}', skipping verification."
return
end
ohai "Verifying SHA-256 checksum for Cask '#{cask}'."
expected = cask.sha256
computed = downloaded_path.sha256
raise CaskSha256MissingError.new(cask.token, expected, computed) if expected.nil? || expected.empty?
return if expected == computed
ohai "Note: Running `brew update` may fix SHA-256 checksum errors."
raise CaskSha256MismatchError.new(cask.token, expected, computed, downloaded_path)
end
end
end

View File

@ -13,12 +13,19 @@ class Checksum
def initialize(hash_type, hexdigest)
@hash_type = hash_type
@hexdigest = hexdigest
@hexdigest = hexdigest.downcase
end
delegate [:empty?, :to_s] => :@hexdigest
delegate [:empty?, :to_s, :length, :[]] => :@hexdigest
def ==(other)
hash_type == other&.hash_type && hexdigest == other.hexdigest
case other
when String
to_s == other.downcase
when Checksum
hash_type == other.hash_type && hexdigest == other.hexdigest
else
false
end
end
end

View File

@ -113,7 +113,7 @@ module Homebrew
def build_from_source_formulae
if build_from_source? || build_bottle?
named.to_formulae.map(&:full_name)
named.to_formulae_and_casks.select { |f| f.is_a?(Formula) }.map(&:full_name)
else
[]
end

View File

@ -35,10 +35,10 @@ module Homebrew
@to_formulae ||= to_formulae_and_casks(only: :formula).freeze
end
def to_formulae_and_casks(only: nil, method: nil)
def to_formulae_and_casks(only: nil, ignore_unavailable: nil, method: nil)
@to_formulae_and_casks ||= {}
@to_formulae_and_casks[only] ||= begin
to_objects(only: only, method: method).reject { |o| o.is_a?(Tap) }.freeze
to_objects(only: only, ignore_unavailable: ignore_unavailable, method: method).freeze
end
end
@ -49,10 +49,10 @@ module Homebrew
.map(&:freeze).freeze
end
def to_formulae_and_casks_and_unavailable(method: nil)
def to_formulae_and_casks_and_unavailable(only: nil, method: nil)
@to_formulae_casks_unknowns ||= {}
@to_formulae_casks_unknowns[method] = downcased_unique_named.map do |name|
load_formula_or_cask(name, method: method)
load_formula_or_cask(name, only: only, method: method)
rescue FormulaOrCaskUnavailableError => e
e
end.uniq.freeze
@ -68,6 +68,9 @@ module Homebrew
resolve_formula(name)
when :keg
resolve_keg(name)
when :kegs
rack = Formulary.to_rack(name)
rack.directory? ? rack.subdirs.map { |d| Keg.new(d) } : []
else
raise
end
@ -108,10 +111,12 @@ module Homebrew
# Convert named arguments to {Formula} or {Cask} objects.
# If both a formula and cask exist with the same name, returns the
# formula and prints a warning unless `only` is specified.
def to_objects(only: nil, method: nil)
def to_objects(only: nil, ignore_unavailable: nil, method: nil)
@to_objects ||= {}
@to_objects[only] ||= downcased_unique_named.map do |name|
@to_objects[only] ||= downcased_unique_named.flat_map do |name|
load_formula_or_cask(name, only: only, method: method)
rescue NoSuchKegError, FormulaUnavailableError, Cask::CaskUnavailableError
ignore_unavailable ? [] : raise
end.uniq.freeze
end
private :to_objects
@ -142,7 +147,7 @@ module Homebrew
paths << formula_path if formula_path.exist?
paths << cask_path if cask_path.exist?
paths.empty? ? name : paths
paths.empty? ? Pathname(name) : paths
end
end.uniq.freeze
end
@ -159,11 +164,17 @@ module Homebrew
end
end
sig { params(only: T.nilable(Symbol)).returns([T::Array[Keg], T::Array[Cask::Cask]]) }
def to_kegs_to_casks(only: nil)
@to_kegs_to_casks ||= to_formulae_and_casks(only: only, method: :keg)
.partition { |o| o.is_a?(Keg) }
.map(&:freeze).freeze
sig do
params(only: T.nilable(Symbol), ignore_unavailable: T.nilable(T::Boolean), all_kegs: T.nilable(T::Boolean))
.returns([T::Array[Keg], T::Array[Cask::Cask]])
end
def to_kegs_to_casks(only: nil, ignore_unavailable: nil, all_kegs: nil)
method = all_kegs ? :kegs : :keg
@to_kegs_to_casks ||= {}
@to_kegs_to_casks[method] ||=
to_formulae_and_casks(only: only, ignore_unavailable: ignore_unavailable, method: method)
.partition { |o| o.is_a?(Keg) }
.map(&:freeze).freeze
end
sig { returns(T::Array[String]) }

View File

@ -4,6 +4,7 @@
require "formula"
require "fetch"
require "cli/parser"
require "cask/download"
module Homebrew
extend T::Sig
@ -18,8 +19,8 @@ module Homebrew
usage_banner <<~EOS
`fetch` [<options>] <formula>
Download a bottle (if available) or source packages for <formula>.
For tarballs, also print SHA-256 checksums.
Download a bottle (if available) or source packages for <formula>e
and binaries for <cask>s. For files, also print SHA-256 checksums.
EOS
switch "--HEAD",
description: "Fetch HEAD version instead of stable version."
@ -42,58 +43,98 @@ module Homebrew
switch "--force-bottle",
description: "Download a bottle if it exists for the current or newest version of macOS, "\
"even if it would not be used during installation."
switch "--[no-]quarantine",
description: "Disable/enable quarantining of downloads (default: enabled).",
env: :cask_opts_quarantine
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--formula", "--cask"
conflicts "--devel", "--HEAD"
conflicts "--build-from-source", "--build-bottle", "--force-bottle"
min_named :formula
conflicts "--cask", "--HEAD"
conflicts "--cask", "--devel"
conflicts "--cask", "--deps"
conflicts "--cask", "-s"
conflicts "--cask", "--build-bottle"
conflicts "--cask", "--force-bottle"
min_named :formula_or_cask
end
end
def fetch
args = fetch_args.parse
if args.deps?
bucket = []
args.named.to_formulae.each do |f|
bucket << f
bucket.concat f.recursive_dependencies.map(&:to_formula)
end
bucket.uniq!
else
bucket = args.named.to_formulae
end
only = :formula if args.formula? && !args.cask?
only = :cask if args.cask? && !args.formula?
puts "Fetching: #{bucket * ", "}" if bucket.size > 1
bucket.each do |f|
f.print_tap_action verb: "Fetching"
bucket = if args.deps?
args.named.to_formulae_and_casks.flat_map do |formula_or_cask|
case formula_or_cask
when Formula
f = formula_or_cask
fetched_bottle = false
if fetch_bottle?(f, args: args)
begin
fetch_formula(f.bottle, args: args)
rescue Interrupt
raise
rescue => e
raise if Homebrew::EnvConfig.developer?
fetched_bottle = false
onoe e.message
opoo "Bottle fetch failed: fetching the source."
[f, *f.recursive_dependencies.map(&:to_formula)]
else
fetched_bottle = true
formula_or_cask
end
end
else
args.named.to_formulae_and_casks(only: only)
end.uniq
next if fetched_bottle
puts "Fetching: #{bucket * ", "}" if bucket.size > 1
bucket.each do |formula_or_cask|
case formula_or_cask
when Formula
f = formula_or_cask
fetch_formula(f, args: args)
f.print_tap_action verb: "Fetching"
f.resources.each do |r|
fetch_resource(r, args: args)
r.patches.each { |p| fetch_patch(p, args: args) if p.external? }
fetched_bottle = false
if fetch_bottle?(f, args: args)
begin
fetch_formula(f.bottle, args: args)
rescue Interrupt
raise
rescue => e
raise if Homebrew::EnvConfig.developer?
fetched_bottle = false
onoe e.message
opoo "Bottle fetch failed: fetching the source."
else
fetched_bottle = true
end
end
next if fetched_bottle
fetch_formula(f, args: args)
f.resources.each do |r|
fetch_resource(r, args: args)
r.patches.each { |p| fetch_patch(p, args: args) if p.external? }
end
f.patchlist.each { |p| fetch_patch(p, args: args) if p.external? }
else
cask = formula_or_cask
options = {
force: args.force?,
quarantine: args.quarantine?,
}.compact
options[:quarantine] = true if options[:quarantine].nil?
download = Cask::Download.new(cask, **options)
fetch_cask(download, args: args)
end
f.patchlist.each { |p| fetch_patch(p, args: args) if p.external? }
end
end
@ -112,6 +153,13 @@ module Homebrew
opoo "Formula reports different #{e.hash_type}: #{e.expected}"
end
def fetch_cask(cask_download, args:)
fetch_fetchable cask_download, args: args
rescue ChecksumMismatchError => e
retry if retry_fetch?(cask_download, args: args)
opoo "Cask reports different #{e.hash_type}: #{e.expected}"
end
def fetch_patch(p, args:)
fetch_fetchable p, args: args
rescue ChecksumMismatchError => e

View File

@ -25,11 +25,11 @@ module Homebrew
def info_args
Homebrew::CLI::Parser.new do
usage_banner <<~EOS
`info` [<options>] [<formula>]
`info` [<options>] [<formula>|<cask>]
Display brief statistics for your Homebrew installation.
If <formula> is provided, show summary of information about <formula>.
If a <formula> or <cask> is provided, show summary of information about it.
EOS
switch "--analytics",
description: "List global Homebrew analytics data or, if specified, installation and "\
@ -61,13 +61,23 @@ module Homebrew
switch "-v", "--verbose",
description: "Show more verbose analytics data for <formula>."
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--formula", "--cask"
conflicts "--installed", "--all"
end
end
sig { void }
def info
args = info_args.parse
only = :formula if args.formula? && !args.cask?
only = :cask if args.cask? && !args.formula?
if args.analytics?
if args.days.present? && !VALID_DAYS.include?(args.days)
raise UsageError, "--days must be one of #{VALID_DAYS.join(", ")}"
@ -83,20 +93,21 @@ module Homebrew
end
end
print_analytics(args: args)
print_analytics(args: args, only: only)
elsif args.json
print_json(args: args)
print_json(args: args, only: only)
elsif args.github?
raise FormulaOrCaskUnspecifiedError if args.no_named?
exec_browser(*args.named.to_formulae_and_casks.map { |f| github_info(f) })
exec_browser(*args.named.to_formulae_and_casks(only: only).map { |f| github_info(f) })
elsif args.no_named?
print_statistics
else
print_info(args: args)
print_info(args: args, only: only)
end
end
sig { void }
def print_statistics
return unless HOMEBREW_CELLAR.exist?
@ -104,13 +115,14 @@ module Homebrew
puts "#{count} #{"keg".pluralize(count)}, #{HOMEBREW_CELLAR.dup.abv}"
end
def print_analytics(args:)
sig { params(args: CLI::Args, only: T.nilable(Symbol)).void }
def print_analytics(args:, only: nil)
if args.no_named?
Utils::Analytics.output(args: args)
return
end
args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i|
args.named.to_formulae_and_casks_and_unavailable(only: only).each_with_index do |obj, i|
puts unless i.zero?
case obj
@ -126,8 +138,9 @@ module Homebrew
end
end
def print_info(args:)
args.named.to_formulae_and_casks_and_unavailable.each_with_index do |obj, i|
sig { params(args: CLI::Args, only: T.nilable(Symbol)).void }
def print_info(args:, only: nil)
args.named.to_formulae_and_casks_and_unavailable(only: only).each_with_index do |obj, i|
puts unless i.zero?
case obj
@ -159,7 +172,8 @@ module Homebrew
version_hash[version]
end
def print_json(args:)
sig { params(args: CLI::Args, only: T.nilable(Symbol)).void }
def print_json(args:, only: nil)
raise FormulaOrCaskUnspecifiedError if !(args.all? || args.installed?) && args.no_named?
json = case json_version(args.json)
@ -179,7 +193,7 @@ module Homebrew
elsif args.installed?
[Formula.installed.sort, Cask::Caskroom.casks.sort_by(&:full_name)]
else
args.named.to_formulae_to_casks
args.named.to_formulae_to_casks(only: only)
end
{

View File

@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "keg"
@ -19,12 +19,17 @@ module Homebrew
def uninstall_args
Homebrew::CLI::Parser.new do
usage_banner <<~EOS
`uninstall`, `rm`, `remove` [<options>] <formula>
`uninstall`, `rm`, `remove` [<options>] <formula>|<cask>
Uninstall <formula>.
Uninstall a <formula> or <cask>.
EOS
switch "-f", "--force",
description: "Delete all installed versions of <formula>."
description: "Delete all installed versions of <formula>. Uninstall even if <cask> is not " \
"installed, overwrite existing files and ignore errors when removing files."
switch "--zap",
description: "Remove all files associated with a <cask>. " \
"*May remove files which are shared between applications.*"
conflicts "--formula", "--zap"
switch "--ignore-dependencies",
description: "Don't fail uninstall, even if <formula> is a dependency of any installed "\
"formulae."
@ -35,7 +40,7 @@ module Homebrew
description: "Treat all named arguments as casks."
conflicts "--formula", "--cask"
min_named :formula
min_named :formula_or_cask
end
end
@ -45,51 +50,29 @@ module Homebrew
only = :formula if args.formula? && !args.cask?
only = :cask if args.cask? && !args.formula?
if args.force?
casks = []
kegs_by_rack = {}
all_kegs, casks = args.named.to_kegs_to_casks(only: only, ignore_unavailable: args.force?, all_kegs: args.force?)
kegs_by_rack = all_kegs.group_by(&:rack)
args.named.each do |name|
if only != :cask
rack = Formulary.to_rack(name)
kegs_by_rack[rack] = rack.subdirs.map { |d| Keg.new(d) } if rack.directory?
end
next if only == :formula
begin
casks << Cask::CaskLoader.load(name)
rescue Cask::CaskUnavailableError
# Since the uninstall was forced, ignore any unavailable casks.
end
end
else
all_kegs, casks = args.named.to_kegs_to_casks(only: only)
kegs_by_rack = all_kegs.group_by(&:rack)
end
Uninstall.uninstall_kegs(kegs_by_rack,
force: args.force?,
ignore_dependencies: args.ignore_dependencies?,
named_args: args.named)
return if casks.blank?
Cask::Cmd::Uninstall.uninstall_casks(
*casks,
binaries: EnvConfig.cask_opts_binaries?,
verbose: args.verbose?,
force: args.force?,
Uninstall.uninstall_kegs(
kegs_by_rack,
force: args.force?,
ignore_dependencies: args.ignore_dependencies?,
named_args: args.named,
)
rescue MultipleVersionsInstalledError => e
ofail e
ensure
# If we delete Cellar/newname, then Cellar/oldname symlink
# can become broken and we have to remove it.
if HOMEBREW_CELLAR.directory?
HOMEBREW_CELLAR.children.each do |rack|
rack.unlink if rack.symlink? && !rack.resolved_path_exists?
end
if args.zap?
Cask::Cmd::Zap.zap_casks(
*casks,
verbose: args.verbose?,
force: args.force?,
)
else
Cask::Cmd::Uninstall.uninstall_casks(
*casks,
binaries: EnvConfig.cask_opts_binaries?,
verbose: args.verbose?,
force: args.force?,
)
end
end
end

View File

@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true
require "formula"
@ -13,17 +13,27 @@ module Homebrew
def edit_args
Homebrew::CLI::Parser.new do
usage_banner <<~EOS
`edit` [<formula>]
`edit` [<formula>|<cask>]
Open <formula> in the editor set by `EDITOR` or `HOMEBREW_EDITOR`, or open the
Homebrew repository for editing if no formula is provided.
Open a <formula> or <cask> in the editor set by `EDITOR` or `HOMEBREW_EDITOR`,
or open the Homebrew repository for editing if no formula is provided.
EOS
switch "--formula", "--formulae",
description: "Treat all named arguments as formulae."
switch "--cask", "--casks",
description: "Treat all named arguments as casks."
conflicts "--formula", "--cask"
end
end
sig { void }
def edit
args = edit_args.parse
only = :formula if args.formula? && !args.cask?
only = :cask if args.cask? && !args.formula?
unless (HOMEBREW_REPOSITORY/".git").directory?
raise <<~EOS
Changes will be lost!
@ -32,7 +42,7 @@ module Homebrew
EOS
end
paths = args.named.to_formulae_paths.select do |path|
paths = args.named.to_paths(only: only).select do |path|
next path if path.exist?
raise UsageError, "#{path} doesn't exist on disk. " \

View File

@ -617,15 +617,15 @@ class ChecksumMissingError < ArgumentError; end
class ChecksumMismatchError < RuntimeError
attr_reader :expected, :hash_type
def initialize(fn, expected, actual)
def initialize(path, expected, actual)
@expected = expected
@hash_type = expected.hash_type.to_s.upcase
super <<~EOS
#{@hash_type} mismatch
Expected: #{expected}
Actual: #{actual}
Archive: #{fn}
Expected: #{Formatter.success(expected.to_s)}
Actual: #{Formatter.error(actual.to_s)}
File: #{path}
To retry an incomplete download, remove the file above.
EOS
end

View File

@ -27,11 +27,13 @@ class JavaRequirement < Requirement
def java_home_cmd
# TODO: enable for all macOS versions and Linux on next minor release
# but --version is broken on Big Sur today.
if @version && MacOS.version >= :big_sur
odisabled "depends_on :java",
'"depends_on "openjdk@11", "depends_on "openjdk@8" or "depends_on "openjdk"'
# but --version with ranges is broken on Big Sur today.
if MacOS.version >= :big_sur && @version&.end_with?("+")
odisabled %Q(depends_on java: "#{@version}"),
'depends_on "openjdk@11", depends_on "openjdk@8" or depends_on "openjdk"'
end
# odeprecated "depends_on :java",
# 'depends_on "openjdk@11", depends_on "openjdk@8" or depends_on "openjdk"'
return unless File.executable?("/usr/libexec/java_home")

View File

@ -654,7 +654,7 @@ class FormulaInstaller
@show_header = true unless deps.empty?
end
sig { params(dep: Formula).void }
sig { params(dep: Dependency).void }
def fetch_dependency(dep)
df = dep.to_formula
fi = FormulaInstaller.new(
@ -675,7 +675,7 @@ class FormulaInstaller
fi.fetch
end
sig { params(dep: Formula, inherited_options: Options).void }
sig { params(dep: Dependency, inherited_options: Options).void }
def install_dependency(dep, inherited_options)
df = dep.to_formula
tab = Tab.for_formula(df)

View File

@ -146,13 +146,16 @@ class Resource
def verify_download_integrity(fn)
if fn.file?
ohai "Verifying #{fn.basename} checksum" if verbose?
ohai "Verifying checksum for '#{fn.basename}'." if verbose?
fn.verify_checksum(checksum)
end
rescue ChecksumMissingError
opoo "Cannot verify integrity of #{fn.basename}"
puts "A checksum was not provided for this resource."
puts "For your reference the SHA-256 is: #{fn.sha256}"
opoo <<~EOS
Cannot verify integrity of '#{fn.basename}'.
No checksum was provided for this resource.
For your reference, the checksum is:
sha256 "#{fn.sha256}"
EOS
end
Checksum::TYPES.each do |type|

View File

@ -10,7 +10,7 @@ module Homebrew
attr_reader :name, :path, :tap_audit_exceptions, :problems
sig { params(tap: Tap, strict: T::Boolean).void }
sig { params(tap: Tap, strict: T.nilable(T::Boolean)).void }
def initialize(tap, strict:)
@name = tap.name
@path = tap.path

View File

@ -808,7 +808,6 @@ describe Cask::Audit, :cask do
let(:cask_token) { "with-binary" }
let(:cask) { Cask::CaskLoader.load(cask_token) }
let(:download_double) { instance_double(Cask::Download) }
let(:verify) { class_double(Cask::Verify).as_stubbed_const }
let(:message) { "Download Failed" }
before do
@ -817,19 +816,12 @@ describe Cask::Audit, :cask do
end
it "when download and verification succeed it does not fail" do
expect(download_double).to receive(:perform)
expect(verify).to receive(:all)
expect(download_double).to receive(:fetch)
expect(subject).to pass
end
it "when download fails it fails" do
expect(download_double).to receive(:perform).and_raise(StandardError.new(message))
expect(subject).to fail_with(/#{message}/)
end
it "when verification fails it fails" do
expect(download_double).to receive(:perform)
expect(verify).to receive(:all).and_raise(StandardError.new(message))
expect(download_double).to receive(:fetch).and_raise(StandardError.new(message))
expect(subject).to fail_with(/#{message}/)
end
end

View File

@ -36,7 +36,7 @@ describe Cask::Cmd::Fetch, :cask do
end
it "prevents double fetch (without nuking existing installation)" do
cached_location = Cask::Download.new(local_transmission).perform
cached_location = Cask::Download.new(local_transmission).fetch
old_ctime = File.stat(cached_location).ctime
@ -47,7 +47,7 @@ describe Cask::Cmd::Fetch, :cask do
end
it "allows double fetch with --force" do
cached_location = Cask::Download.new(local_transmission).perform
cached_location = Cask::Download.new(local_transmission).fetch
old_ctime = File.stat(cached_location).ctime
sleep(1)

View File

@ -11,7 +11,6 @@ describe Cask::Cmd::Install, :cask do
it "displays the installation progress" do
output = Regexp.new <<~EOS
==> Downloading file:.*caffeine.zip
==> Verifying SHA-256 checksum for Cask 'local-caffeine'.
==> Installing Cask local-caffeine
==> Moving App 'Caffeine.app' to '.*Caffeine.app'.
.*local-caffeine was successfully installed!

View File

@ -14,7 +14,6 @@ describe Cask::Cmd::Reinstall, :cask do
output = Regexp.new <<~EOS
==> Downloading file:.*caffeine.zip
Already downloaded: .*--caffeine.zip
==> Verifying SHA-256 checksum for Cask 'local-caffeine'.
==> Uninstalling Cask local-caffeine
==> Backing App 'Caffeine.app' up to '.*Caffeine.app'.
==> Removing App '.*Caffeine.app'.

View File

@ -75,7 +75,7 @@ describe Cask::Cmd::Style, :cask do
end
it "tries to find paths for all tokens" do
expect(Cask::CaskLoader).to receive(:load).twice.and_return(double("cask", sourcefile_path: nil))
expect(Cask::CaskLoader).to receive(:load).twice.and_return(instance_double(Cask::Cask, sourcefile_path: nil))
subject
end
end

View File

@ -344,7 +344,7 @@ describe Cask::Cmd::Upgrade, :cask do
expect {
described_class.run("bad-checksum")
}.to raise_error(Cask::CaskSha256MismatchError).and(not_to_output(output_reverted).to_stderr)
}.to raise_error(ChecksumMismatchError).and(not_to_output(output_reverted).to_stderr)
expect(bad_checksum).to be_installed
expect(bad_checksum_path).to be_a_directory

View File

@ -2,26 +2,30 @@
# frozen_string_literal: true
module Cask
describe Verify, :cask do
describe "::all" do
subject(:verification) { described_class.all(cask, downloaded_path) }
describe Download, :cask do
describe "#verify_download_integrity" do
subject(:verification) { described_class.new(cask).verify_download_integrity(downloaded_path) }
let(:cask) { instance_double(Cask, token: "cask", sha256: expected_sha256) }
let(:cafebabe) { "cafebabecafebabecafebabecafebabecafebabecafebabecafebabecafebabe" }
let(:deadbeef) { "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" }
let(:computed_sha256) { cafebabe }
let(:downloaded_path) { instance_double(Pathname, sha256: computed_sha256) }
let(:downloaded_path) { Pathname.new("cask.zip") }
before do
allow(downloaded_path).to receive(:sha256).and_return(computed_sha256)
end
context "when the expected checksum is :no_check" do
let(:expected_sha256) { :no_check }
it "skips the check" do
expect { verification }.to output(/skipping verification/).to_stdout
expect { verification }.to output(/skipping verification/).to_stderr
end
end
context "when expected and computed checksums match" do
let(:expected_sha256) { cafebabe }
let(:expected_sha256) { Checksum.new(:sha256, cafebabe) }
it "does not raise an error" do
expect { verification }.not_to raise_error
@ -31,24 +35,24 @@ module Cask
context "when the expected checksum is nil" do
let(:expected_sha256) { nil }
it "raises an error" do
expect { verification }.to raise_error(CaskSha256MissingError, /sha256 "#{computed_sha256}"/)
it "outputs an error" do
expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr
end
end
context "when the expected checksum is empty" do
let(:expected_sha256) { "" }
let(:expected_sha256) { Checksum.new(:sha256, "") }
it "raises an error" do
expect { verification }.to raise_error(CaskSha256MissingError, /sha256 "#{computed_sha256}"/)
it "outputs an error" do
expect { verification }.to output(/sha256 "#{computed_sha256}"/).to_stderr
end
end
context "when expected and computed checksums do not match" do
let(:expected_sha256) { deadbeef }
let(:expected_sha256) { Checksum.new(:sha256, deadbeef) }
it "raises an error" do
expect { verification }.to raise_error CaskSha256MismatchError
expect { verification }.to raise_error ChecksumMismatchError
end
end
end

View File

@ -65,14 +65,14 @@ describe Cask::Installer, :cask do
bad_checksum = Cask::CaskLoader.load(cask_path("bad-checksum"))
expect {
described_class.new(bad_checksum).install
}.to raise_error(Cask::CaskSha256MismatchError)
}.to raise_error(ChecksumMismatchError)
end
it "blows up on a missing checksum" do
missing_checksum = Cask::CaskLoader.load(cask_path("missing-checksum"))
expect {
described_class.new(missing_checksum).install
}.to raise_error(Cask::CaskSha256MissingError)
}.to output(/Cannot verify integrity/).to_stderr
end
it "installs fine if sha256 :no_check is used" do
@ -87,7 +87,7 @@ describe Cask::Installer, :cask do
no_checksum = Cask::CaskLoader.load(cask_path("no-checksum"))
expect {
described_class.new(no_checksum, require_sha: true).install
}.to raise_error(Cask::CaskNoShasumError)
}.to raise_error(/--require-sha/)
end
it "installs fine if sha256 :no_check is used with --require-sha and --force" do
@ -116,7 +116,6 @@ describe Cask::Installer, :cask do
}.to output(
<<~EOS,
==> Downloading file://#{HOMEBREW_LIBRARY_PATH}/test/support/fixtures/cask/caffeine.zip
==> Verifying SHA-256 checksum for Cask 'with-installer-manual'.
==> Installing Cask with-installer-manual
To complete the installation of Cask with-installer-manual, you must also
run the installer at:

View File

@ -31,7 +31,7 @@ describe Cask::Quarantine, :cask do
it "quarantines Cask fetches" do
Cask::Cmd::Fetch.run("local-transmission")
local_transmission = Cask::CaskLoader.load(cask_path("local-transmission"))
cached_location = Cask::Download.new(local_transmission).perform
cached_location = Cask::Download.new(local_transmission).fetch
expect(cached_location).to be_quarantined
end
@ -40,7 +40,7 @@ describe Cask::Quarantine, :cask do
Cask::Cmd::Audit.run("local-transmission", "--download")
local_transmission = Cask::CaskLoader.load(cask_path("local-transmission"))
cached_location = Cask::Download.new(local_transmission).perform
cached_location = Cask::Download.new(local_transmission).fetch
expect(cached_location).to be_quarantined
end
@ -142,7 +142,7 @@ describe Cask::Quarantine, :cask do
it "does not quarantine Cask fetches" do
Cask::Cmd::Fetch.run("local-transmission", "--no-quarantine")
local_transmission = Cask::CaskLoader.load(cask_path("local-transmission"))
cached_location = Cask::Download.new(local_transmission).perform
cached_location = Cask::Download.new(local_transmission).fetch
expect(cached_location).not_to be_quarantined
end
@ -151,7 +151,7 @@ describe Cask::Quarantine, :cask do
Cask::Cmd::Audit.run("local-transmission", "--download", "--no-quarantine")
local_transmission = Cask::CaskLoader.load(cask_path("local-transmission"))
cached_location = Cask::Download.new(local_transmission).perform
cached_location = Cask::Download.new(local_transmission).fetch
expect(cached_location).not_to be_quarantined
end

View File

@ -1,6 +1,6 @@
cask "invalid-manpage-no-section" do
version "1.2.3"
sha256 "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94"
sha256 "68b7e71a2ca7585b004f52652749589941e3029ff0884e8aa3b099594e0282c0"
url "file://#{TEST_FIXTURE_DIR}/cask/AppWithManpage.zip"
homepage "https://brew.sh/with-generic-artifact"

View File

@ -1,6 +1,6 @@
cask "with-autodetected-manpage-section" do
version "1.2.3"
sha256 "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94"
sha256 "68b7e71a2ca7585b004f52652749589941e3029ff0884e8aa3b099594e0282c0"
url "file://#{TEST_FIXTURE_DIR}/cask/AppWithManpage.zip"
homepage "https://brew.sh/with-autodetected-manpage-section"

View File

@ -1,6 +1,6 @@
cask "with-non-executable-binary" do
version "1.2.3"
sha256 "d5b2dfbef7ea28c25f7a77cd7fa14d013d82b626db1d82e00e25822464ba19e2"
sha256 "306c6ca7407560340797866e077e053627ad409277d1b9da58106fce4cf717cb"
url "file://#{TEST_FIXTURE_DIR}/cask/naked_non_executable"
homepage "https://brew.sh/with-binary"

View File

@ -82,6 +82,16 @@ module Homebrew
end
end
end
rescue MultipleVersionsInstalledError => e
ofail e
ensure
# If we delete Cellar/newname, then Cellar/oldname symlink
# can become broken and we have to remove it.
if HOMEBREW_CELLAR.directory?
HOMEBREW_CELLAR.children.each do |rack|
rack.unlink if rack.symlink? && !rack.resolved_path_exists?
end
end
end
def handle_unsatisfied_dependents(kegs_by_rack, ignore_dependencies: false, named_args: [])

View File

@ -199,8 +199,8 @@ an issue; just ignore this.
### `fetch` [*`options`*] *`formula`*
Download a bottle (if available) or source packages for *`formula`*.
For tarballs, also print SHA-256 checksums.
Download a bottle (if available) or source packages for *`formula`*e
and binaries for *`cask`*s. For files, also print SHA-256 checksums.
* `--HEAD`:
Fetch HEAD version instead of stable version.
@ -220,6 +220,12 @@ For tarballs, also print SHA-256 checksums.
Download source packages (for eventual bottling) rather than a bottle.
* `--force-bottle`:
Download a bottle if it exists for the current or newest version of macOS, even if it would not be used during installation.
* `--[no-]quarantine`:
Disable/enable quarantining of downloads (default: enabled).
* `--formula`:
Treat all named arguments as formulae.
* `--cask`:
Treat all named arguments as casks.
### `formulae`
@ -242,11 +248,11 @@ error message if no logs are found.
Open *`formula`*'s homepage in a browser, or open Homebrew's own homepage
if no formula is provided.
### `info` [*`options`*] [*`formula`*]
### `info` [*`options`*] [*`formula`*|*`cask`*]
Display brief statistics for your Homebrew installation.
If *`formula`* is provided, show summary of information about *`formula`*.
If a *`formula`* or *`cask`* is provided, show summary of information about it.
* `--analytics`:
List global Homebrew analytics data or, if specified, installation and build error data for *`formula`* (provided neither `HOMEBREW_NO_ANALYTICS` nor `HOMEBREW_NO_GITHUB_API` are set).
@ -264,6 +270,10 @@ If *`formula`* is provided, show summary of information about *`formula`*.
Print JSON of all available formulae.
* `-v`, `--verbose`:
Show more verbose analytics data for *`formula`*.
* `--formula`:
Treat all named arguments as formulae.
* `--cask`:
Treat all named arguments as casks.
### `install` [*`options`*] *`formula`*|*`cask`*
@ -567,12 +577,14 @@ If no *`tap`* names are provided, display brief statistics for all installed tap
* `--json`:
Print a JSON representation of *`tap`*. Currently the default and only accepted value for *`version`* is `v1`. See the docs for examples of using the JSON output: <https://docs.brew.sh/Querying-Brew>
### `uninstall`, `rm`, `remove` [*`options`*] *`formula`*
### `uninstall`, `rm`, `remove` [*`options`*] *`formula`*|*`cask`*
Uninstall *`formula`*.
Uninstall a *`formula`* or *`cask`*.
* `-f`, `--force`:
Delete all installed versions of *`formula`*.
Delete all installed versions of *`formula`*. Uninstall even if *`cask`* is not installed, overwrite existing files and ignore errors when removing files.
* `--zap`:
Remove all files associated with a *`cask`*. *May remove files which are shared between applications.*
* `--ignore-dependencies`:
Don't fail uninstall, even if *`formula`* is a dependency of any installed formulae.
* `--formula`:
@ -1001,10 +1013,15 @@ the Cellar and then link it into Homebrew's prefix with `brew link`.
* `--version`:
Explicitly set the *`version`* of the package being installed.
### `edit` [*`formula`*]
### `edit` [*`formula`*|*`cask`*]
Open *`formula`* in the editor set by `EDITOR` or `HOMEBREW_EDITOR`, or open the
Homebrew repository for editing if no formula is provided.
Open a *`formula`* or *`cask`* in the editor set by `EDITOR` or `HOMEBREW_EDITOR`,
or open the Homebrew repository for editing if no formula is provided.
* `--formula`:
Treat all named arguments as formulae.
* `--cask`:
Treat all named arguments as casks.
### `extract` [*`options`*] *`formula`* *`tap`*

View File

@ -254,7 +254,7 @@ List all audit methods, which can be run individually if provided as arguments\.
Enable debugging and profiling of audit methods\.
.
.SS "\fBfetch\fR [\fIoptions\fR] \fIformula\fR"
Download a bottle (if available) or source packages for \fIformula\fR\. For tarballs, also print SHA\-256 checksums\.
Download a bottle (if available) or source packages for \fIformula\fRe and binaries for \fIcask\fRs\. For files, also print SHA\-256 checksums\.
.
.TP
\fB\-\-HEAD\fR
@ -292,6 +292,18 @@ Download source packages (for eventual bottling) rather than a bottle\.
\fB\-\-force\-bottle\fR
Download a bottle if it exists for the current or newest version of macOS, even if it would not be used during installation\.
.
.TP
\fB\-\-[no\-]quarantine\fR
Disable/enable quarantining of downloads (default: enabled)\.
.
.TP
\fB\-\-formula\fR
Treat all named arguments as formulae\.
.
.TP
\fB\-\-cask\fR
Treat all named arguments as casks\.
.
.SS "\fBformulae\fR"
List all locally installable formulae including short names\.
.
@ -313,11 +325,11 @@ The Gist will be marked private and will not appear in listings but will be acce
.SS "\fBhome\fR [\fIformula\fR]"
Open \fIformula\fR\'s homepage in a browser, or open Homebrew\'s own homepage if no formula is provided\.
.
.SS "\fBinfo\fR [\fIoptions\fR] [\fIformula\fR]"
.SS "\fBinfo\fR [\fIoptions\fR] [\fIformula\fR|\fIcask\fR]"
Display brief statistics for your Homebrew installation\.
.
.P
If \fIformula\fR is provided, show summary of information about \fIformula\fR\.
If a \fIformula\fR or \fIcask\fR is provided, show summary of information about it\.
.
.TP
\fB\-\-analytics\fR
@ -351,6 +363,14 @@ Print JSON of all available formulae\.
\fB\-v\fR, \fB\-\-verbose\fR
Show more verbose analytics data for \fIformula\fR\.
.
.TP
\fB\-\-formula\fR
Treat all named arguments as formulae\.
.
.TP
\fB\-\-cask\fR
Treat all named arguments as casks\.
.
.SS "\fBinstall\fR [\fIoptions\fR] \fIformula\fR|\fIcask\fR"
Install a \fIformula\fR or \fIcask\fR\. Additional options specific to a \fIformula\fR may be appended to the command\.
.
@ -785,12 +805,16 @@ Show information on each installed tap\.
\fB\-\-json\fR
Print a JSON representation of \fItap\fR\. Currently the default and only accepted value for \fIversion\fR is \fBv1\fR\. See the docs for examples of using the JSON output: \fIhttps://docs\.brew\.sh/Querying\-Brew\fR
.
.SS "\fBuninstall\fR, \fBrm\fR, \fBremove\fR [\fIoptions\fR] \fIformula\fR"
Uninstall \fIformula\fR\.
.SS "\fBuninstall\fR, \fBrm\fR, \fBremove\fR [\fIoptions\fR] \fIformula\fR|\fIcask\fR"
Uninstall a \fIformula\fR or \fIcask\fR\.
.
.TP
\fB\-f\fR, \fB\-\-force\fR
Delete all installed versions of \fIformula\fR\.
Delete all installed versions of \fIformula\fR\. Uninstall even if \fIcask\fR is not installed, overwrite existing files and ignore errors when removing files\.
.
.TP
\fB\-\-zap\fR
Remove all files associated with a \fIcask\fR\. \fIMay remove files which are shared between applications\.\fR
.
.TP
\fB\-\-ignore\-dependencies\fR
@ -1396,8 +1420,16 @@ Explicitly set the \fIname\fR of the package being installed\.
\fB\-\-version\fR
Explicitly set the \fIversion\fR of the package being installed\.
.
.SS "\fBedit\fR [\fIformula\fR]"
Open \fIformula\fR in the editor set by \fBEDITOR\fR or \fBHOMEBREW_EDITOR\fR, or open the Homebrew repository for editing if no formula is provided\.
.SS "\fBedit\fR [\fIformula\fR|\fIcask\fR]"
Open a \fIformula\fR or \fIcask\fR in the editor set by \fBEDITOR\fR or \fBHOMEBREW_EDITOR\fR, or open the Homebrew repository for editing if no formula is provided\.
.
.TP
\fB\-\-formula\fR
Treat all named arguments as formulae\.
.
.TP
\fB\-\-cask\fR
Treat all named arguments as casks\.
.
.SS "\fBextract\fR [\fIoptions\fR] \fIformula\fR \fItap\fR"
Look through repository history to find the most recent version of \fIformula\fR and create a copy in \fItap\fR\fB/Formula/\fR\fIformula\fR\fB@\fR\fIversion\fR\fB\.rb\fR\. If the tap is not installed yet, attempt to install/clone the tap before continuing\. To extract a formula from a tap that is not \fBhomebrew/core\fR use its fully\-qualified form of \fIuser\fR\fB/\fR\fIrepo\fR\fB/\fR\fIformula\fR\.