diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 4b208fd0ec..e4028b2678 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -2,7 +2,6 @@ # frozen_string_literal: true require "api/analytics" -require "api/bottle" require "api/cask" require "api/cask-source" require "api/formula" @@ -21,6 +20,7 @@ module Homebrew module_function API_DOMAIN = "https://formulae.brew.sh/api" + HOMEBREW_CACHE_API = (HOMEBREW_CACHE/"api").freeze sig { params(endpoint: String, json: T::Boolean).returns(T.any(String, Hash)) } def fetch(endpoint, json: true) diff --git a/Library/Homebrew/api/bottle.rb b/Library/Homebrew/api/bottle.rb deleted file mode 100644 index f7327bdf10..0000000000 --- a/Library/Homebrew/api/bottle.rb +++ /dev/null @@ -1,96 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "github_packages" - -module Homebrew - module API - # Helper functions for using the bottle JSON API. - # - # @api private - module Bottle - class << self - extend T::Sig - - sig { returns(String) } - def bottle_api_path - "bottle" - end - alias generic_bottle_api_path bottle_api_path - - GITHUB_PACKAGES_SHA256_REGEX = %r{#{GitHubPackages::URL_REGEX}.*/blobs/sha256:(?\h{64})$}.freeze - - sig { params(name: String).returns(Hash) } - def fetch(name) - name = name.sub(%r{^homebrew/core/}, "") - Homebrew::API.fetch "#{bottle_api_path}/#{name}.json" - end - - sig { params(name: String).returns(T::Boolean) } - def 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 - - if !hash["bottles"].key?(bottle_tag) && !hash["bottles"].key?("all") - odie "No bottle available for #{name} on the current OS" - end - - download_bottle(hash, bottle_tag) - - hash["dependencies"].each do |dep_hash| - existing_formula = begin - Formulary.factory dep_hash["name"] - rescue FormulaUnavailableError - # The formula might not exist if it's not installed and homebrew/core isn't tapped - nil - end - - next if existing_formula.present? && existing_formula.latest_version_installed? - - 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: String).void } - def download_bottle(hash, tag) - bottle = hash["bottles"][tag] - bottle ||= hash["bottles"]["all"] - 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`. - [hash["name"], "homebrew/core/#{hash["name"]}"].each do |name| - Formulary.map_formula_name_to_local_bottle_path name, resource.downloader.cached_location - end - end - end - end - end -end diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index 5e0774230b..a70fe31b8c 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -16,10 +16,34 @@ module Homebrew end alias generic_formula_api_path formula_api_path + sig { returns(String) } + def cached_formula_json_file + HOMEBREW_CACHE_API/"#{formula_api_path}.json" + end + sig { params(name: String).returns(Hash) } def fetch(name) Homebrew::API.fetch "#{formula_api_path}/#{name}.json" end + + sig { returns(Array) } + def all_formulae + @all_formulae ||= begin + curl_args = %w[--compressed --silent https://formulae.brew.sh/api/formula.json] + if cached_formula_json_file.exist? + last_modified = cached_formula_json_file.mtime.utc + last_modified = last_modified.strftime("%a, %d %b %Y %H:%M:%S GMT") + curl_args = ["--header", "If-Modified-Since: #{last_modified}", *curl_args] + end + curl_download(*curl_args, to: HOMEBREW_CACHE_API/"#{formula_api_path}.json", max_time: 5) + + json_formulae = JSON.parse(cached_formula_json_file.read) + + json_formulae.to_h do |json_formula| + [json_formula["name"], json_formula.except("name")] + end + end + end end end end diff --git a/Library/Homebrew/brew.sh b/Library/Homebrew/brew.sh index e22cc6c789..3e5bd2cedf 100644 --- a/Library/Homebrew/brew.sh +++ b/Library/Homebrew/brew.sh @@ -764,7 +764,7 @@ then export HOMEBREW_DEVELOPER_MODE="1" fi -if [[ -n "${HOMEBREW_INSTALL_FROM_API}" && -n "${HOMEBREW_DEVELOPER_COMMAND}" ]] +if [[ -n "${HOMEBREW_INSTALL_FROM_API}" && -n "${HOMEBREW_DEVELOPER_COMMAND}" && "${HOMEBREW_COMMAND}" != "irb" ]] then odie "Developer commands cannot be run while HOMEBREW_INSTALL_FROM_API is set!" elif [[ -n "${HOMEBREW_INSTALL_FROM_API}" && -n "${HOMEBREW_DEVELOPER_MODE}" ]] diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index c25726316a..85a59c0582 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -6,7 +6,6 @@ require "cask/config" require "cask/dsl" require "cask/metadata" require "searchable" -require "api" module Cask # An instance of a cask. @@ -166,14 +165,7 @@ module Cask # special case: tap version is not available return [] if version.nil? - latest_version = if Homebrew::EnvConfig.install_from_api? && - (latest_cask_version = Homebrew::API::Versions.latest_cask_version(token)) - DSL::Version.new latest_cask_version.to_s - else - version - end - - if latest_version.latest? + if version.latest? return versions if (greedy || greedy_latest) && outdated_download_sha? return [] @@ -185,10 +177,10 @@ module Cask current = installed.last # not outdated unless there is a different version on tap - return [] if current == latest_version + return [] if current == version # collect all installed versions that are different than tap version and return them - installed.reject { |v| v == latest_version } + installed.reject { |v| v == version } end def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates) diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 0b231fcbc9..5116aadd9f 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -214,7 +214,15 @@ module Cask FromTapPathLoader, FromPathLoader, ].each do |loader_class| - return loader_class.new(ref) if loader_class.can_load?(ref) + next unless loader_class.can_load?(ref) + + if loader_class == FromTapLoader && Homebrew::EnvConfig.install_from_api? && + ref.start_with?("homebrew/cask/") && !Tap.fetch("homebrew/cask").installed? && + Homebrew::API::CaskSource.available?(ref) + return FromContentLoader.new(Homebrew::API::CaskSource.fetch(ref)) + end + + return loader_class.new(ref) end return FromTapPathLoader.new(default_path(ref)) if FromTapPathLoader.can_load?(default_path(ref)) @@ -231,6 +239,10 @@ module Cask EOS end + if Homebrew::EnvConfig.install_from_api? && Homebrew::API::CaskSource.available?(ref) + return FromContentLoader.new(Homebrew::API::CaskSource.fetch(ref)) + end + possible_installed_cask = Cask.new(ref) return FromPathLoader.new(possible_installed_cask.installed_caskfile) if possible_installed_cask.installed? diff --git a/Library/Homebrew/cask/caskroom.rb b/Library/Homebrew/cask/caskroom.rb index 29558f7476..7fe5fc5461 100644 --- a/Library/Homebrew/cask/caskroom.rb +++ b/Library/Homebrew/cask/caskroom.rb @@ -49,7 +49,8 @@ module Cask begin if (tap_path = CaskLoader.tap_paths(token).first) CaskLoader::FromTapPathLoader.new(tap_path).load(config: config) - elsif (caskroom_path = Pathname.glob(path.join(".metadata/*/*/*/*.rb")).first) + elsif (caskroom_path = Pathname.glob(path.join(".metadata/*/*/*/*.rb")).first) && + (!Homebrew::EnvConfig.install_from_api? || !Homebrew::API::CaskSource.available?(token)) CaskLoader::FromPathLoader.new(caskroom_path).load(config: config) else CaskLoader.load(token, config: config) diff --git a/Library/Homebrew/cask_dependent.rb b/Library/Homebrew/cask_dependent.rb index e1f3f6bd34..cb9841fb8b 100644 --- a/Library/Homebrew/cask_dependent.rb +++ b/Library/Homebrew/cask_dependent.rb @@ -45,8 +45,8 @@ class CaskDependent end end - def recursive_dependencies(ignore_missing: false, &block) - Dependency.expand(self, ignore_missing: ignore_missing, &block) + def recursive_dependencies(&block) + Dependency.expand(self, &block) end def recursive_requirements(&block) diff --git a/Library/Homebrew/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 9d484a141d..e7337d2cf8 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -45,18 +45,16 @@ module Homebrew # the formula and prints a warning unless `only` is specified. sig { params( - only: T.nilable(Symbol), - ignore_unavailable: T.nilable(T::Boolean), - method: T.nilable(Symbol), - uniq: T::Boolean, - prefer_loading_from_api: T::Boolean, + only: T.nilable(Symbol), + ignore_unavailable: T.nilable(T::Boolean), + method: T.nilable(Symbol), + uniq: T::Boolean, ).returns(T::Array[T.any(Formula, Keg, Cask::Cask)]) } - def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true, - prefer_loading_from_api: false) + def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true) @to_formulae_and_casks ||= {} @to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name| - load_formula_or_cask(name, only: only, method: method, prefer_loading_from_api: prefer_loading_from_api) + load_formula_or_cask(name, only: only, method: method) rescue FormulaUnreadableError, FormulaClassUnavailableError, TapFormulaUnreadableError, TapFormulaClassUnavailableError, Cask::CaskUnreadableError @@ -90,15 +88,10 @@ module Homebrew end.uniq.freeze end - def load_formula_or_cask(name, only: nil, method: nil, prefer_loading_from_api: false) + def load_formula_or_cask(name, only: nil, method: nil) unreadable_error = nil if only != :cask - if prefer_loading_from_api && Homebrew::EnvConfig.install_from_api? && - Homebrew::API::Bottle.available?(name) - Homebrew::API::Bottle.fetch_bottles(name) - end - begin formula = case method when nil, :factory @@ -129,16 +122,11 @@ module Homebrew end if only != :formula - if prefer_loading_from_api && Homebrew::EnvConfig.install_from_api? && - Homebrew::API::CaskSource.available?(name) - contents = Homebrew::API::CaskSource.fetch(name) - end - want_keg_like_cask = [:latest_kegs, :default_kegs, :kegs].include?(method) begin config = Cask::Config.from_args(@parent) if @cask_options - cask = Cask::CaskLoader.load(contents || name, config: config) + cask = Cask::CaskLoader.load(name, config: config) if unreadable_error.present? onoe <<~EOS diff --git a/Library/Homebrew/cmd/fetch.rb b/Library/Homebrew/cmd/fetch.rb index daba7dbfef..4d68457bde 100644 --- a/Library/Homebrew/cmd/fetch.rb +++ b/Library/Homebrew/cmd/fetch.rb @@ -66,26 +66,18 @@ module Homebrew args = fetch_args.parse bucket = if args.deps? - args.named.to_formulae_and_casks(prefer_loading_from_api: true).flat_map do |formula_or_cask| + args.named.to_formulae_and_casks.flat_map do |formula_or_cask| case formula_or_cask when Formula f = formula_or_cask - deps = if Homebrew::EnvConfig.install_from_api? - f.recursive_dependencies do |_, dependency| - Dependency.prune if EnvConfig.install_from_api? && (dependency.build? || dependency.test?) - end - else - f.recursive_dependencies - end - - [f, *deps.map(&:to_formula)] + [f, *f.recursive_dependencies.map(&:to_formula)] else formula_or_cask end end else - args.named.to_formulae_and_casks(prefer_loading_from_api: true) + args.named.to_formulae_and_casks end.uniq puts "Fetching: #{bucket * ", "}" if bucket.size > 1 diff --git a/Library/Homebrew/cmd/info.rb b/Library/Homebrew/cmd/info.rb index a541b36ef1..a9e489bc4f 100644 --- a/Library/Homebrew/cmd/info.rb +++ b/Library/Homebrew/cmd/info.rb @@ -252,16 +252,7 @@ module Homebrew def info_formula(f, args:) specs = [] - if Homebrew::EnvConfig.install_from_api? && Homebrew::API::Bottle.available?(f.name) - info = Homebrew::API::Bottle.fetch(f.name) - - latest_version = info["pkg_version"].split("_").first - bottle_exists = info["bottles"].key?(Utils::Bottles.tag.to_s) || info["bottles"].key?("all") - - s = "stable #{latest_version}" - s += " (bottled)" if bottle_exists - specs << s - elsif (stable = f.stable) + if (stable = f.stable) s = "stable #{stable.version}" s += " (bottled)" if stable.bottled? && f.pour_bottle? specs << s diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index ae56fc16d6..4e817276d2 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -167,7 +167,7 @@ module Homebrew end begin - formulae, casks = args.named.to_formulae_and_casks(prefer_loading_from_api: true) + formulae, casks = args.named.to_formulae_and_casks .partition { |formula_or_cask| formula_or_cask.is_a?(Formula) } rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e retry if Tap.install_default_cask_tap_if_necessary(force: args.cask?) diff --git a/Library/Homebrew/cmd/outdated.rb b/Library/Homebrew/cmd/outdated.rb index 568bf925ae..933de05583 100644 --- a/Library/Homebrew/cmd/outdated.rb +++ b/Library/Homebrew/cmd/outdated.rb @@ -98,10 +98,7 @@ module Homebrew if verbose? outdated_kegs = f.outdated_kegs(fetch_head: args.fetch_HEAD?) - current_version = if !f.head? && Homebrew::EnvConfig.install_from_api? && - (f.core_formula? || f.tap.blank?) - Homebrew::API::Versions.latest_formula_version(f.name)&.to_s || f.pkg_version.to_s - elsif f.alias_changed? && !f.latest_formula.latest_version_installed? + current_version = if f.alias_changed? && !f.latest_formula.latest_version_installed? latest = f.latest_formula "#{latest.name} (#{latest.pkg_version})" elsif f.head? && outdated_kegs.any? { |k| k.version.to_s == f.pkg_version.to_s } diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index e4b692afab..fa0f68f0b4 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -88,22 +88,6 @@ module Homebrew def reinstall args = reinstall_args.parse - # We need to use the bottle API instead of just using the formula file - # from an installed keg because it will not contain bottle information. - # As a consequence, `brew reinstall` will also upgrade outdated formulae - if Homebrew::EnvConfig.install_from_api? - args.named.each do |name| - formula = Formulary.factory(name) - next unless formula.any_version_installed? - next if formula.tap.present? && !formula.core_formula? - next unless Homebrew::API::Bottle.available?(name) - - Homebrew::API::Bottle.fetch_bottles(name) - rescue FormulaUnavailableError - next - end - end - formulae, casks = args.named.to_formulae_and_casks(method: :resolve) .partition { |o| o.is_a?(Formula) } diff --git a/Library/Homebrew/cmd/update.sh b/Library/Homebrew/cmd/update.sh index 551a063257..19285aa4f8 100644 --- a/Library/Homebrew/cmd/update.sh +++ b/Library/Homebrew/cmd/update.sh @@ -745,6 +745,20 @@ EOS fi done + if [[ -n "${HOMEBREW_INSTALL_FROM_API}" ]] + then + mkdir -p "${HOMEBREW_CACHE}/api" + # TODO: use --header If-Modified-Since + curl \ + "${CURL_DISABLE_CURLRC_ARGS[@]}" \ + --fail --compressed --silent --max-time 5 \ + --location --remote-time --output "${HOMEBREW_CACHE}/api/formula.json" \ + --user-agent "${HOMEBREW_USER_AGENT_CURL}" \ + "https://formulae.brew.sh/api/formula.json" + # TODO: we probably want to print an error if this fails. + # TODO: set HOMEBREW_UPDATED or HOMEBREW_UPDATE_FAILED + fi + safe_cd "${HOMEBREW_REPOSITORY}" # HOMEBREW_UPDATE_AUTO wasn't modified in subshell. diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index b7fa29b427..c9d7c007c8 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -161,19 +161,6 @@ module Homebrew puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", " end - if Homebrew::EnvConfig.install_from_api? - formulae_to_install.map! do |formula| - next formula if formula.head? - next formula if formula.tap.present? && !formula.core_formula? - next formula unless Homebrew::API::Bottle.available?(formula.name) - - Homebrew::API::Bottle.fetch_bottles(formula.name) - Formulary.factory(formula.name) - rescue FormulaUnavailableError - formula - end - end - if formulae_to_install.empty? oh1 "No packages to upgrade" else @@ -226,15 +213,6 @@ module Homebrew def upgrade_outdated_casks(casks, args:) return false if args.formula? - if Homebrew::EnvConfig.install_from_api? - casks = casks.map do |cask| - next cask if cask.tap.present? && cask.tap != "homebrew/cask" - next cask unless Homebrew::API::CaskSource.available?(cask.token) - - Cask::CaskLoader.load Homebrew::API::CaskSource.fetch(cask.token) - end - end - Cask::Cmd::Upgrade.upgrade_casks( *casks, force: args.force?, diff --git a/Library/Homebrew/dependency.rb b/Library/Homebrew/dependency.rb index 2b3f9b2013..7ac088dc16 100644 --- a/Library/Homebrew/dependency.rb +++ b/Library/Homebrew/dependency.rb @@ -46,15 +46,6 @@ class Dependency formula end - def unavailable_core_formula? - to_formula - false - rescue CoreTapFormulaUnavailableError - true - rescue - false - end - def installed? to_formula.latest_version_installed? end @@ -98,7 +89,7 @@ class Dependency # the list. # The default filter, which is applied when a block is not given, omits # optionals and recommendeds based on what the dependent has asked for - def expand(dependent, deps = dependent.deps, cache_key: nil, ignore_missing: false, &block) + def expand(dependent, deps = dependent.deps, cache_key: nil, &block) # Keep track dependencies to avoid infinite cyclic dependency recursion. @expand_stack ||= [] @expand_stack.push dependent.name @@ -112,22 +103,20 @@ class Dependency deps.each do |dep| next if dependent.name == dep.name - # avoid downloading build dependency bottles - next if dep.build? && dependent.pour_bottle? && Homebrew::EnvConfig.install_from_api? - case action(dependent, dep, ignore_missing: ignore_missing, &block) + case action(dependent, dep, &block) when :prune next when :skip next if @expand_stack.include? dep.name - expanded_deps.concat(expand(dep.to_formula, cache_key: cache_key, ignore_missing: ignore_missing, &block)) + expanded_deps.concat(expand(dep.to_formula, cache_key: cache_key, &block)) when :keep_but_prune_recursive_deps expanded_deps << dep else next if @expand_stack.include? dep.name - expanded_deps.concat(expand(dep.to_formula, cache_key: cache_key, ignore_missing: ignore_missing, &block)) + expanded_deps.concat(expand(dep.to_formula, cache_key: cache_key, &block)) expanded_deps << dep end end @@ -139,10 +128,8 @@ class Dependency @expand_stack.pop end - def action(dependent, dep, ignore_missing: false, &block) + def action(dependent, dep, &block) catch(:action) do - prune if ignore_missing && dep.unavailable_core_formula? - if block yield dependent, dep elsif dep.optional? || dep.recommended? diff --git a/Library/Homebrew/diagnostic.rb b/Library/Homebrew/diagnostic.rb index d262d3541d..6d1eee6e41 100644 --- a/Library/Homebrew/diagnostic.rb +++ b/Library/Homebrew/diagnostic.rb @@ -890,7 +890,7 @@ module Homebrew # Formulae installed with HOMEBREW_INSTALL_FROM_API should not count as deleted formulae # but may not have a tap listed in their tab tap = Tab.for_keg(keg).tap - next if (tap.blank? || tap.core_tap?) && Homebrew::API::Bottle.available?(keg.name) + next if (tap.blank? || tap.core_tap?) && Homebrew::API::Formula.all_formulae.key?(keg.name) end keg.name diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 4e1d0387f9..6f83563226 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -234,13 +234,6 @@ class TapFormulaUnavailableError < FormulaUnavailableError end end -# Raised when a formula in a the core tap is unavailable. -class CoreTapFormulaUnavailableError < TapFormulaUnavailableError - def initialize(name) - super CoreTap.instance, name - end -end - # Raised when a formula in a specific tap does not contain a formula class. class TapFormulaClassUnavailableError < TapFormulaUnavailableError include FormulaClassUnavailableErrorModule diff --git a/Library/Homebrew/extend/os/linux/keg_relocate.rb b/Library/Homebrew/extend/os/linux/keg_relocate.rb index b76f3cb15c..4fafbbd6fa 100644 --- a/Library/Homebrew/extend/os/linux/keg_relocate.rb +++ b/Library/Homebrew/extend/os/linux/keg_relocate.rb @@ -84,16 +84,9 @@ class Keg def self.bottle_dependencies @bottle_dependencies ||= begin formulae = relocation_formulae - if Homebrew::EnvConfig.install_from_api? - gcc_hash = Homebrew::API::Formula.fetch(CompilerSelector.preferred_gcc) - preferred_gcc_version = Version.new gcc_hash["versions"]["stable"] - else - gcc = Formulary.factory(CompilerSelector.preferred_gcc) - preferred_gcc_version = gcc.version - end + gcc = Formulary.factory(CompilerSelector.preferred_gcc) if !Homebrew::EnvConfig.simulate_macos_on_linux? && - DevelopmentTools.non_apple_gcc_version("gcc") < preferred_gcc_version.major - gcc = Formulary.factory(CompilerSelector.preferred_gcc) if Homebrew::EnvConfig.install_from_api? + DevelopmentTools.non_apple_gcc_version("gcc") < gcc.version.to_i formulae += gcc.recursive_dependencies.map(&:name) formulae << gcc.name end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index e1d43818a3..ee2cae9dd0 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -525,14 +525,7 @@ class Formula # exists and is not empty. # @private def latest_version_installed? - latest_prefix = if !head? && Homebrew::EnvConfig.install_from_api? && - (latest_pkg_version = Homebrew::API::Versions.latest_formula_version(name)) - prefix latest_pkg_version - else - latest_installed_prefix - end - - (dir = latest_prefix).directory? && !dir.children.empty? + (dir = latest_installed_prefix).directory? && !dir.children.empty? end # If at least one version of {Formula} is installed. @@ -1352,11 +1345,6 @@ class Formula Formula.cache[:outdated_kegs][cache_key] ||= begin all_kegs = [] current_version = T.let(false, T::Boolean) - latest_version = if !head? && Homebrew::EnvConfig.install_from_api? && (core_formula? || tap.blank?) - Homebrew::API::Versions.latest_formula_version(name) || pkg_version - else - pkg_version - end installed_kegs.each do |keg| all_kegs << keg @@ -1364,8 +1352,8 @@ class Formula next if version.head? tab = Tab.for_keg(keg) - next if version_scheme > tab.version_scheme && latest_version != version - next if version_scheme == tab.version_scheme && latest_version > version + next if version_scheme > tab.version_scheme && pkg_version != version + next if version_scheme == tab.version_scheme && pkg_version > version # don't consider this keg current if there's a newer formula available next if follow_installed_alias? && new_formula_available? diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 149c501ddc..53b72b9428 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -218,11 +218,6 @@ class FormulaInstaller def verify_deps_exist begin compute_dependencies - rescue CoreTapFormulaUnavailableError => e - raise unless Homebrew::API::Bottle.available? e.name - - Homebrew::API::Bottle.fetch_bottles(e.name) - retry rescue TapFormulaUnavailableError => e raise if e.tap.installed? diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 4889b5785e..4bc7f65c8b 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -6,6 +6,8 @@ require "extend/cachable" require "tab" require "utils/bottles" +require "active_support/core_ext/hash/deep_transform_values" + # The {Formulary} is responsible for creating instances of {Formula}. # It is not meant to be used directly from formulae. # @@ -26,22 +28,32 @@ module Formulary !@factory_cache.nil? end - def self.formula_class_defined?(path) - cache.key?(path) + def self.formula_class_defined_from_path?(path) + cache.key?(:path) && cache[:path].key?(path) end - def self.formula_class_get(path) - cache.fetch(path) + def self.formula_class_defined_from_api?(name) + cache.key?(:api) && cache[:api].key?(name) + end + + def self.formula_class_get_from_path(path) + cache[:path].fetch(path) + end + + def self.formula_class_get_from_api(name) + cache[:api].fetch(name) end def self.clear_cache - cache.each do |key, klass| - next if key == :formulary_factory + cache.each do |type, cached_objects| + next if type == :formulary_factory - namespace = klass.name.deconstantize - next if namespace.deconstantize != name + cached_objects.each_value do |klass| + namespace = klass.name.deconstantize + next if namespace.deconstantize != name - remove_const(namespace.demodulize) + remove_const(namespace.demodulize) + end end super @@ -108,7 +120,95 @@ module Formulary contents = path.open("r") { |f| ensure_utf8_encoding(f).read } namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}" klass = load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors) - cache[path] = klass + cache[:path] ||= {} + cache[:path][path] = klass + end + + def self.load_formula_from_api(name, flags:) + namespace = "FormulaNamespaceAPI#{Digest::MD5.hexdigest(name)}" + + mod = Module.new + remove_const(namespace) if const_defined?(namespace) + const_set(namespace, mod) + + mod.const_set(:BUILD_FLAGS, flags) + + class_s = Formulary.class_s(name) + json_formula = Homebrew::API::Formula.all_formulae[name] + + klass = Class.new(::Formula) do + desc json_formula["desc"] + homepage json_formula["homepage"] + license json_formula["license"] + revision json_formula["revision"] + version_scheme json_formula["version_scheme"] + + if (urls_stable = json_formula["urls"]["stable"]).present? + stable do + url urls_stable["url"] + version json_formula["versions"]["stable"] + end + end + + if (bottles_stable = json_formula["bottle"]["stable"]).present? + bottle do + root_url bottles_stable["root_url"] + rebuild bottles_stable["rebuild"] + bottles_stable["files"].each do |tag, bottle_spec| + cellar = Formulary.convert_to_string_or_symbol bottle_spec["cellar"] + sha256 cellar: cellar, tag.to_sym => bottle_spec["sha256"] + end + end + end + + if (keg_only_reason = json_formula["keg_only_reason"]).present? + reason = Formulary.convert_to_string_or_symbol keg_only_reason["reason"] + keg_only reason, keg_only_reason["explanation"] + end + + if (deprecation_date = json_formula["deprecation_date"]).present? + deprecate! date: deprecation_date, because: json_formula["deprecation_reason"] + end + + if (disable_date = json_formula["disable_date"]).present? + disable! date: disable_date, because: json_formula["disable_reason"] + end + + json_formula["build_dependencies"].each do |dep| + depends_on dep => :build + end + + json_formula["dependencies"].each do |dep| + depends_on dep + end + + json_formula["recommended_dependencies"].each do |dep| + depends_on dep => :recommended + end + + json_formula["optional_dependencies"].each do |dep| + depends_on dep => :optional + end + + json_formula["uses_from_macos"].each do |dep| + dep = dep.deep_transform_values(&:to_sym) if dep.is_a?(Hash) + uses_from_macos dep + end + + def install + raise "Cannot build from source from abstract formula." + end + + @caveats_string = json_formula["caveats"] + def caveats + @caveats_string + end + end + + mod.const_set(class_s, klass) + + cache[:api] ||= {} + cache[:api][name] = klass end def self.resolve(name, spec: nil, force_bottle: false, flags: []) @@ -155,6 +255,12 @@ module Formulary class_name end + def self.convert_to_string_or_symbol(string) + return string[1..].to_sym if string.start_with?(":") + + string + end + # A {FormulaLoader} returns instances of formulae. # Subclasses implement loaders for particular sources of formulae. class FormulaLoader @@ -182,8 +288,8 @@ module Formulary end def klass(flags:, ignore_errors:) - load_file(flags: flags, ignore_errors: ignore_errors) unless Formulary.formula_class_defined?(path) - Formulary.formula_class_get(path) + load_file(flags: flags, ignore_errors: ignore_errors) unless Formulary.formula_class_defined_from_path?(path) + Formulary.formula_class_get_from_path(path) end private @@ -345,10 +451,6 @@ module Formulary rescue FormulaClassUnavailableError => e raise TapFormulaClassUnavailableError.new(tap, name, e.path, e.class_name, e.class_list), "", e.backtrace rescue FormulaUnavailableError => e - if tap.core_tap? && Homebrew::EnvConfig.install_from_api? - raise CoreTapFormulaUnavailableError.new(name), "", e.backtrace - end - raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace end @@ -367,10 +469,6 @@ module Formulary end def get_formula(*) - if !CoreTap.instance.installed? && Homebrew::EnvConfig.install_from_api? - raise CoreTapFormulaUnavailableError, name - end - raise FormulaUnavailableError, name end end @@ -392,6 +490,26 @@ module Formulary end end + # Load formulae from the API. + class FormulaAPILoader < FormulaLoader + def initialize(name) + super name, Formulary.core_path(name) + end + + def klass(flags:, ignore_errors:) + load_from_api(flags: flags) unless Formulary.formula_class_defined_from_api?(name) + Formulary.formula_class_get_from_api(name) + end + + private + + def load_from_api(flags:) + $stderr.puts "#{$PROGRAM_NAME} (#{self.class.name}): loading #{name} from API" if debug? + + Formulary.load_formula_from_api(name, flags: flags) + end + end + # Return a {Formula} instance for the given reference. # `ref` is a string containing: # @@ -405,12 +523,6 @@ module Formulary ) raise ArgumentError, "Formulae must have a ref!" unless ref - if Homebrew::EnvConfig.install_from_api? && - @formula_name_local_bottle_path_map.present? && - @formula_name_local_bottle_path_map.key?(ref) - ref = @formula_name_local_bottle_path_map[ref] - end - cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}" if factory_cached? && cache[:formulary_factory] && cache[:formulary_factory][cache_key] @@ -427,24 +539,6 @@ module Formulary formula end - # Map a formula name to a local/fetched bottle archive. This mapping will be used by {Formulary::factory} - # to allow formulae to be loaded automatically from their local bottle archive without - # needing to exist in a tap or be passed as a complete path. For example, - # to map `hello` from its bottle archive: - #
Formulary.map_formula_name_to_local_bottle_path "hello", HOMEBREW_CACHE/"hello--2.10"
-  # Formulary.factory "hello" # returns the hello formula from the local bottle archive
-  # 
- # @param formula_name the formula name string to map. - # @param local_bottle_path a path pointing to the target bottle archive. - def self.map_formula_name_to_local_bottle_path(formula_name, local_bottle_path) - unless Homebrew::EnvConfig.install_from_api? - raise UsageError, "HOMEBREW_INSTALL_FROM_API not set but required for #{__method__}!" - end - - @formula_name_local_bottle_path_map ||= {} - @formula_name_local_bottle_path_map[formula_name] = Pathname(local_bottle_path).realpath - end - # Return a {Formula} instance for the given rack. # # @param spec when nil, will auto resolve the formula's spec. @@ -539,11 +633,9 @@ module Formulary when URL_START_REGEX return FromUrlLoader.new(ref) when HOMEBREW_TAP_FORMULA_REGEX - # If `homebrew/core` is specified and not installed, check whether the formula is already installed. if ref.start_with?("homebrew/core/") && !CoreTap.instance.installed? && Homebrew::EnvConfig.install_from_api? name = ref.split("/", 3).last - possible_keg_formula = Pathname.new("#{HOMEBREW_PREFIX}/opt/#{name}/.brew/#{name}.rb") - return FormulaLoader.new(name, possible_keg_formula) if possible_keg_formula.file? + return FormulaAPILoader.new(name) if Homebrew::API::Formula.all_formulae.key?(name) end return TapLoader.new(ref, from: from) @@ -557,6 +649,12 @@ module Formulary possible_alias = CoreTap.instance.alias_dir/ref return AliasLoader.new(possible_alias) if possible_alias.file? + if !CoreTap.instance.installed? && + Homebrew::EnvConfig.install_from_api? && + Homebrew::API::Formula.all_formulae.key?(ref) + return FormulaAPILoader.new(ref) + end + possible_tap_formulae = tap_paths(ref) raise TapFormulaAmbiguityError.new(ref, possible_tap_formulae) if possible_tap_formulae.size > 1 diff --git a/Library/Homebrew/tap.rb b/Library/Homebrew/tap.rb index 173c13ce40..7a2fba9256 100644 --- a/Library/Homebrew/tap.rb +++ b/Library/Homebrew/tap.rb @@ -142,8 +142,6 @@ class Tap # The remote repository name of this {Tap}. # e.g. `user/homebrew-repo` def remote_repo - raise TapUnavailableError, name unless installed? - return unless remote @remote_repo ||= remote.delete_prefix("https://github.com/") @@ -795,6 +793,12 @@ class CoreTap < Tap safe_system HOMEBREW_BREW_FILE, "tap", instance.name end + def remote + super if installed? || !Homebrew::EnvConfig.install_from_api? + + Homebrew::EnvConfig.core_git_remote + end + # CoreTap never allows shallow clones (on request from GitHub). def install(quiet: false, clone_target: nil, force_auto_update: nil, custom_remote: false) remote = Homebrew::EnvConfig.core_git_remote # set by HOMEBREW_CORE_GIT_REMOTE diff --git a/Library/Homebrew/test/api/bottle_spec.rb b/Library/Homebrew/test/api/bottle_spec.rb deleted file mode 100644 index 3fe9e450a1..0000000000 --- a/Library/Homebrew/test/api/bottle_spec.rb +++ /dev/null @@ -1,95 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "api" - -describe Homebrew::API::Bottle do - let(:bottle_json) { - <<~EOS - { - "name": "hello", - "pkg_version": "2.10", - "rebuild": 0, - "bottles": { - "arm64_big_sur": { - "url": "https://ghcr.io/v2/homebrew/core/hello/blobs/sha256:b3b083db0807ff92c6e289a298f378198354b7727fb9ba9f4d550b8e08f90a60" - }, - "big_sur": { - "url": "https://ghcr.io/v2/homebrew/core/hello/blobs/sha256:69489ae397e4645127aa7773211310f81ebb6c99e1f8e3e22c5cdb55333f5408" - }, - "x86_64_linux": { - "url": "https://ghcr.io/v2/homebrew/core/hello/blobs/sha256:e6980196298e0a9cfe4fa4e328a71a1869a4d5e1d31c38442150ed784cfc0e29" - } - }, - "dependencies": [] - } - EOS - } - let(:bottle_hash) { JSON.parse(bottle_json) } - - def mock_curl_output(stdout: "", success: true) - curl_output = OpenStruct.new(stdout: stdout, success?: success) - allow(Utils::Curl).to receive(:curl_output).and_return curl_output - end - - describe "::fetch" do - it "fetches the bottle JSON for a formula that exists" do - mock_curl_output stdout: bottle_json - fetched_hash = described_class.fetch("foo") - expect(fetched_hash).to eq bottle_hash - end - - it "raises an error if the formula does not exist" do - mock_curl_output success: false - expect { described_class.fetch("bar") }.to raise_error(ArgumentError, /No file found/) - end - - it "raises an error if the bottle JSON is invalid" do - mock_curl_output stdout: "foo" - expect { described_class.fetch("baz") }.to raise_error(ArgumentError, /Invalid JSON file/) - end - end - - describe "::available?" do - it "returns `true` if `fetch` succeeds" do - allow(described_class).to receive(:fetch) - expect(described_class.available?("foo")).to be true - end - - it "returns `false` if `fetch` fails" do - allow(described_class).to receive(:fetch).and_raise ArgumentError - expect(described_class.available?("foo")).to be false - end - end - - describe "::fetch_bottles" do - before do - ENV["HOMEBREW_INSTALL_FROM_API"] = "1" - allow(described_class).to receive(:fetch).and_return bottle_hash - end - - it "fetches bottles if a bottle is available" do - allow(Utils::Bottles).to receive(:tag).and_return :arm64_big_sur - expect { described_class.fetch_bottles("hello") }.not_to raise_error - end - - it "raises an error if no bottle is available" do - allow(Utils::Bottles).to receive(:tag).and_return :catalina - expect { described_class.fetch_bottles("hello") }.to raise_error(SystemExit) - end - end - - describe "::checksum_from_url" do - let(:sha256) { "b3b083db0807ff92c6e289a298f378198354b7727fb9ba9f4d550b8e08f90a60" } - let(:url) { "https://ghcr.io/v2/homebrew/core/hello/blobs/sha256:#{sha256}" } - let(:non_ghp_url) { "https://formulae.brew.sh/api/formula/hello.json" } - - it "returns the `sha256` for a GitHub packages URL" do - expect(described_class.checksum_from_url(url)).to eq sha256 - end - - it "returns `nil` for a non-GitHub packages URL" do - expect(described_class.checksum_from_url(non_ghp_url)).to be_nil - end - end -end diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 78b0a413fc..b371c84c26 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -201,28 +201,107 @@ describe Formulary do }.to raise_error(TapFormulaAmbiguityError) end end - end - describe "::map_formula_name_to_local_bottle_path" do - before do - formula_path.dirname.mkpath - formula_path.write formula_content - end + context "when loading from the API" do + def formula_json_contents(extra_items = {}) + { + formula_name => { + "desc" => "testball", + "homepage" => "https://example.com", + "license" => "MIT", + "revision" => 0, + "version_scheme" => 0, + "versions" => { "stable" => "0.1" }, + "urls" => { + "stable" => { + "url" => "file://#{TEST_FIXTURE_DIR}/tarballs/testball-0.1.tbz", + "tag" => nil, + "revision" => nil, + }, + }, + "bottle" => { + "stable" => { + "rebuild" => 0, + "root_url" => "file://#{bottle_dir}", + "files" => { + Utils::Bottles.tag.to_s => { + "cellar" => ":any", + "url" => "file://#{bottle_dir}/#{formula_name}", + "sha256" => "8f9aecd233463da6a4ea55f5f88fc5841718c013f3e2a7941350d6130f1dc149", + }, + }, + }, + }, + "keg_only_reason" => { + "reason" => ":provided_by_macos", + "explanation" => "", + }, + "build_dependencies" => ["build_dep"], + "dependencies" => ["dep"], + "recommended_dependencies" => ["recommended_dep"], + "optional_dependencies" => ["optional_dep"], + "uses_from_macos" => ["uses_from_macos_dep"], + "caveats" => "", + }.merge(extra_items), + } + end - it "maps a reference to a new Formula" do - expect { - described_class.factory("formula-to-map") - }.to raise_error(FormulaUnavailableError) + let(:deprecate_json) do + { + "deprecation_date" => "2022-06-15", + "deprecation_reason" => "repo_archived", + } + end - ENV["HOMEBREW_INSTALL_FROM_API"] = nil - expect { - described_class.map_formula_name_to_local_bottle_path "formula-to-map", formula_path - }.to raise_error(UsageError, /HOMEBREW_INSTALL_FROM_API not set/) + let(:disable_json) do + { + "disable_date" => "2022-06-15", + "disable_reason" => "repo_archived", + } + end - ENV["HOMEBREW_INSTALL_FROM_API"] = "1" - described_class.map_formula_name_to_local_bottle_path "formula-to-map", formula_path + before do + allow(described_class).to receive(:loader_for).and_return(described_class::FormulaAPILoader.new(formula_name)) + end - expect(described_class.factory("formula-to-map")).to be_kind_of(Formula) + it "returns a Formula when given a name" do + allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents + + formula = described_class.factory(formula_name) + expect(formula).to be_kind_of(Formula) + expect(formula.keg_only_reason.reason).to eq :provided_by_macos + if OS.mac? + expect(formula.deps.count).to eq 4 + elsif OS.linux? + expect(formula.deps.count).to eq 5 + end + expect(formula.uses_from_macos_elements).to eq ["uses_from_macos_dep"] + expect { + formula.install + }.to raise_error("Cannot build from source from abstract formula.") + end + + it "returns a deprecated Formula when given a name" do + allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(deprecate_json) + + formula = described_class.factory(formula_name) + expect(formula).to be_kind_of(Formula) + expect(formula.deprecated?).to be true + expect { + formula.install + }.to raise_error("Cannot build from source from abstract formula.") + end + + it "returns a disabled Formula when given a name" do + allow(Homebrew::API::Formula).to receive(:all_formulae).and_return formula_json_contents(disable_json) + + formula = described_class.factory(formula_name) + expect(formula).to be_kind_of(Formula) + expect(formula.disabled?).to be true + expect { + formula.install + }.to raise_error("Cannot build from source from abstract formula.") + end end end