Merge pull request #12936 from Bo98/api-offline

Improve consistency between Git and API formula handling
This commit is contained in:
Rylan Polster 2022-06-16 16:11:38 -04:00 committed by GitHub
commit d23dba67ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 332 additions and 413 deletions

View File

@ -2,7 +2,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "api/analytics" require "api/analytics"
require "api/bottle"
require "api/cask" require "api/cask"
require "api/cask-source" require "api/cask-source"
require "api/formula" require "api/formula"
@ -21,6 +20,7 @@ module Homebrew
module_function module_function
API_DOMAIN = "https://formulae.brew.sh/api" 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)) } sig { params(endpoint: String, json: T::Boolean).returns(T.any(String, Hash)) }
def fetch(endpoint, json: true) def fetch(endpoint, json: true)

View File

@ -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:(?<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

View File

@ -16,10 +16,34 @@ module Homebrew
end end
alias generic_formula_api_path formula_api_path 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) } sig { params(name: String).returns(Hash) }
def fetch(name) def fetch(name)
Homebrew::API.fetch "#{formula_api_path}/#{name}.json" Homebrew::API.fetch "#{formula_api_path}/#{name}.json"
end 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 end
end end

View File

@ -764,7 +764,7 @@ then
export HOMEBREW_DEVELOPER_MODE="1" export HOMEBREW_DEVELOPER_MODE="1"
fi 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 then
odie "Developer commands cannot be run while HOMEBREW_INSTALL_FROM_API is set!" odie "Developer commands cannot be run while HOMEBREW_INSTALL_FROM_API is set!"
elif [[ -n "${HOMEBREW_INSTALL_FROM_API}" && -n "${HOMEBREW_DEVELOPER_MODE}" ]] elif [[ -n "${HOMEBREW_INSTALL_FROM_API}" && -n "${HOMEBREW_DEVELOPER_MODE}" ]]

View File

@ -6,7 +6,6 @@ require "cask/config"
require "cask/dsl" require "cask/dsl"
require "cask/metadata" require "cask/metadata"
require "searchable" require "searchable"
require "api"
module Cask module Cask
# An instance of a cask. # An instance of a cask.
@ -166,14 +165,7 @@ module Cask
# special case: tap version is not available # special case: tap version is not available
return [] if version.nil? return [] if version.nil?
latest_version = if Homebrew::EnvConfig.install_from_api? && if version.latest?
(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?
return versions if (greedy || greedy_latest) && outdated_download_sha? return versions if (greedy || greedy_latest) && outdated_download_sha?
return [] return []
@ -185,10 +177,10 @@ module Cask
current = installed.last current = installed.last
# not outdated unless there is a different version on tap # 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 # 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 end
def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates) def outdated_info(greedy, verbose, json, greedy_latest, greedy_auto_updates)

View File

@ -214,7 +214,15 @@ module Cask
FromTapPathLoader, FromTapPathLoader,
FromPathLoader, FromPathLoader,
].each do |loader_class| ].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 end
return FromTapPathLoader.new(default_path(ref)) if FromTapPathLoader.can_load?(default_path(ref)) return FromTapPathLoader.new(default_path(ref)) if FromTapPathLoader.can_load?(default_path(ref))
@ -231,6 +239,10 @@ module Cask
EOS EOS
end 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) possible_installed_cask = Cask.new(ref)
return FromPathLoader.new(possible_installed_cask.installed_caskfile) if possible_installed_cask.installed? return FromPathLoader.new(possible_installed_cask.installed_caskfile) if possible_installed_cask.installed?

View File

@ -49,7 +49,8 @@ module Cask
begin begin
if (tap_path = CaskLoader.tap_paths(token).first) if (tap_path = CaskLoader.tap_paths(token).first)
CaskLoader::FromTapPathLoader.new(tap_path).load(config: config) 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) CaskLoader::FromPathLoader.new(caskroom_path).load(config: config)
else else
CaskLoader.load(token, config: config) CaskLoader.load(token, config: config)

View File

@ -45,8 +45,8 @@ class CaskDependent
end end
end end
def recursive_dependencies(ignore_missing: false, &block) def recursive_dependencies(&block)
Dependency.expand(self, ignore_missing: ignore_missing, &block) Dependency.expand(self, &block)
end end
def recursive_requirements(&block) def recursive_requirements(&block)

View File

@ -45,18 +45,16 @@ module Homebrew
# the formula and prints a warning unless `only` is specified. # the formula and prints a warning unless `only` is specified.
sig { sig {
params( params(
only: T.nilable(Symbol), only: T.nilable(Symbol),
ignore_unavailable: T.nilable(T::Boolean), ignore_unavailable: T.nilable(T::Boolean),
method: T.nilable(Symbol), method: T.nilable(Symbol),
uniq: T::Boolean, uniq: T::Boolean,
prefer_loading_from_api: T::Boolean,
).returns(T::Array[T.any(Formula, Keg, Cask::Cask)]) ).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, def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true)
prefer_loading_from_api: false)
@to_formulae_and_casks ||= {} @to_formulae_and_casks ||= {}
@to_formulae_and_casks[only] ||= downcased_unique_named.flat_map do |name| @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, rescue FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError, TapFormulaUnreadableError, TapFormulaClassUnavailableError,
Cask::CaskUnreadableError Cask::CaskUnreadableError
@ -90,15 +88,10 @@ module Homebrew
end.uniq.freeze end.uniq.freeze
end 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 unreadable_error = nil
if only != :cask 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 begin
formula = case method formula = case method
when nil, :factory when nil, :factory
@ -129,16 +122,11 @@ module Homebrew
end end
if only != :formula 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) want_keg_like_cask = [:latest_kegs, :default_kegs, :kegs].include?(method)
begin begin
config = Cask::Config.from_args(@parent) if @cask_options 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? if unreadable_error.present?
onoe <<~EOS onoe <<~EOS

View File

@ -66,26 +66,18 @@ module Homebrew
args = fetch_args.parse args = fetch_args.parse
bucket = if args.deps? 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 case formula_or_cask
when Formula when Formula
f = formula_or_cask f = formula_or_cask
deps = if Homebrew::EnvConfig.install_from_api? [f, *f.recursive_dependencies.map(&:to_formula)]
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)]
else else
formula_or_cask formula_or_cask
end end
end end
else else
args.named.to_formulae_and_casks(prefer_loading_from_api: true) args.named.to_formulae_and_casks
end.uniq end.uniq
puts "Fetching: #{bucket * ", "}" if bucket.size > 1 puts "Fetching: #{bucket * ", "}" if bucket.size > 1

View File

@ -252,16 +252,7 @@ module Homebrew
def info_formula(f, args:) def info_formula(f, args:)
specs = [] specs = []
if Homebrew::EnvConfig.install_from_api? && Homebrew::API::Bottle.available?(f.name) if (stable = f.stable)
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)
s = "stable #{stable.version}" s = "stable #{stable.version}"
s += " (bottled)" if stable.bottled? && f.pour_bottle? s += " (bottled)" if stable.bottled? && f.pour_bottle?
specs << s specs << s

View File

@ -167,7 +167,7 @@ module Homebrew
end end
begin 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) } .partition { |formula_or_cask| formula_or_cask.is_a?(Formula) }
rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e rescue FormulaOrCaskUnavailableError, Cask::CaskUnavailableError => e
retry if Tap.install_default_cask_tap_if_necessary(force: args.cask?) retry if Tap.install_default_cask_tap_if_necessary(force: args.cask?)

View File

@ -98,10 +98,7 @@ module Homebrew
if verbose? if verbose?
outdated_kegs = f.outdated_kegs(fetch_head: args.fetch_HEAD?) outdated_kegs = f.outdated_kegs(fetch_head: args.fetch_HEAD?)
current_version = if !f.head? && Homebrew::EnvConfig.install_from_api? && current_version = if f.alias_changed? && !f.latest_formula.latest_version_installed?
(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?
latest = f.latest_formula latest = f.latest_formula
"#{latest.name} (#{latest.pkg_version})" "#{latest.name} (#{latest.pkg_version})"
elsif f.head? && outdated_kegs.any? { |k| k.version.to_s == f.pkg_version.to_s } elsif f.head? && outdated_kegs.any? { |k| k.version.to_s == f.pkg_version.to_s }

View File

@ -88,22 +88,6 @@ module Homebrew
def reinstall def reinstall
args = reinstall_args.parse 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) formulae, casks = args.named.to_formulae_and_casks(method: :resolve)
.partition { |o| o.is_a?(Formula) } .partition { |o| o.is_a?(Formula) }

View File

@ -745,6 +745,20 @@ EOS
fi fi
done 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}" safe_cd "${HOMEBREW_REPOSITORY}"
# HOMEBREW_UPDATE_AUTO wasn't modified in subshell. # HOMEBREW_UPDATE_AUTO wasn't modified in subshell.

View File

@ -161,19 +161,6 @@ module Homebrew
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", " puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
end 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? if formulae_to_install.empty?
oh1 "No packages to upgrade" oh1 "No packages to upgrade"
else else
@ -226,15 +213,6 @@ module Homebrew
def upgrade_outdated_casks(casks, args:) def upgrade_outdated_casks(casks, args:)
return false if args.formula? 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( Cask::Cmd::Upgrade.upgrade_casks(
*casks, *casks,
force: args.force?, force: args.force?,

View File

@ -46,15 +46,6 @@ class Dependency
formula formula
end end
def unavailable_core_formula?
to_formula
false
rescue CoreTapFormulaUnavailableError
true
rescue
false
end
def installed? def installed?
to_formula.latest_version_installed? to_formula.latest_version_installed?
end end
@ -98,7 +89,7 @@ class Dependency
# the list. # the list.
# The default filter, which is applied when a block is not given, omits # The default filter, which is applied when a block is not given, omits
# optionals and recommendeds based on what the dependent has asked for # 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. # Keep track dependencies to avoid infinite cyclic dependency recursion.
@expand_stack ||= [] @expand_stack ||= []
@expand_stack.push dependent.name @expand_stack.push dependent.name
@ -112,22 +103,20 @@ class Dependency
deps.each do |dep| deps.each do |dep|
next if dependent.name == dep.name 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 when :prune
next next
when :skip when :skip
next if @expand_stack.include? dep.name 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 when :keep_but_prune_recursive_deps
expanded_deps << dep expanded_deps << dep
else else
next if @expand_stack.include? dep.name 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 expanded_deps << dep
end end
end end
@ -139,10 +128,8 @@ class Dependency
@expand_stack.pop @expand_stack.pop
end end
def action(dependent, dep, ignore_missing: false, &block) def action(dependent, dep, &block)
catch(:action) do catch(:action) do
prune if ignore_missing && dep.unavailable_core_formula?
if block if block
yield dependent, dep yield dependent, dep
elsif dep.optional? || dep.recommended? elsif dep.optional? || dep.recommended?

View File

@ -890,7 +890,7 @@ module Homebrew
# Formulae installed with HOMEBREW_INSTALL_FROM_API should not count as deleted formulae # Formulae installed with HOMEBREW_INSTALL_FROM_API should not count as deleted formulae
# but may not have a tap listed in their tab # but may not have a tap listed in their tab
tap = Tab.for_keg(keg).tap 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 end
keg.name keg.name

View File

@ -234,13 +234,6 @@ class TapFormulaUnavailableError < FormulaUnavailableError
end end
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. # Raised when a formula in a specific tap does not contain a formula class.
class TapFormulaClassUnavailableError < TapFormulaUnavailableError class TapFormulaClassUnavailableError < TapFormulaUnavailableError
include FormulaClassUnavailableErrorModule include FormulaClassUnavailableErrorModule

View File

@ -84,16 +84,9 @@ class Keg
def self.bottle_dependencies def self.bottle_dependencies
@bottle_dependencies ||= begin @bottle_dependencies ||= begin
formulae = relocation_formulae formulae = relocation_formulae
if Homebrew::EnvConfig.install_from_api? gcc = Formulary.factory(CompilerSelector.preferred_gcc)
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
if !Homebrew::EnvConfig.simulate_macos_on_linux? && if !Homebrew::EnvConfig.simulate_macos_on_linux? &&
DevelopmentTools.non_apple_gcc_version("gcc") < preferred_gcc_version.major DevelopmentTools.non_apple_gcc_version("gcc") < gcc.version.to_i
gcc = Formulary.factory(CompilerSelector.preferred_gcc) if Homebrew::EnvConfig.install_from_api?
formulae += gcc.recursive_dependencies.map(&:name) formulae += gcc.recursive_dependencies.map(&:name)
formulae << gcc.name formulae << gcc.name
end end

View File

@ -525,14 +525,7 @@ class Formula
# exists and is not empty. # exists and is not empty.
# @private # @private
def latest_version_installed? def latest_version_installed?
latest_prefix = if !head? && Homebrew::EnvConfig.install_from_api? && (dir = latest_installed_prefix).directory? && !dir.children.empty?
(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?
end end
# If at least one version of {Formula} is installed. # If at least one version of {Formula} is installed.
@ -1352,11 +1345,6 @@ class Formula
Formula.cache[:outdated_kegs][cache_key] ||= begin Formula.cache[:outdated_kegs][cache_key] ||= begin
all_kegs = [] all_kegs = []
current_version = T.let(false, T::Boolean) 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| installed_kegs.each do |keg|
all_kegs << keg all_kegs << keg
@ -1364,8 +1352,8 @@ class Formula
next if version.head? next if version.head?
tab = Tab.for_keg(keg) tab = Tab.for_keg(keg)
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 && latest_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 # don't consider this keg current if there's a newer formula available
next if follow_installed_alias? && new_formula_available? next if follow_installed_alias? && new_formula_available?

View File

@ -218,11 +218,6 @@ class FormulaInstaller
def verify_deps_exist def verify_deps_exist
begin begin
compute_dependencies compute_dependencies
rescue CoreTapFormulaUnavailableError => e
raise unless Homebrew::API::Bottle.available? e.name
Homebrew::API::Bottle.fetch_bottles(e.name)
retry
rescue TapFormulaUnavailableError => e rescue TapFormulaUnavailableError => e
raise if e.tap.installed? raise if e.tap.installed?

View File

@ -6,6 +6,8 @@ require "extend/cachable"
require "tab" require "tab"
require "utils/bottles" require "utils/bottles"
require "active_support/core_ext/hash/deep_transform_values"
# The {Formulary} is responsible for creating instances of {Formula}. # The {Formulary} is responsible for creating instances of {Formula}.
# It is not meant to be used directly from formulae. # It is not meant to be used directly from formulae.
# #
@ -26,22 +28,32 @@ module Formulary
!@factory_cache.nil? !@factory_cache.nil?
end end
def self.formula_class_defined?(path) def self.formula_class_defined_from_path?(path)
cache.key?(path) cache.key?(:path) && cache[:path].key?(path)
end end
def self.formula_class_get(path) def self.formula_class_defined_from_api?(name)
cache.fetch(path) 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 end
def self.clear_cache def self.clear_cache
cache.each do |key, klass| cache.each do |type, cached_objects|
next if key == :formulary_factory next if type == :formulary_factory
namespace = klass.name.deconstantize cached_objects.each_value do |klass|
next if namespace.deconstantize != name namespace = klass.name.deconstantize
next if namespace.deconstantize != name
remove_const(namespace.demodulize) remove_const(namespace.demodulize)
end
end end
super super
@ -108,7 +120,95 @@ module Formulary
contents = path.open("r") { |f| ensure_utf8_encoding(f).read } contents = path.open("r") { |f| ensure_utf8_encoding(f).read }
namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}" namespace = "FormulaNamespace#{Digest::MD5.hexdigest(path.to_s)}"
klass = load_formula(name, path, contents, namespace, flags: flags, ignore_errors: ignore_errors) 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 end
def self.resolve(name, spec: nil, force_bottle: false, flags: []) def self.resolve(name, spec: nil, force_bottle: false, flags: [])
@ -155,6 +255,12 @@ module Formulary
class_name class_name
end 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. # A {FormulaLoader} returns instances of formulae.
# Subclasses implement loaders for particular sources of formulae. # Subclasses implement loaders for particular sources of formulae.
class FormulaLoader class FormulaLoader
@ -182,8 +288,8 @@ module Formulary
end end
def klass(flags:, ignore_errors:) def klass(flags:, ignore_errors:)
load_file(flags: flags, ignore_errors: ignore_errors) unless Formulary.formula_class_defined?(path) load_file(flags: flags, ignore_errors: ignore_errors) unless Formulary.formula_class_defined_from_path?(path)
Formulary.formula_class_get(path) Formulary.formula_class_get_from_path(path)
end end
private private
@ -345,10 +451,6 @@ module Formulary
rescue FormulaClassUnavailableError => e rescue FormulaClassUnavailableError => e
raise TapFormulaClassUnavailableError.new(tap, name, e.path, e.class_name, e.class_list), "", e.backtrace raise TapFormulaClassUnavailableError.new(tap, name, e.path, e.class_name, e.class_list), "", e.backtrace
rescue FormulaUnavailableError => e 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 raise TapFormulaUnavailableError.new(tap, name), "", e.backtrace
end end
@ -367,10 +469,6 @@ module Formulary
end end
def get_formula(*) def get_formula(*)
if !CoreTap.instance.installed? && Homebrew::EnvConfig.install_from_api?
raise CoreTapFormulaUnavailableError, name
end
raise FormulaUnavailableError, name raise FormulaUnavailableError, name
end end
end end
@ -392,6 +490,26 @@ module Formulary
end end
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. # Return a {Formula} instance for the given reference.
# `ref` is a string containing: # `ref` is a string containing:
# #
@ -405,12 +523,6 @@ module Formulary
) )
raise ArgumentError, "Formulae must have a ref!" unless ref 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}" cache_key = "#{ref}-#{spec}-#{alias_path}-#{from}"
if factory_cached? && cache[:formulary_factory] && if factory_cached? && cache[:formulary_factory] &&
cache[:formulary_factory][cache_key] cache[:formulary_factory][cache_key]
@ -427,24 +539,6 @@ module Formulary
formula formula
end 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:
# <pre>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
# </pre>
# @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. # Return a {Formula} instance for the given rack.
# #
# @param spec when nil, will auto resolve the formula's spec. # @param spec when nil, will auto resolve the formula's spec.
@ -539,11 +633,9 @@ module Formulary
when URL_START_REGEX when URL_START_REGEX
return FromUrlLoader.new(ref) return FromUrlLoader.new(ref)
when HOMEBREW_TAP_FORMULA_REGEX 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? if ref.start_with?("homebrew/core/") && !CoreTap.instance.installed? && Homebrew::EnvConfig.install_from_api?
name = ref.split("/", 3).last name = ref.split("/", 3).last
possible_keg_formula = Pathname.new("#{HOMEBREW_PREFIX}/opt/#{name}/.brew/#{name}.rb") return FormulaAPILoader.new(name) if Homebrew::API::Formula.all_formulae.key?(name)
return FormulaLoader.new(name, possible_keg_formula) if possible_keg_formula.file?
end end
return TapLoader.new(ref, from: from) return TapLoader.new(ref, from: from)
@ -557,6 +649,12 @@ module Formulary
possible_alias = CoreTap.instance.alias_dir/ref possible_alias = CoreTap.instance.alias_dir/ref
return AliasLoader.new(possible_alias) if possible_alias.file? 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) possible_tap_formulae = tap_paths(ref)
raise TapFormulaAmbiguityError.new(ref, possible_tap_formulae) if possible_tap_formulae.size > 1 raise TapFormulaAmbiguityError.new(ref, possible_tap_formulae) if possible_tap_formulae.size > 1

View File

@ -142,8 +142,6 @@ class Tap
# The remote repository name of this {Tap}. # The remote repository name of this {Tap}.
# e.g. `user/homebrew-repo` # e.g. `user/homebrew-repo`
def remote_repo def remote_repo
raise TapUnavailableError, name unless installed?
return unless remote return unless remote
@remote_repo ||= remote.delete_prefix("https://github.com/") @remote_repo ||= remote.delete_prefix("https://github.com/")
@ -795,6 +793,12 @@ class CoreTap < Tap
safe_system HOMEBREW_BREW_FILE, "tap", instance.name safe_system HOMEBREW_BREW_FILE, "tap", instance.name
end 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). # CoreTap never allows shallow clones (on request from GitHub).
def install(quiet: false, clone_target: nil, force_auto_update: nil, custom_remote: false) 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 remote = Homebrew::EnvConfig.core_git_remote # set by HOMEBREW_CORE_GIT_REMOTE

View File

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

View File

@ -201,28 +201,107 @@ describe Formulary do
}.to raise_error(TapFormulaAmbiguityError) }.to raise_error(TapFormulaAmbiguityError)
end end
end end
end
describe "::map_formula_name_to_local_bottle_path" do context "when loading from the API" do
before do def formula_json_contents(extra_items = {})
formula_path.dirname.mkpath {
formula_path.write formula_content formula_name => {
end "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 let(:deprecate_json) do
expect { {
described_class.factory("formula-to-map") "deprecation_date" => "2022-06-15",
}.to raise_error(FormulaUnavailableError) "deprecation_reason" => "repo_archived",
}
end
ENV["HOMEBREW_INSTALL_FROM_API"] = nil let(:disable_json) do
expect { {
described_class.map_formula_name_to_local_bottle_path "formula-to-map", formula_path "disable_date" => "2022-06-15",
}.to raise_error(UsageError, /HOMEBREW_INSTALL_FROM_API not set/) "disable_reason" => "repo_archived",
}
end
ENV["HOMEBREW_INSTALL_FROM_API"] = "1" before do
described_class.map_formula_name_to_local_bottle_path "formula-to-map", formula_path 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
end end