diff --git a/Library/Homebrew/bottle_api.rb b/Library/Homebrew/bottle_api.rb new file mode 100644 index 0000000000..2c29bb1086 --- /dev/null +++ b/Library/Homebrew/bottle_api.rb @@ -0,0 +1,104 @@ +# typed: true +# frozen_string_literal: true + +require "github_packages" + +# Helper functions for using the Bottle JSON API. +# +# @api private +module BottleAPI + extend T::Sig + + module_function + + FORMULAE_BREW_SH_BOTTLE_API_DOMAIN = if OS.mac? + "https://formulae.brew.sh/api/bottle" + else + "https://formulae.brew.sh/api/bottle-linux" + end.freeze + + FORMULAE_BREW_SH_VERSIONS_API_URL = if OS.mac? + "https://formulae.brew.sh/api/versions-formulae.json" + else + "https://formulae.brew.sh/api/versions-linux.json" + end.freeze + + GITHUB_PACKAGES_SHA256_REGEX = %r{#{GitHubPackages::URL_REGEX}.*/blobs/sha256:(?\h{64})$}.freeze + + sig { params(name: String).returns(Hash) } + def fetch(name) + return @cache[name] if @cache.present? && @cache.key?(name) + + api_url = "#{FORMULAE_BREW_SH_BOTTLE_API_DOMAIN}/#{name}.json" + output = Utils::Curl.curl_output("--fail", api_url) + raise ArgumentError, "No JSON file found at #{Tty.underline}#{api_url}#{Tty.reset}" unless output.success? + + @cache ||= {} + @cache[name] = JSON.parse(output.stdout) + rescue JSON::ParserError + raise ArgumentError, "Invalid JSON file: #{Tty.underline}#{api_url}#{Tty.reset}" + end + + sig { params(name: String).returns(T.nilable(PkgVersion)) } + def latest_pkg_version(name) + @formula_versions ||= begin + output = Utils::Curl.curl_output("--fail", FORMULAE_BREW_SH_VERSIONS_API_URL) + JSON.parse(output.stdout) + end + + return unless @formula_versions.key? name + + PkgVersion.new(@formula_versions[name]["version"], @formula_versions[name]["revision"]) + end + + sig { params(name: String).returns(T::Boolean) } + def bottle_available?(name) + fetch name + true + rescue ArgumentError + false + end + + sig { params(name: String).void } + def fetch_bottles(name) + hash = fetch(name) + bottle_tag = Utils::Bottles.tag.to_s + + odie "No bottle available for current OS" unless hash["bottles"].key? bottle_tag + + download_bottle(hash, bottle_tag) + + hash["dependencies"].each do |dep_hash| + download_bottle(dep_hash, bottle_tag) + end + end + + sig { params(url: String).returns(T.nilable(String)) } + def checksum_from_url(url) + match = url.match GITHUB_PACKAGES_SHA256_REGEX + return if match.blank? + + match[:sha256] + end + + sig { params(hash: Hash, tag: Symbol).void } + def download_bottle(hash, tag) + bottle = hash["bottles"][tag] + return if bottle.blank? + + sha256 = bottle["sha256"] || checksum_from_url(bottle["url"]) + bottle_filename = Bottle::Filename.new(hash["name"], hash["pkg_version"], tag, hash["rebuild"]) + + resource = Resource.new hash["name"] + resource.url bottle["url"] + resource.sha256 sha256 + resource.version hash["pkg_version"] + resource.downloader.resolved_basename = bottle_filename + + resource.fetch + + # Map the name of this formula to the local bottle path to allow the + # formula to be loaded by passing just the name to `Formulary::factory`. + Formulary.map_formula_name_to_local_bottle_path hash["name"], resource.downloader.cached_location + end +end diff --git a/Library/Homebrew/bottle_api.rbi b/Library/Homebrew/bottle_api.rbi new file mode 100644 index 0000000000..6fefceb732 --- /dev/null +++ b/Library/Homebrew/bottle_api.rbi @@ -0,0 +1,5 @@ +# typed: strict + +module BottleAPI + include Kernel +end diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 3160fb2252..4a8fbbd847 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -2,7 +2,7 @@ # frozen_string_literal: true require "delegate" - +require "bottle_api" require "cli/args" module Homebrew @@ -124,8 +124,8 @@ module Homebrew unreadable_error ||= e rescue NoSuchKegError, FormulaUnavailableError => e if load_from_json && ENV["HOMEBREW_JSON_CORE"].present? && !CoreTap.instance.installed? && - Utils::BottleAPI.bottle_available?(name) - Utils::BottleAPI.fetch_bottles(name) + BottleAPI.bottle_available?(name) + BottleAPI.fetch_bottles(name) retry end diff --git a/Library/Homebrew/cmd/outdated.rb b/Library/Homebrew/cmd/outdated.rb index 801b3e8370..dc49defd60 100644 --- a/Library/Homebrew/cmd/outdated.rb +++ b/Library/Homebrew/cmd/outdated.rb @@ -6,6 +6,7 @@ require "keg" require "cli/parser" require "cask/cmd" require "cask/caskroom" +require "bottle_api" module Homebrew extend T::Sig @@ -100,7 +101,7 @@ module Homebrew elsif f.tap.present? f.pkg_version.to_s else - Utils::BottleAPI.latest_pkg_version(f.name)&.to_s || f.pkg_version.to_s + BottleAPI.latest_pkg_version(f.name)&.to_s || f.pkg_version.to_s end outdated_versions = outdated_kegs.group_by { |keg| Formulary.from_keg(keg).full_name } diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index 9dc0b25e40..bd64922c22 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -12,6 +12,7 @@ require "cask/cmd" require "cask/utils" require "cask/macos" require "upgrade" +require "bottle_api" module Homebrew extend T::Sig @@ -89,9 +90,9 @@ module Homebrew formula = Formulary.factory(name) next unless formula.any_version_installed? next if formula.tap.present? - next unless Utils::BottleAPI.bottle_available?(name) + next unless BottleAPI.bottle_available?(name) - Utils::BottleAPI.fetch_bottles(name) + BottleAPI.fetch_bottles(name) rescue FormulaUnavailableError next end diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index ded57bc74d..3e8ff5c935 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -8,6 +8,7 @@ require "upgrade" require "cask/cmd" require "cask/utils" require "cask/macos" +require "bottle_api" module Homebrew extend T::Sig @@ -162,9 +163,9 @@ module Homebrew if ENV["HOMEBREW_JSON_CORE"].present? && !CoreTap.instance.installed? formulae_to_install.map! do |formula| next formula if formula.tap.present? - next formula unless Utils::BottleAPI.bottle_available?(formula.name) + next formula unless BottleAPI.bottle_available?(formula.name) - Utils::BottleAPI.fetch_bottles(formula.name) + BottleAPI.fetch_bottles(formula.name) Formulary.factory(formula.name) rescue FormulaUnavailableError formula diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 8c6bda7216..719aec9c07 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -29,6 +29,7 @@ require "mktemp" require "find" require "utils/spdx" require "extend/on_os" +require "bottle_api" # A formula provides instructions and metadata for Homebrew to install a piece # of software. Every Homebrew formula is a {Formula}. @@ -1328,7 +1329,7 @@ class Formula latest_version = if tap.present? pkg_version else - Utils::BottleAPI.latest_pkg_version(name) || pkg_version + BottleAPI.latest_pkg_version(name) || pkg_version end installed_kegs.each do |keg| diff --git a/Library/Homebrew/utils.rb b/Library/Homebrew/utils.rb index 5b4de1c9de..361cc285f7 100644 --- a/Library/Homebrew/utils.rb +++ b/Library/Homebrew/utils.rb @@ -4,7 +4,6 @@ require "time" require "utils/analytics" -require "utils/bottle_api" require "utils/curl" require "utils/fork" require "utils/formatter" diff --git a/Library/Homebrew/utils/bottle_api.rb b/Library/Homebrew/utils/bottle_api.rb deleted file mode 100644 index 838367f57b..0000000000 --- a/Library/Homebrew/utils/bottle_api.rb +++ /dev/null @@ -1,106 +0,0 @@ -# typed: true -# frozen_string_literal: true - -require "github_packages" - -module Utils - # Helper functions for using the Bottle JSON API. - # - # @api private - module BottleAPI - extend T::Sig - - module_function - - FORMULAE_BREW_SH_BOTTLE_API_DOMAIN = if OS.mac? - "https://formulae.brew.sh/api/bottle" - else - "https://formulae.brew.sh/api/bottle-linux" - end.freeze - - FORMULAE_BREW_SH_VERSIONS_API_URL = if OS.mac? - "https://formulae.brew.sh/api/versions-formulae.json" - else - "https://formulae.brew.sh/api/versions-linux.json" - end.freeze - - GITHUB_PACKAGES_SHA256_REGEX = %r{#{GitHubPackages::URL_REGEX}.*/blobs/sha256:(?\h{64})$}.freeze - - sig { params(name: String).returns(Hash) } - def fetch(name) - return @cache[name] if @cache.present? && @cache.key?(name) - - api_url = "#{FORMULAE_BREW_SH_BOTTLE_API_DOMAIN}/#{name}.json" - output = Utils::Curl.curl_output("--fail", api_url) - raise ArgumentError, "No JSON file found at #{Tty.underline}#{api_url}#{Tty.reset}" unless output.success? - - @cache ||= {} - @cache[name] = JSON.parse(output.stdout) - rescue JSON::ParserError - raise ArgumentError, "Invalid JSON file: #{Tty.underline}#{api_url}#{Tty.reset}" - end - - sig { params(name: String).returns(PkgVersion) } - def latest_pkg_version(name) - @formula_versions ||= begin - output = Utils::Curl.curl_output("--fail", FORMULAE_BREW_SH_VERSIONS_API_URL) - JSON.parse(output.stdout) - end - - return unless @formula_versions.key? name - - PkgVersion.new(@formula_versions[name]["version"], @formula_versions[name]["revision"]) - end - - sig { params(name: String).returns(T::Boolean) } - def bottle_available?(name) - fetch name - true - rescue ArgumentError - false - end - - sig { params(name: String).void } - def fetch_bottles(name) - hash = fetch(name) - bottle_tag = Utils::Bottles.tag.to_s - - odie "No bottle available for current OS" unless hash["bottles"].key? bottle_tag - - download_bottle(hash, bottle_tag) - - hash["dependencies"].each do |dep_hash| - download_bottle(dep_hash, bottle_tag) - end - end - - sig { params(url: String).returns(T.nilable(String)) } - def checksum_from_url(url) - match = url.match GITHUB_PACKAGES_SHA256_REGEX - return if match.blank? - - match[:sha256] - end - - sig { params(hash: Hash, tag: Symbol).void } - def download_bottle(hash, tag) - bottle = hash["bottles"][tag] - return if bottle.blank? - - sha256 = bottle["sha256"] || checksum_from_url(bottle["url"]) - bottle_filename = Bottle::Filename.new(hash["name"], hash["pkg_version"], tag, hash["rebuild"]) - - resource = Resource.new hash["name"] - resource.url bottle["url"] - resource.sha256 sha256 - resource.version hash["pkg_version"] - resource.downloader.resolved_basename = bottle_filename - - resource.fetch - - # Map the name of this formula to the local bottle path to allow the - # formula to be loaded by passing just the name to `Formulary::factory`. - Formulary.map_formula_name_to_local_bottle_path hash["name"], resource.downloader.cached_location - end - end -end diff --git a/Library/Homebrew/utils/bottle_api.rbi b/Library/Homebrew/utils/bottle_api.rbi deleted file mode 100644 index 403c511f59..0000000000 --- a/Library/Homebrew/utils/bottle_api.rbi +++ /dev/null @@ -1,7 +0,0 @@ -# typed: strict - -module Utils - module BottleAPI - include Kernel - end -end