Skip to content

Commit

Permalink
Merge pull request #11648 from Rylan12/homebrew-json
Browse files Browse the repository at this point in the history
Install formulae from JSON files
  • Loading branch information
Rylan12 committed Jul 13, 2021
2 parents 92a206f + 1973e72 commit e344cb6
Show file tree
Hide file tree
Showing 10 changed files with 290 additions and 13 deletions.
106 changes: 106 additions & 0 deletions Library/Homebrew/bottle_api.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# typed: true
# frozen_string_literal: true

require "github_packages"

# Helper functions for using the Bottle JSON API.
#
# @api private
module BottleAPI
extend T::Sig

module_function

FORMULAE_BREW_SH_BOTTLE_API_DOMAIN = if OS.mac?
"https://formulae.brew.sh/api/bottle"
else
"https://formulae.brew.sh/api/bottle-linux"
end.freeze

FORMULAE_BREW_SH_VERSIONS_API_URL = if OS.mac?
"https://formulae.brew.sh/api/versions-formulae.json"
else
"https://formulae.brew.sh/api/versions-linux.json"
end.freeze

GITHUB_PACKAGES_SHA256_REGEX = %r{#{GitHubPackages::URL_REGEX}.*/blobs/sha256:(?<sha256>\h{64})$}.freeze

sig { params(name: String).returns(Hash) }
def fetch(name)
return @cache[name] if @cache.present? && @cache.key?(name)

api_url = "#{FORMULAE_BREW_SH_BOTTLE_API_DOMAIN}/#{name}.json"
output = Utils::Curl.curl_output("--fail", api_url)
raise ArgumentError, "No JSON file found at #{Tty.underline}#{api_url}#{Tty.reset}" unless output.success?

@cache ||= {}
@cache[name] = JSON.parse(output.stdout)
rescue JSON::ParserError
raise ArgumentError, "Invalid JSON file: #{Tty.underline}#{api_url}#{Tty.reset}"
end

sig { params(name: String).returns(T.nilable(PkgVersion)) }
def latest_pkg_version(name)
@formula_versions ||= begin
output = Utils::Curl.curl_output("--fail", FORMULAE_BREW_SH_VERSIONS_API_URL)
JSON.parse(output.stdout)
end

return unless @formula_versions.key? name

version = Version.new(@formula_versions[name]["version"])
revision = @formula_versions[name]["revision"]
PkgVersion.new(version, revision)
end

sig { params(name: String).returns(T::Boolean) }
def bottle_available?(name)
fetch name
true
rescue ArgumentError
false
end

sig { params(name: String).void }
def fetch_bottles(name)
hash = fetch(name)
bottle_tag = Utils::Bottles.tag.to_s

odie "No bottle available for current OS" unless hash["bottles"].key? bottle_tag

download_bottle(hash, bottle_tag)

hash["dependencies"].each do |dep_hash|
download_bottle(dep_hash, bottle_tag)
end
end

sig { params(url: String).returns(T.nilable(String)) }
def checksum_from_url(url)
match = url.match GITHUB_PACKAGES_SHA256_REGEX
return if match.blank?

match[:sha256]
end

sig { params(hash: Hash, tag: Symbol).void }
def download_bottle(hash, tag)
bottle = hash["bottles"][tag]
return if bottle.blank?

sha256 = bottle["sha256"] || checksum_from_url(bottle["url"])
bottle_filename = Bottle::Filename.new(hash["name"], hash["pkg_version"], tag, hash["rebuild"])

resource = Resource.new hash["name"]
resource.url bottle["url"]
resource.sha256 sha256
resource.version hash["pkg_version"]
resource.downloader.resolved_basename = bottle_filename

resource.fetch

# Map the name of this formula to the local bottle path to allow the
# formula to be loaded by passing just the name to `Formulary::factory`.
Formulary.map_formula_name_to_local_bottle_path hash["name"], resource.downloader.cached_location
end
end
5 changes: 5 additions & 0 deletions Library/Homebrew/bottle_api.rbi
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# typed: strict

module BottleAPI
include Kernel
end
22 changes: 14 additions & 8 deletions Library/Homebrew/cli/named_args.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

require "delegate"

require "bottle_api"
require "cli/args"

module Homebrew
Expand Down Expand Up @@ -45,16 +45,18 @@ def to_formulae
# 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,
only: T.nilable(Symbol),
ignore_unavailable: T.nilable(T::Boolean),
method: T.nilable(Symbol),
uniq: T::Boolean,
prefer_loading_from_json: 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)
def to_formulae_and_casks(only: parent&.only_formula_or_cask, ignore_unavailable: nil, method: nil, uniq: true,
prefer_loading_from_json: false)
@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)
load_formula_or_cask(name, only: only, method: method, prefer_loading_from_json: prefer_loading_from_json)
rescue FormulaUnreadableError, FormulaClassUnavailableError,
TapFormulaUnreadableError, TapFormulaClassUnavailableError,
Cask::CaskUnreadableError
Expand Down Expand Up @@ -88,10 +90,14 @@ def to_formulae_and_casks_and_unavailable(only: parent&.only_formula_or_cask, me
end.uniq.freeze
end

def load_formula_or_cask(name, only: nil, method: nil)
def load_formula_or_cask(name, only: nil, method: nil, prefer_loading_from_json: false)
unreadable_error = nil

if only != :cask
if prefer_loading_from_json && ENV["HOMEBREW_JSON_CORE"].present? && BottleAPI.bottle_available?(name)
BottleAPI.fetch_bottles(name)
end

begin
formula = case method
when nil, :factory
Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/cmd/install.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def install
end

begin
formulae, casks = args.named.to_formulae_and_casks
formulae, casks = args.named.to_formulae_and_casks(prefer_loading_from_json: true)
.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?)
Expand Down
5 changes: 4 additions & 1 deletion Library/Homebrew/cmd/outdated.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
require "cli/parser"
require "cask/cmd"
require "cask/caskroom"
require "bottle_api"

module Homebrew
extend T::Sig
Expand Down Expand Up @@ -91,7 +92,9 @@ def print_outdated(formulae_or_casks, args:)
if verbose?
outdated_kegs = f.outdated_kegs(fetch_head: args.fetch_HEAD?)

current_version = if f.alias_changed? && !f.latest_formula.latest_version_installed?
current_version = if ENV["HOMEBREW_JSON_CORE"].present? && (f.core_formula? || f.tap.blank?)
BottleAPI.latest_pkg_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.name} (#{latest.pkg_version})"
elsif f.head? && outdated_kegs.any? { |k| k.version.to_s == f.pkg_version.to_s }
Expand Down
14 changes: 14 additions & 0 deletions Library/Homebrew/cmd/reinstall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
require "cask/utils"
require "cask/macos"
require "upgrade"
require "bottle_api"

module Homebrew
extend T::Sig
Expand Down Expand Up @@ -84,6 +85,19 @@ def reinstall_args
def reinstall
args = reinstall_args.parse

if ENV["HOMEBREW_JSON_CORE"].present?
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 BottleAPI.bottle_available?(name)

BottleAPI.fetch_bottles(name)
rescue FormulaUnavailableError
next
end
end

formulae, casks = args.named.to_formulae_and_casks(method: :resolve)
.partition { |o| o.is_a?(Formula) }

Expand Down
13 changes: 13 additions & 0 deletions Library/Homebrew/cmd/upgrade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "cask/cmd"
require "cask/utils"
require "cask/macos"
require "bottle_api"

module Homebrew
extend T::Sig
Expand Down Expand Up @@ -159,6 +160,18 @@ def upgrade_outdated_formulae(formulae, args:)
puts pinned.map { |f| "#{f.full_specified_name} #{f.pkg_version}" } * ", "
end

if ENV["HOMEBREW_JSON_CORE"].present?
formulae_to_install.map! do |formula|
next formula if formula.tap.present? && !formula.core_formula?
next formula unless BottleAPI.bottle_available?(formula.name)

BottleAPI.fetch_bottles(formula.name)
Formulary.factory(formula.name)
rescue FormulaUnavailableError
formula
end
end

if formulae_to_install.empty?
oh1 "No packages to upgrade"
else
Expand Down
10 changes: 8 additions & 2 deletions Library/Homebrew/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
require "find"
require "utils/spdx"
require "extend/on_os"
require "bottle_api"

# A formula provides instructions and metadata for Homebrew to install a piece
# of software. Every Homebrew formula is a {Formula}.
Expand Down Expand Up @@ -1325,15 +1326,20 @@ def outdated_kegs(fetch_head: false)
Formula.cache[:outdated_kegs][cache_key] ||= begin
all_kegs = []
current_version = T.let(false, T::Boolean)
latest_version = if ENV["HOMEBREW_JSON_CORE"].present? && (core_formula? || tap.blank?)
BottleAPI.latest_pkg_version(name) || pkg_version
else
pkg_version
end

installed_kegs.each do |keg|
all_kegs << keg
version = keg.version
next if version.head?

tab = Tab.for_keg(keg)
next if version_scheme > tab.version_scheme && pkg_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 && latest_version > version

# don't consider this keg current if there's a newer formula available
next if follow_installed_alias? && new_formula_available?
Expand Down

0 comments on commit e344cb6

Please sign in to comment.