diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index 4b208fd0ec..7798262da8 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -21,6 +21,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/formula.rb b/Library/Homebrew/api/formula.rb index 5e0774230b..dabdce75d4 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -20,6 +20,17 @@ module Homebrew def fetch(name) Homebrew::API.fetch "#{formula_api_path}/#{name}.json" end + + sig { returns(Array) } + def all_formulae + @all_formulae ||= begin + json_formulae = JSON.parse((HOMEBREW_CACHE_API/"#{formula_api_path}.json").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 429d7e5c5f..1a1ce32233 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/cli/named_args.rb b/Library/Homebrew/cli/named_args.rb index 9d484a141d..cd2a9ab899 100644 --- a/Library/Homebrew/cli/named_args.rb +++ b/Library/Homebrew/cli/named_args.rb @@ -94,11 +94,6 @@ module Homebrew 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 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/update.sh b/Library/Homebrew/cmd/update.sh index 551a063257..706fb936dd 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: etags? + curl \ + "${CURL_DISABLE_CURLRC_ARGS[@]}" \ + --fail --compressed --silent --max-time 5 \ + --location --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..5b5ef6b607 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 diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 4889b5785e..fa20e35d9c 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. # @@ -44,6 +46,8 @@ module Formulary remove_const(namespace.demodulize) end + remove_const("FormulaNamespaceAPI") + super end @@ -392,6 +396,102 @@ 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:) + namespace = "FormulaNamespaceAPI" + mod = if Formulary.const_defined?(namespace) + Formulary.const_get(namespace) + else + Formulary.const_set(namespace, Module.new) + end + + mod.send(:remove_const, :BUILD_FLAGS) if mod.const_defined?(:BUILD_FLAGS) + mod.const_set(:BUILD_FLAGS, flags) + + class_s = Formulary.class_s(name) + if mod.const_defined?(class_s) + mod.const_get(class_s) + else + 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"] + bottles_stable["files"].each do |tag, bottle_spec| + cellar = bottle_spec["cellar"] + cellar = cellar[1..].to_sym if cellar.start_with?(":") + sha256 cellar: cellar, tag.to_sym => bottle_spec["sha256"] + end + end + end + + if (keg_only_reason = json_formula["keg_only_reason"]).present? + reason = keg_only_reason["reason"] + reason = reason[1..].to_sym if reason.start_with?(":") + 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) + end + end + end + # Return a {Formula} instance for the given reference. # `ref` is a string containing: # @@ -539,11 +639,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 +655,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