cask: delay loading from source API

For casks with certain stanzas, *flight and language
stanzas in this case, we need to use the caskfile
to install them correctly. The JSON API is not an option.

This delays loading from the source API until just before
we try to install one of these casks. This reduces the
number of requests we make to the source API.
This commit is contained in:
apainintheneck 2023-02-25 02:08:39 -08:00
parent 0172ad1028
commit fd62cdf636
5 changed files with 103 additions and 93 deletions

View File

@ -83,15 +83,14 @@ module Cask
@tap @tap
end end
def initialize(token, sourcefile_path: nil, source: nil, source_checksum: nil, tap: nil, def initialize(token, sourcefile_path: nil, source: nil, tap: nil,
config: nil, allow_reassignment: false, loaded_from_api: false, loader: nil, &block) config: nil, allow_reassignment: false, loader: nil, &block)
@token = token @token = token
@sourcefile_path = sourcefile_path @sourcefile_path = sourcefile_path
@source = source @source = source
@ruby_source_checksum = source_checksum
@tap = tap @tap = tap
@allow_reassignment = allow_reassignment @allow_reassignment = allow_reassignment
@loaded_from_api = loaded_from_api @loaded_from_api = false
@loader = loader @loader = loader
@block = block @block = block
@ -166,6 +165,12 @@ module Cask
!versions.empty? !versions.empty?
end end
# The caskfile is needed during installation when there are
# `*flight` blocks or the cask has multiple languages
def caskfile_only?
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
end
sig { returns(T.nilable(Time)) } sig { returns(T.nilable(Time)) }
def install_time def install_time
_, time = timestamped_versions.last _, time = timestamped_versions.last
@ -258,6 +263,27 @@ module Cask
end end
end end
def ruby_source_checksum
@ruby_source_checksum ||= {
"sha256" => Digest::SHA256.file(sourcefile_path).hexdigest,
}.freeze
end
def languages
@languages ||= @dsl.languages
end
def tap_git_head
@tap_git_head ||= tap&.git_head
end
def populate_from_api!(json_cask)
@loaded_from_api = true
@languages = json_cask[:languages]
@tap_git_head = json_cask[:tap_git_head]
@ruby_source_checksum = json_cask[:ruby_source_checksum].freeze
end
def to_s def to_s
@token @token
end end
@ -306,7 +332,7 @@ module Cask
"conflicts_with" => conflicts_with, "conflicts_with" => conflicts_with,
"container" => container&.pairs, "container" => container&.pairs,
"auto_updates" => auto_updates, "auto_updates" => auto_updates,
"tap_git_head" => tap&.git_head, "tap_git_head" => tap_git_head,
"languages" => languages, "languages" => languages,
"ruby_source_checksum" => ruby_source_checksum, "ruby_source_checksum" => ruby_source_checksum,
} }
@ -360,12 +386,6 @@ module Cask
hash hash
end end
def ruby_source_checksum
@ruby_source_checksum ||= {
"sha256" => Digest::SHA256.file(sourcefile_path).hexdigest,
}
end
def artifacts_list def artifacts_list
artifacts.map do |artifact| artifacts.map do |artifact|
case artifact case artifact

View File

@ -45,10 +45,7 @@ module Cask
private private
def cask(header_token, **options, &block) def cask(header_token, **options, &block)
checksum = { Cask.new(header_token, source: content, **options, config: @config, &block)
"sha256" => Digest::SHA256.hexdigest(content),
}
Cask.new(header_token, source: content, source_checksum: checksum, **options, config: @config, &block)
end end
end end
@ -212,8 +209,6 @@ module Cask
class FromAPILoader class FromAPILoader
attr_reader :token, :path attr_reader :token, :path
FLIGHT_STANZAS = [:preflight, :postflight, :uninstall_preflight, :uninstall_postflight].freeze
def self.can_load?(ref) def self.can_load?(ref)
return false if Homebrew::EnvConfig.no_install_from_api? return false if Homebrew::EnvConfig.no_install_from_api?
return false unless ref.is_a?(String) return false unless ref.is_a?(String)
@ -233,16 +228,7 @@ module Cask
json_cask = @from_json || Homebrew::API::Cask.all_casks[token] json_cask = @from_json || Homebrew::API::Cask.all_casks[token]
cask_source = JSON.pretty_generate(json_cask) cask_source = JSON.pretty_generate(json_cask)
json_cask = Homebrew::API.merge_variations(json_cask).deep_symbolize_keys json_cask = Homebrew::API.merge_variations(json_cask).deep_symbolize_keys.freeze
# Use the cask-source API if there are any `*flight` blocks or the cask has multiple languages
if json_cask[:artifacts].any? { |artifact| FLIGHT_STANZAS.include?(artifact.keys.first) } ||
json_cask[:languages].any?
cask_source = Homebrew::API::Cask.fetch_source(token,
git_head: json_cask[:tap_git_head],
sha256: json_cask.dig(:ruby_source_checksum, :sha256))
return FromContentLoader.new(cask_source).load(config: config)
end
tap = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/") tap = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/")
@ -252,13 +238,7 @@ module Cask
json_cask[:url_specs][:using] = using.to_sym json_cask[:url_specs][:using] = using.to_sym
end end
Cask.new(token, api_cask = Cask.new(token, tap: tap, source: cask_source, config: config, loader: self) do
tap: tap,
source: cask_source,
source_checksum: json_cask[:ruby_source_checksum],
config: config,
loaded_from_api: true,
loader: self) do
version json_cask[:version] version json_cask[:version]
if json_cask[:sha256] == "no_check" if json_cask[:sha256] == "no_check"
@ -318,15 +298,21 @@ module Cask
# convert generic string replacements into actual ones # convert generic string replacements into actual ones
artifact = cask.loader.from_h_hash_gsubs(artifact, appdir) artifact = cask.loader.from_h_hash_gsubs(artifact, appdir)
key = artifact.keys.first key = artifact.keys.first
send(key, *artifact[key]) if artifact[key].nil?
# for artifacts with blocks that can't be loaded from the API
send(key) {} # empty on purpose
else
send(key, *artifact[key])
end
end end
if json_cask[:caveats].present? if json_cask[:caveats].present?
# convert generic string replacements into actual ones # convert generic string replacements into actual ones
json_cask[:caveats] = cask.loader.from_h_string_gsubs(json_cask[:caveats], appdir) caveats cask.loader.from_h_string_gsubs(json_cask[:caveats], appdir)
caveats json_cask[:caveats]
end end
end end
api_cask.populate_from_api!(json_cask)
api_cask
end end
def from_h_string_gsubs(string, appdir) def from_h_string_gsubs(string, appdir)

View File

@ -77,7 +77,6 @@ module Cask
:depends_on, :depends_on,
:homepage, :homepage,
:language, :language,
:languages,
:name, :name,
:sha256, :sha256,
:staged_path, :staged_path,

View File

@ -64,6 +64,8 @@ module Cask
def fetch(quiet: nil, timeout: nil) def fetch(quiet: nil, timeout: nil)
odebug "Cask::Installer#fetch" odebug "Cask::Installer#fetch"
load_cask_from_source_api! if @cask.loaded_from_api && @cask.caskfile_only?
verify_has_sha if require_sha? && !force? verify_has_sha if require_sha? && !force?
download(quiet: quiet, timeout: timeout) download(quiet: quiet, timeout: timeout)
@ -149,16 +151,8 @@ module Cask
def uninstall_existing_cask def uninstall_existing_cask
return unless @cask.installed? return unless @cask.installed?
# use the same cask file that was used for installation, if possible
installed_caskfile = @cask.installed_caskfile
installed_cask = begin
installed_caskfile.exist? ? CaskLoader.load(installed_caskfile) : @cask
rescue CaskInvalidError # could be thrown by call to CaskLoader#load with outdated caskfile
@cask # default
end
# Always force uninstallation, ignore method parameter # Always force uninstallation, ignore method parameter
cask_installer = Installer.new(installed_cask, verbose: verbose?, force: true, upgrade: upgrade?) cask_installer = Installer.new(@cask, verbose: verbose?, force: true, upgrade: upgrade?)
zap? ? cask_installer.zap : cask_installer.uninstall zap? ? cask_installer.zap : cask_installer.uninstall
end end
@ -403,6 +397,7 @@ module Cask
end end
def uninstall def uninstall
load_installed_caskfile!
oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}" oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}"
uninstall_artifacts(clear: true) uninstall_artifacts(clear: true)
if !reinstall? && !upgrade? if !reinstall? && !upgrade?
@ -480,6 +475,7 @@ module Cask
end end
def zap def zap
load_installed_caskfile!
ohai "Implied `brew uninstall --cask #{@cask}`" ohai "Implied `brew uninstall --cask #{@cask}`"
uninstall_artifacts uninstall_artifacts
if (zap_stanzas = @cask.artifacts.select { |a| a.is_a?(Artifact::Zap) }).empty? if (zap_stanzas = @cask.artifacts.select { |a| a.is_a?(Artifact::Zap) }).empty?
@ -547,5 +543,30 @@ module Cask
odebug "Purging all staged versions of Cask #{@cask}" odebug "Purging all staged versions of Cask #{@cask}"
gain_permissions_remove(@cask.caskroom_path) gain_permissions_remove(@cask.caskroom_path)
end end
private
# load the same cask file that was used for installation, if possible
def load_installed_caskfile!
installed_caskfile = @cask.installed_caskfile
if installed_caskfile.exist?
begin
@cask = CaskLoader.load(installed_caskfile)
return
rescue CaskInvalidError
# could be caused by trying to load outdated caskfile
end
end
load_cask_from_source_api! if @cask.loaded_from_api && @cask.caskfile_only?
# otherwise we default to the current cask
end
def load_cask_from_source_api!
options = { git_head: @cask.tap_git_head, sha256: @cask.ruby_source_checksum["sha256"] }
cask_source = Homebrew::API::Cask.fetch_source(@cask.token, **options)
@cask = CaskLoader::FromContentLoader.new(cask_source).load(config: @cask.config)
end
end end
end end

View File

@ -59,80 +59,64 @@ describe Cask::CaskLoader::FromAPILoader, :cask do
end end
describe "#load" do describe "#load" do
shared_examples "loads from fetched source" do |cask_token| shared_examples "loads from API" do |cask_token, caskfile_only|
include_context "with API setup", cask_token
let(:content_loader) { instance_double(Cask::CaskLoader::FromContentLoader) }
it "fetches cask source from API" do
expect(Homebrew::API::Cask).to receive(:fetch_source).once
expect(Cask::CaskLoader::FromContentLoader)
.to receive(:new).once
.and_return(content_loader)
expect(content_loader).to receive(:load).once
api_loader.load(config: nil)
end
end
context "with a preflight stanza" do
include_examples "loads from fetched source", "with-preflight"
end
context "with an uninstall-preflight stanza" do
include_examples "loads from fetched source", "with-uninstall-preflight"
end
context "with a postflight stanza" do
include_examples "loads from fetched source", "with-postflight"
end
context "with an uninstall-postflight stanza" do
include_examples "loads from fetched source", "with-uninstall-postflight"
end
context "with a language stanza" do
include_examples "loads from fetched source", "with-languages"
end
shared_examples "loads from API" do |cask_token|
include_context "with API setup", cask_token include_context "with API setup", cask_token
let(:cask_from_api) { api_loader.load(config: nil) } let(:cask_from_api) { api_loader.load(config: nil) }
it "loads from JSON API" do it "loads from JSON API" do
expect(Homebrew::API::Cask).not_to receive(:fetch_source)
expect(Cask::CaskLoader::FromContentLoader).not_to receive(:new)
expect(cask_from_api).to be_a(Cask::Cask) expect(cask_from_api).to be_a(Cask::Cask)
expect(cask_from_api.token).to eq(cask_token) expect(cask_from_api.token).to eq(cask_token)
expect(cask_from_api.loaded_from_api).to be(true)
expect(cask_from_api.caskfile_only?).to be(caskfile_only)
end end
end end
context "with a binary stanza" do context "with a binary stanza" do
include_examples "loads from API", "with-binary" include_examples "loads from API", "with-binary", false
end end
context "with cask dependencies" do context "with cask dependencies" do
include_examples "loads from API", "with-depends-on-cask-multiple" include_examples "loads from API", "with-depends-on-cask-multiple", false
end end
context "with formula dependencies" do context "with formula dependencies" do
include_examples "loads from API", "with-depends-on-formula-multiple" include_examples "loads from API", "with-depends-on-formula-multiple", false
end end
context "with macos dependencies" do context "with macos dependencies" do
include_examples "loads from API", "with-depends-on-macos-array" include_examples "loads from API", "with-depends-on-macos-array", false
end end
context "with an installer stanza" do context "with an installer stanza" do
include_examples "loads from API", "with-installer-script" include_examples "loads from API", "with-installer-script", false
end end
context "with uninstall stanzas" do context "with uninstall stanzas" do
include_examples "loads from API", "with-uninstall-multi" include_examples "loads from API", "with-uninstall-multi", false
end end
context "with a zap stanza" do context "with a zap stanza" do
include_examples "loads from API", "with-zap" include_examples "loads from API", "with-zap", false
end
context "with a preflight stanza" do
include_examples "loads from API", "with-preflight", true
end
context "with an uninstall-preflight stanza" do
include_examples "loads from API", "with-uninstall-preflight", true
end
context "with a postflight stanza" do
include_examples "loads from API", "with-postflight", true
end
context "with an uninstall-postflight stanza" do
include_examples "loads from API", "with-uninstall-postflight", true
end
context "with a language stanza" do
include_examples "loads from API", "with-languages", true
end end
end end
end end