From 374b61584b560a011f2dc480bea38e5bcb5326ec Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Thu, 29 Dec 2022 02:48:31 -0500 Subject: [PATCH] Load casks from the JSON API with `HOMEBREW_INSTALL_FROM_API` --- Library/Homebrew/api/cask.rb | 35 +++++++++++ Library/Homebrew/cask/cask_loader.rb | 91 +++++++++++++++++++++++++++- Library/Homebrew/cmd/update.sh | 44 ++++++++------ 3 files changed, 147 insertions(+), 23 deletions(-) diff --git a/Library/Homebrew/api/cask.rb b/Library/Homebrew/api/cask.rb index b7ec658bf2..949c739489 100644 --- a/Library/Homebrew/api/cask.rb +++ b/Library/Homebrew/api/cask.rb @@ -10,10 +10,45 @@ module Homebrew class << self extend T::Sig + MAX_RETRIES = 3 + + sig { returns(String) } + def cached_cask_json_file + HOMEBREW_CACHE_API/"cask.json" + end + sig { params(name: String).returns(Hash) } def fetch(name) Homebrew::API.fetch "cask/#{name}.json" end + + sig { returns(Hash) } + def all_casks + @all_casks ||= begin + retry_count = 0 + + url = "https://formulae.brew.sh/api/cask.json" + json_casks = begin + curl_args = %W[--compressed --silent #{url}] + if cached_cask_json_file.exist? && !cached_cask_json_file.empty? + curl_args.prepend("--time-cond", cached_cask_json_file) + end + curl_download(*curl_args, to: cached_cask_json_file, max_time: 5) + + JSON.parse(cached_cask_json_file.read) + rescue JSON::ParserError + cached_cask_json_file.unlink + retry_count += 1 + odie "Cannot download non-corrupt #{url}!" if retry_count > MAX_RETRIES + + retry + end + + json_casks.to_h do |json_cask| + [json_cask["token"], json_cask.except("token")] + end + end + end end end end diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 28c53f7527..4ed6db0c23 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -186,6 +186,91 @@ module Cask end end + # Loads a cask from the JSON API. + class FromAPILoader + attr_reader :token, :path + + FLIGHT_STANZAS = [:preflight, :postflight, :uninstall_preflight, :uninstall_postflight].freeze + + def self.can_load?(ref) + Homebrew::API::Cask.all_casks.key? ref + end + + def initialize(token) + @token = token + @path = CaskLoader.default_path(token) + end + + def load(config:) + json_cask = Homebrew::API::Cask.all_casks[token] + cask_source = Homebrew::API::CaskSource.fetch(token) + + if (bottle_tag = ::Utils::Bottles.tag.to_s.presence) && + (variations = json_cask["variations"].presence) && + (variation = variations[bottle_tag].presence) + json_cask = json_cask.merge(variation) + end + + json_cask.deep_symbolize_keys! + + Cask.new(token, source: cask_source, config: config) do + version json_cask[:version] + + if json_cask[:sha256] == "no_check" + sha256 :no_check + else + sha256 json_cask[:sha256] + end + + url json_cask[:url] + appcast json_cask[:appcast] if json_cask[:appcast].present? + json_cask[:name].each do |cask_name| + name cask_name + end + desc json_cask[:desc] + homepage json_cask[:homepage] + + auto_updates json_cask[:auto_updates] if json_cask[:auto_updates].present? + conflicts_with(**json_cask[:conflicts_with]) if json_cask[:conflicts_with].present? + + if json_cask[:depends_on].present? + dep_hash = json_cask[:depends_on].to_h do |dep_key, dep_value| + next [dep_key, dep_value] unless dep_key == :macos + + dep_type = dep_value.keys.first + if dep_type == :== + version_symbols = dep_value[dep_type].map do |version| + MacOSVersions::SYMBOLS.key(version) || version + end + next [dep_key, version_symbols] + end + + version_symbol = dep_value[dep_type].first + version_symbol = MacOSVersions::SYMBOLS.key(version_symbol) || version_symbol + [dep_key, "#{dep_type} :#{version_symbol}"] + end.compact + depends_on(**dep_hash) + end + + if json_cask[:container].present? + container_hash = json_cask[:container].to_h do |container_key, container_value| + next [container_key, container_value] unless container_key == :type + + [container_key, container_value.to_sym] + end + container(**container_hash) + end + + json_cask[:artifacts].each do |artifact| + key = artifact.keys.first + send(key, *artifact[key]) + end + + caveats json_cask[:caveats] if json_cask[:caveats].present? + end + end + end + # Pseudo-loader which raises an error when trying to load the corresponding cask. class NullLoader < FromPathLoader extend T::Sig @@ -225,15 +310,15 @@ module Cask next unless loader_class.can_load?(ref) if loader_class == FromTapLoader && Homebrew::EnvConfig.install_from_api? && - ref.start_with?("homebrew/cask/") && Homebrew::API::CaskSource.available?(ref) - return FromContentLoader.new(Homebrew::API::CaskSource.fetch(ref)) + ref.start_with?("homebrew/cask/") && FromAPILoader.can_load?(ref) + return FromAPILoader.new(ref) end return loader_class.new(ref) end if Homebrew::EnvConfig.install_from_api? && !need_path && Homebrew::API::CaskSource.available?(ref) - return FromContentLoader.new(Homebrew::API::CaskSource.fetch(ref)) + return FromAPILoader.new(ref) end return FromTapPathLoader.new(default_path(ref)) if FromTapPathLoader.can_load?(default_path(ref)) diff --git a/Library/Homebrew/cmd/update.sh b/Library/Homebrew/cmd/update.sh index a171c450b4..c77a428457 100644 --- a/Library/Homebrew/cmd/update.sh +++ b/Library/Homebrew/cmd/update.sh @@ -754,28 +754,32 @@ EOS if [[ -n "${HOMEBREW_INSTALL_FROM_API}" ]] then mkdir -p "${HOMEBREW_CACHE}/api" - if [[ -f "${HOMEBREW_CACHE}/api/formula.json" ]] - then - INITIAL_JSON_BYTESIZE="$(wc -c "${HOMEBREW_CACHE}"/api/formula.json)" - fi - curl \ - "${CURL_DISABLE_CURLRC_ARGS[@]}" \ - --fail --compressed --silent --max-time 5 \ - --location --remote-time --output "${HOMEBREW_CACHE}/api/formula.json" \ - --time-cond "${HOMEBREW_CACHE}/api/formula.json" \ - --user-agent "${HOMEBREW_USER_AGENT_CURL}" \ - "https://formulae.brew.sh/api/formula.json" - curl_exit_code=$? - if [[ ${curl_exit_code} -eq 0 ]] - then - CURRENT_JSON_BYTESIZE="$(wc -c "${HOMEBREW_CACHE}"/api/formula.json)" - if [[ "${INITIAL_JSON_BYTESIZE}" != "${CURRENT_JSON_BYTESIZE}" ]] + + for formula_or_cask in formula cask + do + if [[ -f "${HOMEBREW_CACHE}/api/${formula_or_cask}.json" ]] then - HOMEBREW_UPDATED="1" + INITIAL_JSON_BYTESIZE="$(wc -c "${HOMEBREW_CACHE}"/api/"${formula_or_cask}".json)" fi - else - echo "Failed to download formula.json!" >>"${update_failed_file}" - fi + curl \ + "${CURL_DISABLE_CURLRC_ARGS[@]}" \ + --fail --compressed --silent --max-time 5 \ + --location --remote-time --output "${HOMEBREW_CACHE}/api/${formula_or_cask}.json" \ + --time-cond "${HOMEBREW_CACHE}/api/${formula_or_cask}.json" \ + --user-agent "${HOMEBREW_USER_AGENT_CURL}" \ + "https://formulae.brew.sh/api/${formula_or_cask}.json" + curl_exit_code=$? + if [[ ${curl_exit_code} -eq 0 ]] + then + CURRENT_JSON_BYTESIZE="$(wc -c "${HOMEBREW_CACHE}"/api/"${formula_or_cask}".json)" + if [[ "${INITIAL_JSON_BYTESIZE}" != "${CURRENT_JSON_BYTESIZE}" ]] + then + HOMEBREW_UPDATED="1" + fi + else + echo "Failed to download ${formula_or_cask}.json!" >>"${update_failed_file}" + fi + done fi safe_cd "${HOMEBREW_REPOSITORY}"