brew/Library/Homebrew/utils/bottles.rb

395 lines
12 KiB
Ruby
Raw Normal View History

2025-08-31 16:56:56 -07:00
# typed: strict
# frozen_string_literal: true
2016-04-25 17:57:51 +01:00
require "tab"
module Utils
2020-08-19 07:50:49 +02:00
# Helper functions for bottles.
#
2024-04-22 21:05:48 +02:00
# @api internal
2020-08-19 07:50:49 +02:00
module Bottles
2016-04-25 17:57:51 +01:00
class << self
# Gets the tag for the running OS.
2024-04-22 21:05:48 +02:00
#
# @api internal
sig { params(tag: T.nilable(T.any(Symbol, Tag))).returns(Tag) }
def tag(tag = nil)
case tag
when Symbol
Tag.from_symbol(tag)
when Tag
tag
else
2025-08-31 16:56:56 -07:00
@tag ||= T.let(Tag.new(
system: HOMEBREW_SYSTEM.downcase.to_sym,
arch: HOMEBREW_PROCESSOR.downcase.to_sym,
), T.nilable(Tag))
end
2016-04-25 17:57:51 +01:00
end
2025-08-31 16:56:56 -07:00
sig { params(formula: Formula).returns(T::Boolean) }
def built_as?(formula)
return false unless formula.latest_version_installed?
2018-09-17 02:45:00 +02:00
tab = Keg.new(formula.latest_installed_prefix).tab
2016-04-25 17:57:51 +01:00
tab.built_as_bottle
end
2025-08-31 16:56:56 -07:00
sig { params(formula: Formula, file: Pathname).returns(T::Boolean) }
def file_outdated?(formula, file)
file = file.resolved_path
2016-04-25 17:57:51 +01:00
filename = file.basename.to_s
2025-08-31 16:56:56 -07:00
bottle = formula.bottle
return false unless bottle
2016-04-25 17:57:51 +01:00
_, bottle_tag, bottle_rebuild = extname_tag_rebuild(filename)
return false if bottle_tag.blank?
2016-04-25 17:57:51 +01:00
2025-08-31 16:56:56 -07:00
bottle_tag != bottle.tag.to_s || bottle_rebuild.to_i != bottle.rebuild
2016-04-25 17:57:51 +01:00
end
2025-08-31 16:56:56 -07:00
sig { params(filename: String).returns(T::Array[String]) }
def extname_tag_rebuild(filename)
HOMEBREW_BOTTLES_EXTNAME_REGEX.match(filename).to_a
end
2025-08-31 16:56:56 -07:00
sig { params(bottle_file: Pathname).returns(T.nilable(String)) }
2016-04-25 17:57:51 +01:00
def receipt_path(bottle_file)
bottle_file_list(bottle_file).find do |line|
%r{.+/.+/INSTALL_RECEIPT.json}.match?(line)
end
2016-04-25 17:57:51 +01:00
end
2025-08-31 16:56:56 -07:00
sig { params(bottle_file: Pathname, file_path: String).returns(String) }
def file_from_bottle(bottle_file, file_path)
Utils.popen_read("tar", "--extract", "--to-stdout", "--file", bottle_file, file_path)
end
2025-08-31 16:56:56 -07:00
sig { params(bottle_file: Pathname).returns([String, String]) }
2016-04-25 17:57:51 +01:00
def resolve_formula_names(bottle_file)
2025-08-31 16:56:56 -07:00
name = bottle_file_list(bottle_file).first.to_s.split("/").fetch(0)
full_name = if (receipt_file_path = receipt_path(bottle_file))
receipt_file = file_from_bottle(bottle_file, receipt_file_path)
tap = Tab.from_file_content(receipt_file, "#{bottle_file}/#{receipt_file_path}").tap
"#{tap}/#{name}" if tap.present? && !tap.core_tap?
2023-11-05 08:55:58 -08:00
else
bottle_json_path = Pathname(bottle_file.sub(/\.(\d+\.)?tar\.gz$/, ".json"))
if bottle_json_path.exist? &&
2023-11-05 09:10:28 -08:00
(bottle_json_path_contents = bottle_json_path.read.presence) &&
(bottle_json = JSON.parse(bottle_json_path_contents).presence) &&
bottle_json.is_a?(Hash)
2023-11-05 08:55:58 -08:00
bottle_json.keys.first.presence
end
2016-04-25 17:57:51 +01:00
end
full_name ||= name
2016-04-25 17:57:51 +01:00
[name, full_name]
end
2025-08-31 16:56:56 -07:00
sig { params(bottle_file: Pathname).returns(PkgVersion) }
2016-04-25 17:57:51 +01:00
def resolve_version(bottle_file)
2025-08-31 16:56:56 -07:00
version = bottle_file_list(bottle_file).first.to_s.split("/").fetch(1)
PkgVersion.parse(version)
2016-04-25 17:57:51 +01:00
end
2025-08-31 16:56:56 -07:00
sig { params(bottle_file: Pathname, name: String).returns(String) }
def formula_contents(bottle_file, name: resolve_formula_names(bottle_file)[0])
bottle_version = resolve_version bottle_file
formula_path = "#{name}/#{bottle_version}/.brew/#{name}.rb"
contents = file_from_bottle(bottle_file, formula_path)
raise BottleFormulaUnavailableError.new(bottle_file, formula_path) unless $CHILD_STATUS.success?
2018-09-17 02:45:00 +02:00
contents
end
2025-08-31 16:56:56 -07:00
sig {
params(root_url: String, name: String, checksum: T.any(Checksum, String),
filename: T.nilable(Bottle::Filename)).returns(T.any([String, T.nilable(String)], String))
}
def path_resolved_basename(root_url, name, checksum, filename)
if root_url.match?(GitHubPackages::URL_REGEX)
image_name = GitHubPackages.image_formula_name(name)
["#{image_name}/blobs/sha256:#{checksum}", filename&.github_packages]
else
filename&.url_encode
end
end
2025-08-31 16:56:56 -07:00
sig { params(formula: Formula).returns(Tab) }
def load_tab(formula)
keg = Keg.new(formula.prefix)
2024-06-22 13:31:50 -04:00
tabfile = keg/AbstractTab::FILENAME
bottle_json_path = formula.local_bottle_path&.sub(/\.(\d+\.)?tar\.gz$/, ".json")
if (tab_attributes = formula.bottle_tab_attributes.presence)
Tab.from_file_content(tab_attributes.to_json, tabfile)
elsif !tabfile.exist? && bottle_json_path&.exist?
2025-08-31 16:56:56 -07:00
_, tag, = Utils::Bottles.extname_tag_rebuild(formula.local_bottle_path.to_s)
bottle_hash = JSON.parse(File.read(bottle_json_path))
tab_json = bottle_hash[formula.full_name]["bottle"]["tags"][tag]["tab"].to_json
Tab.from_file_content(tab_json, tabfile)
else
tab = keg.tab
Fixes `openjdk_dep_name_if_applicable` when not using `CurlGitHubPackagesDownloadStrategy` When installing a formula, `FormulaInstaller` calls `#pour`, which in turn calls: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/formula_installer.rb#L1260 This `tab` is expected to have `#runtime_dependencies`, and it typically will because most packages come from http://ghcr.io https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/utils/bottles.rb#L111 Any `DownloadStrategy` that does not match `CurlGitHubPackagesDownloadStrategy` will lead here: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/software_spec.rb#L463 Causing this branch to be executed for creating the `tab`: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/utils/bottles.rb#L119 This causes a slight issue because `openjdk_dep_name_if_applicable` calls `keg.runtime_dependencies` when it's still `nil`. https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/keg_relocate.rb#L134-L140 And if it's blank, it won't do the regex replacement on `@@HOMEBREW_JAVA@@`, resulting in the following error when running `Kafka`: ```console $ tail -f /opt/homebrew/var/log/kafka/kafka_output.log /opt/homebrew/Cellar/kafka/3.6.0/libexec/bin/kafka-run-class.sh: line 346: /opt/homebrew/@@HOMEBREW_JAVA@@/bin/java: No such file or directory /opt/homebrew/Cellar/kafka/3.6.0/libexec/bin/kafka-run-class.sh: line 346: exec: /opt/homebrew/@@HOMEBREW_JAVA@@/bin/java: cannot execute: No such file or directory ``` As mentioned by: https://github.com/orgs/Homebrew/discussions/2530#discussioncomment-2002374 > Installing Java-dependent formulae from bottle mirrors doesn't work properly at the moment. The issue is that brew needs the manifest in order to correctly replace @@HOMEBREW_JAVA@@ but brew only knows how to fetch manifests from ghcr.io. > Pull requests to fix this welcome. This should fix this issue, by getting the `runtime_dependencies` directly from the formula for those cases that it can't get it from https://ghcr.io or tabfile ```ruby f_runtime_deps = formula.runtime_dependencies(read_from_tab: false) tab.runtime_dependencies = Tab.runtime_deps_hash(formula, f_runtime_deps) ```
2024-01-29 15:57:55 -08:00
2024-01-30 17:15:13 +00:00
tab.runtime_dependencies = begin
Fixes `openjdk_dep_name_if_applicable` when not using `CurlGitHubPackagesDownloadStrategy` When installing a formula, `FormulaInstaller` calls `#pour`, which in turn calls: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/formula_installer.rb#L1260 This `tab` is expected to have `#runtime_dependencies`, and it typically will because most packages come from http://ghcr.io https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/utils/bottles.rb#L111 Any `DownloadStrategy` that does not match `CurlGitHubPackagesDownloadStrategy` will lead here: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/software_spec.rb#L463 Causing this branch to be executed for creating the `tab`: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/utils/bottles.rb#L119 This causes a slight issue because `openjdk_dep_name_if_applicable` calls `keg.runtime_dependencies` when it's still `nil`. https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/keg_relocate.rb#L134-L140 And if it's blank, it won't do the regex replacement on `@@HOMEBREW_JAVA@@`, resulting in the following error when running `Kafka`: ```console $ tail -f /opt/homebrew/var/log/kafka/kafka_output.log /opt/homebrew/Cellar/kafka/3.6.0/libexec/bin/kafka-run-class.sh: line 346: /opt/homebrew/@@HOMEBREW_JAVA@@/bin/java: No such file or directory /opt/homebrew/Cellar/kafka/3.6.0/libexec/bin/kafka-run-class.sh: line 346: exec: /opt/homebrew/@@HOMEBREW_JAVA@@/bin/java: cannot execute: No such file or directory ``` As mentioned by: https://github.com/orgs/Homebrew/discussions/2530#discussioncomment-2002374 > Installing Java-dependent formulae from bottle mirrors doesn't work properly at the moment. The issue is that brew needs the manifest in order to correctly replace @@HOMEBREW_JAVA@@ but brew only knows how to fetch manifests from ghcr.io. > Pull requests to fix this welcome. This should fix this issue, by getting the `runtime_dependencies` directly from the formula for those cases that it can't get it from https://ghcr.io or tabfile ```ruby f_runtime_deps = formula.runtime_dependencies(read_from_tab: false) tab.runtime_dependencies = Tab.runtime_deps_hash(formula, f_runtime_deps) ```
2024-01-29 15:57:55 -08:00
f_runtime_deps = formula.runtime_dependencies(read_from_tab: false)
2024-01-30 17:15:13 +00:00
Tab.runtime_deps_hash(formula, f_runtime_deps)
Fixes `openjdk_dep_name_if_applicable` when not using `CurlGitHubPackagesDownloadStrategy` When installing a formula, `FormulaInstaller` calls `#pour`, which in turn calls: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/formula_installer.rb#L1260 This `tab` is expected to have `#runtime_dependencies`, and it typically will because most packages come from http://ghcr.io https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/utils/bottles.rb#L111 Any `DownloadStrategy` that does not match `CurlGitHubPackagesDownloadStrategy` will lead here: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/software_spec.rb#L463 Causing this branch to be executed for creating the `tab`: https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/utils/bottles.rb#L119 This causes a slight issue because `openjdk_dep_name_if_applicable` calls `keg.runtime_dependencies` when it's still `nil`. https://github.com/Homebrew/brew/blob/6f20c0300aee2ab0feae3132d13f859d91cf295b/Library/Homebrew/keg_relocate.rb#L134-L140 And if it's blank, it won't do the regex replacement on `@@HOMEBREW_JAVA@@`, resulting in the following error when running `Kafka`: ```console $ tail -f /opt/homebrew/var/log/kafka/kafka_output.log /opt/homebrew/Cellar/kafka/3.6.0/libexec/bin/kafka-run-class.sh: line 346: /opt/homebrew/@@HOMEBREW_JAVA@@/bin/java: No such file or directory /opt/homebrew/Cellar/kafka/3.6.0/libexec/bin/kafka-run-class.sh: line 346: exec: /opt/homebrew/@@HOMEBREW_JAVA@@/bin/java: cannot execute: No such file or directory ``` As mentioned by: https://github.com/orgs/Homebrew/discussions/2530#discussioncomment-2002374 > Installing Java-dependent formulae from bottle mirrors doesn't work properly at the moment. The issue is that brew needs the manifest in order to correctly replace @@HOMEBREW_JAVA@@ but brew only knows how to fetch manifests from ghcr.io. > Pull requests to fix this welcome. This should fix this issue, by getting the `runtime_dependencies` directly from the formula for those cases that it can't get it from https://ghcr.io or tabfile ```ruby f_runtime_deps = formula.runtime_dependencies(read_from_tab: false) tab.runtime_dependencies = Tab.runtime_deps_hash(formula, f_runtime_deps) ```
2024-01-29 15:57:55 -08:00
end
tab
end
end
private
2025-08-31 16:56:56 -07:00
sig { params(bottle_file: Pathname).returns(T::Array[String]) }
def bottle_file_list(bottle_file)
2025-08-31 16:56:56 -07:00
@bottle_file_list ||= T.let({}, T.nilable(T::Hash[Pathname, T::Array[String]]))
@bottle_file_list[bottle_file] ||= Utils.popen_read("tar", "--list", "--file", bottle_file)
.lines
.map(&:chomp)
end
2016-04-25 17:57:51 +01:00
end
# Denotes the arch and OS of a bottle.
class Tag
2025-08-31 16:56:56 -07:00
sig { returns(Symbol) }
attr_reader :system, :arch
sig { params(value: Symbol).returns(T.attached_class) }
def self.from_symbol(value)
return new(system: :all, arch: :all) if value == :all
2025-08-31 16:56:56 -07:00
@all_archs_regex ||= T.let(begin
all_archs = Hardware::CPU::ALL_ARCHS.map(&:to_s)
/
^((?<arch>#{Regexp.union(all_archs)})_)?
(?<system>[\w.]+)$
/x
2025-08-31 16:56:56 -07:00
end, T.nilable(Regexp))
match = @all_archs_regex.match(value.to_s)
raise ArgumentError, "Invalid bottle tag symbol" unless match
2025-08-31 16:56:56 -07:00
system = T.must(match[:system]).to_sym
arch = match[:arch]&.to_sym || :x86_64
2024-03-07 16:20:20 +00:00
new(system:, arch:)
end
sig { params(system: Symbol, arch: Symbol).void }
def initialize(system:, arch:)
@system = system
@arch = arch
end
2025-08-31 16:56:56 -07:00
sig { override.params(other: BasicObject).returns(T::Boolean) }
def ==(other)
2025-08-31 16:56:56 -07:00
case other
when Symbol
to_sym == other
2025-08-31 16:56:56 -07:00
when self.class
system == other.system && standardized_arch == other.standardized_arch
else false
end
end
2025-08-31 16:56:56 -07:00
sig { override.params(other: BasicObject).returns(T::Boolean) }
def eql?(other)
2025-08-31 16:56:56 -07:00
case other
when self.class
self == other
else false
end
end
2025-08-31 16:56:56 -07:00
sig { override.returns(Integer) }
def hash
[system, standardized_arch].hash
end
sig { returns(Symbol) }
2022-07-21 15:32:51 +02:00
def standardized_arch
return :x86_64 if [:x86_64, :intel].include? arch
return :arm64 if [:arm64, :arm, :aarch64].include? arch
2022-07-21 15:32:51 +02:00
arch
end
sig { returns(Symbol) }
def to_sym
arch_to_symbol(standardized_arch)
end
2025-08-31 16:56:56 -07:00
sig { override.returns(String) }
def to_s
to_sym.to_s
end
2025-08-31 16:56:56 -07:00
sig { returns(Symbol) }
def to_unstandardized_sym
# Never allow these generic names
return to_sym if [:intel, :arm].include? arch
# Backwards compatibility with older bottle names
arch_to_symbol(arch)
end
sig { returns(MacOSVersion) }
def to_macos_version
2025-08-31 16:56:56 -07:00
@to_macos_version ||= T.let(MacOSVersion.from_symbol(system), T.nilable(MacOSVersion))
end
sig { returns(T::Boolean) }
def linux?
system == :linux
end
sig { returns(T::Boolean) }
def macos?
MacOSVersion::SYMBOLS.key?(system)
end
2022-07-21 15:32:51 +02:00
sig { returns(T::Boolean) }
def valid_combination?
return true unless [:arm64, :arm, :aarch64].include? arch
return true unless macos?
2022-07-21 15:32:51 +02:00
# Big Sur is the first version of macOS that runs on ARM
to_macos_version >= :big_sur
end
sig { returns(String) }
def default_prefix
if linux?
HOMEBREW_LINUX_DEFAULT_PREFIX
elsif standardized_arch == :arm64
HOMEBREW_MACOS_ARM_DEFAULT_PREFIX
else
HOMEBREW_DEFAULT_PREFIX
end
end
sig { returns(String) }
def default_cellar
if linux?
Homebrew::DEFAULT_LINUX_CELLAR
elsif standardized_arch == :arm64
Homebrew::DEFAULT_MACOS_ARM_CELLAR
else
Homebrew::DEFAULT_MACOS_CELLAR
end
end
private
sig { params(arch: Symbol).returns(Symbol) }
def arch_to_symbol(arch)
if system == :all && arch == :all
:all
elsif macos? && standardized_arch == :x86_64
system
else
:"#{arch}_#{system}"
end
end
end
# The specification for a specific tag
class TagSpecification
sig { returns(Utils::Bottles::Tag) }
attr_reader :tag
2020-02-19 11:54:42 +00:00
sig { returns(Checksum) }
attr_reader :checksum
sig { returns(T.any(Symbol, String)) }
attr_reader :cellar
2025-08-31 16:56:56 -07:00
sig { params(tag: Utils::Bottles::Tag, checksum: Checksum, cellar: T.any(Symbol, String)).void }
def initialize(tag:, checksum:, cellar:)
@tag = tag
@checksum = checksum
@cellar = cellar
end
2025-08-31 16:56:56 -07:00
sig { override.params(other: BasicObject).returns(T::Boolean) }
def ==(other)
2025-08-31 16:56:56 -07:00
case other
when self.class
tag == other.tag && checksum == other.checksum && cellar == other.cellar
else false
end
end
alias eql? ==
end
# Collector for bottle specifications.
class Collector
2020-10-20 12:03:48 +02:00
sig { void }
2016-04-25 17:57:51 +01:00
def initialize
@tag_specs = T.let({}, T::Hash[Utils::Bottles::Tag, Utils::Bottles::TagSpecification])
end
sig { returns(T::Array[Utils::Bottles::Tag]) }
def tags
@tag_specs.keys
end
2025-08-31 16:56:56 -07:00
sig { override.params(other: BasicObject).returns(T::Boolean) }
def ==(other)
2025-08-31 16:56:56 -07:00
case other
when self.class
@tag_specs == other.tag_specs
else false
end
end
alias eql? ==
sig { params(tag: Utils::Bottles::Tag, checksum: Checksum, cellar: T.any(Symbol, String)).void }
def add(tag, checksum:, cellar:)
2024-03-07 16:20:20 +00:00
spec = Utils::Bottles::TagSpecification.new(tag:, checksum:, cellar:)
@tag_specs[tag] = spec
end
sig { params(tag: Utils::Bottles::Tag, no_older_versions: T::Boolean).returns(T::Boolean) }
def tag?(tag, no_older_versions: false)
2024-03-07 16:20:20 +00:00
tag = find_matching_tag(tag, no_older_versions:)
tag.present?
end
sig { params(block: T.proc.params(tag: Utils::Bottles::Tag).void).void }
def each_tag(&block)
@tag_specs.each_key(&block)
2016-04-25 17:57:51 +01:00
end
sig {
params(tag: Utils::Bottles::Tag, no_older_versions: T::Boolean)
.returns(T.nilable(Utils::Bottles::TagSpecification))
}
def specification_for(tag, no_older_versions: false)
2024-03-07 16:20:20 +00:00
tag = find_matching_tag(tag, no_older_versions:)
@tag_specs[tag] if tag
2016-04-25 17:57:51 +01:00
end
2025-08-31 16:56:56 -07:00
protected
sig { returns(T::Hash[Utils::Bottles::Tag, Utils::Bottles::TagSpecification]) }
attr_reader :tag_specs
2016-04-25 17:57:51 +01:00
private
2025-08-31 16:56:56 -07:00
sig { params(tag: Utils::Bottles::Tag, no_older_versions: T::Boolean).returns(T.nilable(Utils::Bottles::Tag)) }
def find_matching_tag(tag, no_older_versions: false)
if @tag_specs.key?(tag)
tag
else
all = Tag.from_symbol(:all)
all if @tag_specs.key?(all)
end
2016-04-25 17:57:51 +01:00
end
end
end
end
require "extend/os/bottles"