Support casks in brew fetch.

This commit is contained in:
Markus Reiter 2020-11-19 18:12:16 +01:00
parent 12495bc804
commit 7a83f34dd1
22 changed files with 204 additions and 154 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

@ -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,44 @@ 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
ohai "Verifying checksum for Cask '#{@cask}'." if verbose?
expected = @cask.sha256
actual = fn.sha256
begin
fn.verify_checksum(expected)
rescue ChecksumMissingError
raise CaskSha256MissingError.new(@cask.token, expected, actual)
rescue ChecksumMismatchError
raise CaskSha256MismatchError.new(@cask.token, expected, actual, fn)
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

@ -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
@ -168,10 +166,6 @@ module Cask
raise CaskNoShasumError, @cask.token
end
def verify
Verify.all(@cask, @downloaded_path)
end
def primary_container
@primary_container ||= begin
download

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

@ -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

@ -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

@ -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
@ -37,7 +41,7 @@ module Cask
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}"/)
@ -45,7 +49,7 @@ module Cask
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

View File

@ -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

@ -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.
### `gist-logs` [*`options`*] *`formula`*

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 "\fBgist\-logs\fR [\fIoptions\fR] \fIformula\fR"
Upload logs for a failed build of \fIformula\fR to a new Gist\. Presents an error message if no logs are found\.
.