Split up SoftwareSpec

This came up in the AGM and has bothered me for years: let's actually
split out `software_spec.rb` into one file per class, as is more typical
in Ruby.

This will make these classes easier to find.
This commit is contained in:
Mike McQuaid 2025-02-04 16:27:39 +00:00
parent 153c6dd300
commit 2b737f0423
No known key found for this signature in database
12 changed files with 398 additions and 384 deletions

219
Library/Homebrew/bottle.rb Normal file
View File

@ -0,0 +1,219 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
class Bottle
include Downloadable
class Filename
attr_reader :name, :version, :tag, :rebuild
sig { params(formula: Formula, tag: Utils::Bottles::Tag, rebuild: Integer).returns(T.attached_class) }
def self.create(formula, tag, rebuild)
new(formula.name, formula.pkg_version, tag, rebuild)
end
sig { params(name: String, version: PkgVersion, tag: Utils::Bottles::Tag, rebuild: Integer).void }
def initialize(name, version, tag, rebuild)
@name = File.basename name
raise ArgumentError, "Invalid bottle name" unless Utils.safe_filename?(@name)
raise ArgumentError, "Invalid bottle version" unless Utils.safe_filename?(version.to_s)
@version = version
@tag = tag.to_unstandardized_sym.to_s
@rebuild = rebuild
end
sig { returns(String) }
def to_str
"#{name}--#{version}#{extname}"
end
sig { returns(String) }
def to_s = to_str
sig { returns(String) }
def json
"#{name}--#{version}.#{tag}.bottle.json"
end
def url_encode
ERB::Util.url_encode("#{name}-#{version}#{extname}")
end
def github_packages
"#{name}--#{version}#{extname}"
end
sig { returns(String) }
def extname
s = rebuild.positive? ? ".#{rebuild}" : ""
".#{tag}.bottle#{s}.tar.gz"
end
end
extend Forwardable
attr_reader :name, :resource, :tag, :cellar, :rebuild
def_delegators :resource, :url, :verify_download_integrity
def_delegators :resource, :cached_download, :downloader
def initialize(formula, spec, tag = nil)
super()
@name = formula.name
@resource = Resource.new
@resource.owner = formula
@spec = spec
tag_spec = spec.tag_specification_for(Utils::Bottles.tag(tag))
@tag = tag_spec.tag
@cellar = tag_spec.cellar
@rebuild = spec.rebuild
@resource.version(formula.pkg_version.to_s)
@resource.checksum = tag_spec.checksum
@fetch_tab_retried = false
root_url(spec.root_url, spec.root_url_specs)
end
sig {
override.params(
verify_download_integrity: T::Boolean,
timeout: T.nilable(T.any(Integer, Float)),
quiet: T.nilable(T::Boolean),
).returns(Pathname)
}
def fetch(verify_download_integrity: true, timeout: nil, quiet: false)
resource.fetch(verify_download_integrity:, timeout:, quiet:)
rescue DownloadError
raise unless fallback_on_error
fetch_tab
retry
end
sig { override.void }
def clear_cache
@resource.clear_cache
github_packages_manifest_resource&.clear_cache
@fetch_tab_retried = false
end
def compatible_locations?
@spec.compatible_locations?(tag: @tag)
end
# Does the bottle need to be relocated?
def skip_relocation?
@spec.skip_relocation?(tag: @tag)
end
def stage = downloader.stage
def fetch_tab(timeout: nil, quiet: false)
return unless (resource = github_packages_manifest_resource)
begin
resource.fetch(timeout:, quiet:)
rescue DownloadError
raise unless fallback_on_error
retry
rescue Resource::BottleManifest::Error
raise if @fetch_tab_retried
@fetch_tab_retried = true
resource.clear_cache
retry
end
end
def tab_attributes
if (resource = github_packages_manifest_resource) && resource.downloaded?
return resource.tab
end
{}
end
sig { returns(T.nilable(Integer)) }
def bottle_size
resource = github_packages_manifest_resource
return unless resource&.downloaded?
resource.bottle_size
end
sig { returns(T.nilable(Integer)) }
def installed_size
resource = github_packages_manifest_resource
return unless resource&.downloaded?
resource.installed_size
end
sig { returns(Filename) }
def filename
Filename.create(resource.owner, @tag, @spec.rebuild)
end
sig { returns(T.nilable(Resource::BottleManifest)) }
def github_packages_manifest_resource
return if @resource.download_strategy != CurlGitHubPackagesDownloadStrategy
@github_packages_manifest_resource ||= begin
resource = Resource::BottleManifest.new(self)
version_rebuild = GitHubPackages.version_rebuild(@resource.version, rebuild)
resource.version(version_rebuild)
image_name = GitHubPackages.image_formula_name(@name)
image_tag = GitHubPackages.image_version_rebuild(version_rebuild)
resource.url(
"#{root_url}/#{image_name}/manifests/#{image_tag}",
using: CurlGitHubPackagesDownloadStrategy,
headers: ["Accept: application/vnd.oci.image.index.v1+json"],
)
T.cast(resource.downloader, CurlGitHubPackagesDownloadStrategy).resolved_basename =
"#{name}-#{version_rebuild}.bottle_manifest.json"
resource
end
end
private
def select_download_strategy(specs)
specs[:using] ||= DownloadStrategyDetector.detect(@root_url)
specs[:bottle] = true
specs
end
def fallback_on_error
# Use the default bottle domain as a fallback mirror
if @resource.url.start_with?(Homebrew::EnvConfig.bottle_domain) &&
Homebrew::EnvConfig.bottle_domain != HOMEBREW_BOTTLE_DEFAULT_DOMAIN
opoo "Bottle missing, falling back to the default domain..."
root_url(HOMEBREW_BOTTLE_DEFAULT_DOMAIN)
@github_packages_manifest_resource = nil
true
else
false
end
end
def root_url(val = nil, specs = {})
return @root_url if val.nil?
@root_url = val
filename = Filename.create(resource.owner, @tag, @spec.rebuild)
path, resolved_basename = Utils::Bottles.path_resolved_basename(val, name, resource.checksum, filename)
@resource.url("#{val}/#{path}", **select_download_strategy(specs))
@resource.downloader.resolved_basename = resolved_basename if resolved_basename.present?
end
end

View File

@ -0,0 +1,135 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
class BottleSpecification
extend Attrable
RELOCATABLE_CELLARS = [:any, :any_skip_relocation].freeze
attr_rw :rebuild
attr_accessor :tap
attr_reader :collector, :root_url_specs, :repository
sig { void }
def initialize
@rebuild = 0
@repository = Homebrew::DEFAULT_REPOSITORY
@collector = Utils::Bottles::Collector.new
@root_url_specs = {}
end
def root_url(var = nil, specs = {})
if var.nil?
@root_url ||= if (github_packages_url = GitHubPackages.root_url_if_match(Homebrew::EnvConfig.bottle_domain))
github_packages_url
else
Homebrew::EnvConfig.bottle_domain
end
else
@root_url = if (github_packages_url = GitHubPackages.root_url_if_match(var))
github_packages_url
else
var
end
@root_url_specs.merge!(specs)
end
end
def ==(other)
self.class == other.class && rebuild == other.rebuild && collector == other.collector &&
root_url == other.root_url && root_url_specs == other.root_url_specs && tap == other.tap
end
alias eql? ==
sig { params(tag: Utils::Bottles::Tag).returns(T.any(Symbol, String)) }
def tag_to_cellar(tag = Utils::Bottles.tag)
spec = collector.specification_for(tag)
if spec.present?
spec.cellar
else
tag.default_cellar
end
end
sig { params(tag: Utils::Bottles::Tag).returns(T::Boolean) }
def compatible_locations?(tag: Utils::Bottles.tag)
cellar = tag_to_cellar(tag)
return true if RELOCATABLE_CELLARS.include?(cellar)
prefix = Pathname(cellar.to_s).parent.to_s
cellar_relocatable = cellar.size >= HOMEBREW_CELLAR.to_s.size && ENV["HOMEBREW_RELOCATE_BUILD_PREFIX"].present?
prefix_relocatable = prefix.size >= HOMEBREW_PREFIX.to_s.size && ENV["HOMEBREW_RELOCATE_BUILD_PREFIX"].present?
compatible_cellar = cellar == HOMEBREW_CELLAR.to_s || cellar_relocatable
compatible_prefix = prefix == HOMEBREW_PREFIX.to_s || prefix_relocatable
compatible_cellar && compatible_prefix
end
# Does the {Bottle} this {BottleSpecification} belongs to need to be relocated?
sig { params(tag: Utils::Bottles::Tag).returns(T::Boolean) }
def skip_relocation?(tag: Utils::Bottles.tag)
spec = collector.specification_for(tag)
spec&.cellar == :any_skip_relocation
end
sig { params(tag: T.any(Symbol, Utils::Bottles::Tag), no_older_versions: T::Boolean).returns(T::Boolean) }
def tag?(tag, no_older_versions: false)
collector.tag?(tag, no_older_versions:)
end
# Checksum methods in the DSL's bottle block take
# a Hash, which indicates the platform the checksum applies on.
# Example bottle block syntax:
# bottle do
# sha256 cellar: :any_skip_relocation, big_sur: "69489ae397e4645..."
# sha256 cellar: :any, catalina: "449de5ea35d0e94..."
# end
def sha256(hash)
sha256_regex = /^[a-f0-9]{64}$/i
# find new `sha256 big_sur: "69489ae397e4645..."` format
tag, digest = hash.find do |key, value|
key.is_a?(Symbol) && value.is_a?(String) && value.match?(sha256_regex)
end
cellar = hash[:cellar] if digest && tag
tag = Utils::Bottles::Tag.from_symbol(tag)
cellar ||= tag.default_cellar
collector.add(tag, checksum: Checksum.new(digest), cellar:)
end
sig {
params(tag: Utils::Bottles::Tag, no_older_versions: T::Boolean)
.returns(T.nilable(Utils::Bottles::TagSpecification))
}
def tag_specification_for(tag, no_older_versions: false)
collector.specification_for(tag, no_older_versions:)
end
def checksums
tags = collector.tags.sort_by do |tag|
version = tag.to_macos_version
# Give `arm64` bottles a higher priority so they are first.
priority = (tag.arch == :arm64) ? 2 : 1
"#{priority}.#{version}_#{tag}"
rescue MacOSVersion::Error
# Sort non-macOS tags below macOS tags.
"0.#{tag}"
end
tags.reverse.map do |tag|
spec = collector.specification_for(tag)
{
"tag" => spec.tag.to_sym,
"digest" => spec.checksum,
"cellar" => spec.cellar,
}
end
end
end
require "extend/os/bottle_specification"

View File

@ -0,0 +1,4 @@
# typed: strict
# frozen_string_literal: true
require "extend/os/linux/bottle_specification" if OS.linux?

View File

@ -1,4 +0,0 @@
# typed: strict
# frozen_string_literal: true
require "extend/os/linux/software_spec" if OS.linux?

View File

@ -19,6 +19,10 @@ require "build_environment"
require "build_options" require "build_options"
require "formulary" require "formulary"
require "software_spec" require "software_spec"
require "bottle"
require "pour_bottle_check"
require "head_software_spec"
require "bottle_specification"
require "livecheck" require "livecheck"
require "service" require "service"
require "install_renamed" require "install_renamed"

View File

@ -0,0 +1,15 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "software_spec"
class HeadSoftwareSpec < SoftwareSpec
def initialize(flags: [])
super
@resource.version(Version.new("HEAD"))
end
def verify_download_integrity(_filename)
# no-op
end
end

View File

@ -0,0 +1,18 @@
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
class PourBottleCheck
include OnSystem::MacOSAndLinux
def initialize(formula)
@formula = formula
end
def reason(reason)
@formula.pour_bottle_check_unsatisfied_reason = reason
end
def satisfy(&block)
@formula.send(:define_method, :pour_bottle?, &block)
end
end

View File

@ -284,380 +284,3 @@ class SoftwareSpec
end end
end end
end end
class HeadSoftwareSpec < SoftwareSpec
def initialize(flags: [])
super
@resource.version(Version.new("HEAD"))
end
def verify_download_integrity(_filename)
# no-op
end
end
class Bottle
include Downloadable
class Filename
attr_reader :name, :version, :tag, :rebuild
sig { params(formula: Formula, tag: Utils::Bottles::Tag, rebuild: Integer).returns(T.attached_class) }
def self.create(formula, tag, rebuild)
new(formula.name, formula.pkg_version, tag, rebuild)
end
sig { params(name: String, version: PkgVersion, tag: Utils::Bottles::Tag, rebuild: Integer).void }
def initialize(name, version, tag, rebuild)
@name = File.basename name
raise ArgumentError, "Invalid bottle name" unless Utils.safe_filename?(@name)
raise ArgumentError, "Invalid bottle version" unless Utils.safe_filename?(version.to_s)
@version = version
@tag = tag.to_unstandardized_sym.to_s
@rebuild = rebuild
end
sig { returns(String) }
def to_str
"#{name}--#{version}#{extname}"
end
sig { returns(String) }
def to_s = to_str
sig { returns(String) }
def json
"#{name}--#{version}.#{tag}.bottle.json"
end
def url_encode
ERB::Util.url_encode("#{name}-#{version}#{extname}")
end
def github_packages
"#{name}--#{version}#{extname}"
end
sig { returns(String) }
def extname
s = rebuild.positive? ? ".#{rebuild}" : ""
".#{tag}.bottle#{s}.tar.gz"
end
end
extend Forwardable
attr_reader :name, :resource, :tag, :cellar, :rebuild
def_delegators :resource, :url, :verify_download_integrity
def_delegators :resource, :cached_download, :downloader
def initialize(formula, spec, tag = nil)
super()
@name = formula.name
@resource = Resource.new
@resource.owner = formula
@spec = spec
tag_spec = spec.tag_specification_for(Utils::Bottles.tag(tag))
@tag = tag_spec.tag
@cellar = tag_spec.cellar
@rebuild = spec.rebuild
@resource.version(formula.pkg_version.to_s)
@resource.checksum = tag_spec.checksum
@fetch_tab_retried = false
root_url(spec.root_url, spec.root_url_specs)
end
sig {
override.params(
verify_download_integrity: T::Boolean,
timeout: T.nilable(T.any(Integer, Float)),
quiet: T.nilable(T::Boolean),
).returns(Pathname)
}
def fetch(verify_download_integrity: true, timeout: nil, quiet: false)
resource.fetch(verify_download_integrity:, timeout:, quiet:)
rescue DownloadError
raise unless fallback_on_error
fetch_tab
retry
end
sig { override.void }
def clear_cache
@resource.clear_cache
github_packages_manifest_resource&.clear_cache
@fetch_tab_retried = false
end
def compatible_locations?
@spec.compatible_locations?(tag: @tag)
end
# Does the bottle need to be relocated?
def skip_relocation?
@spec.skip_relocation?(tag: @tag)
end
def stage = downloader.stage
def fetch_tab(timeout: nil, quiet: false)
return unless (resource = github_packages_manifest_resource)
begin
resource.fetch(timeout:, quiet:)
rescue DownloadError
raise unless fallback_on_error
retry
rescue Resource::BottleManifest::Error
raise if @fetch_tab_retried
@fetch_tab_retried = true
resource.clear_cache
retry
end
end
def tab_attributes
if (resource = github_packages_manifest_resource) && resource.downloaded?
return resource.tab
end
{}
end
sig { returns(T.nilable(Integer)) }
def bottle_size
resource = github_packages_manifest_resource
return unless resource&.downloaded?
resource.bottle_size
end
sig { returns(T.nilable(Integer)) }
def installed_size
resource = github_packages_manifest_resource
return unless resource&.downloaded?
resource.installed_size
end
sig { returns(Filename) }
def filename
Filename.create(resource.owner, @tag, @spec.rebuild)
end
sig { returns(T.nilable(Resource::BottleManifest)) }
def github_packages_manifest_resource
return if @resource.download_strategy != CurlGitHubPackagesDownloadStrategy
@github_packages_manifest_resource ||= begin
resource = Resource::BottleManifest.new(self)
version_rebuild = GitHubPackages.version_rebuild(@resource.version, rebuild)
resource.version(version_rebuild)
image_name = GitHubPackages.image_formula_name(@name)
image_tag = GitHubPackages.image_version_rebuild(version_rebuild)
resource.url(
"#{root_url}/#{image_name}/manifests/#{image_tag}",
using: CurlGitHubPackagesDownloadStrategy,
headers: ["Accept: application/vnd.oci.image.index.v1+json"],
)
T.cast(resource.downloader, CurlGitHubPackagesDownloadStrategy).resolved_basename =
"#{name}-#{version_rebuild}.bottle_manifest.json"
resource
end
end
private
def select_download_strategy(specs)
specs[:using] ||= DownloadStrategyDetector.detect(@root_url)
specs[:bottle] = true
specs
end
def fallback_on_error
# Use the default bottle domain as a fallback mirror
if @resource.url.start_with?(Homebrew::EnvConfig.bottle_domain) &&
Homebrew::EnvConfig.bottle_domain != HOMEBREW_BOTTLE_DEFAULT_DOMAIN
opoo "Bottle missing, falling back to the default domain..."
root_url(HOMEBREW_BOTTLE_DEFAULT_DOMAIN)
@github_packages_manifest_resource = nil
true
else
false
end
end
def root_url(val = nil, specs = {})
return @root_url if val.nil?
@root_url = val
filename = Filename.create(resource.owner, @tag, @spec.rebuild)
path, resolved_basename = Utils::Bottles.path_resolved_basename(val, name, resource.checksum, filename)
@resource.url("#{val}/#{path}", **select_download_strategy(specs))
@resource.downloader.resolved_basename = resolved_basename if resolved_basename.present?
end
end
class BottleSpecification
extend Attrable
RELOCATABLE_CELLARS = [:any, :any_skip_relocation].freeze
attr_rw :rebuild
attr_accessor :tap
attr_reader :collector, :root_url_specs, :repository
sig { void }
def initialize
@rebuild = 0
@repository = Homebrew::DEFAULT_REPOSITORY
@collector = Utils::Bottles::Collector.new
@root_url_specs = {}
end
def root_url(var = nil, specs = {})
if var.nil?
@root_url ||= if (github_packages_url = GitHubPackages.root_url_if_match(Homebrew::EnvConfig.bottle_domain))
github_packages_url
else
Homebrew::EnvConfig.bottle_domain
end
else
@root_url = if (github_packages_url = GitHubPackages.root_url_if_match(var))
github_packages_url
else
var
end
@root_url_specs.merge!(specs)
end
end
def ==(other)
self.class == other.class && rebuild == other.rebuild && collector == other.collector &&
root_url == other.root_url && root_url_specs == other.root_url_specs && tap == other.tap
end
alias eql? ==
sig { params(tag: Utils::Bottles::Tag).returns(T.any(Symbol, String)) }
def tag_to_cellar(tag = Utils::Bottles.tag)
spec = collector.specification_for(tag)
if spec.present?
spec.cellar
else
tag.default_cellar
end
end
sig { params(tag: Utils::Bottles::Tag).returns(T::Boolean) }
def compatible_locations?(tag: Utils::Bottles.tag)
cellar = tag_to_cellar(tag)
return true if RELOCATABLE_CELLARS.include?(cellar)
prefix = Pathname(cellar.to_s).parent.to_s
cellar_relocatable = cellar.size >= HOMEBREW_CELLAR.to_s.size && ENV["HOMEBREW_RELOCATE_BUILD_PREFIX"].present?
prefix_relocatable = prefix.size >= HOMEBREW_PREFIX.to_s.size && ENV["HOMEBREW_RELOCATE_BUILD_PREFIX"].present?
compatible_cellar = cellar == HOMEBREW_CELLAR.to_s || cellar_relocatable
compatible_prefix = prefix == HOMEBREW_PREFIX.to_s || prefix_relocatable
compatible_cellar && compatible_prefix
end
# Does the {Bottle} this {BottleSpecification} belongs to need to be relocated?
sig { params(tag: Utils::Bottles::Tag).returns(T::Boolean) }
def skip_relocation?(tag: Utils::Bottles.tag)
spec = collector.specification_for(tag)
spec&.cellar == :any_skip_relocation
end
sig { params(tag: T.any(Symbol, Utils::Bottles::Tag), no_older_versions: T::Boolean).returns(T::Boolean) }
def tag?(tag, no_older_versions: false)
collector.tag?(tag, no_older_versions:)
end
# Checksum methods in the DSL's bottle block take
# a Hash, which indicates the platform the checksum applies on.
# Example bottle block syntax:
# bottle do
# sha256 cellar: :any_skip_relocation, big_sur: "69489ae397e4645..."
# sha256 cellar: :any, catalina: "449de5ea35d0e94..."
# end
def sha256(hash)
sha256_regex = /^[a-f0-9]{64}$/i
# find new `sha256 big_sur: "69489ae397e4645..."` format
tag, digest = hash.find do |key, value|
key.is_a?(Symbol) && value.is_a?(String) && value.match?(sha256_regex)
end
cellar = hash[:cellar] if digest && tag
tag = Utils::Bottles::Tag.from_symbol(tag)
cellar ||= tag.default_cellar
collector.add(tag, checksum: Checksum.new(digest), cellar:)
end
sig {
params(tag: Utils::Bottles::Tag, no_older_versions: T::Boolean)
.returns(T.nilable(Utils::Bottles::TagSpecification))
}
def tag_specification_for(tag, no_older_versions: false)
collector.specification_for(tag, no_older_versions:)
end
def checksums
tags = collector.tags.sort_by do |tag|
version = tag.to_macos_version
# Give `arm64` bottles a higher priority so they are first.
priority = (tag.arch == :arm64) ? 2 : 1
"#{priority}.#{version}_#{tag}"
rescue MacOSVersion::Error
# Sort non-macOS tags below macOS tags.
"0.#{tag}"
end
tags.reverse.map do |tag|
spec = collector.specification_for(tag)
{
"tag" => spec.tag.to_sym,
"digest" => spec.checksum,
"cellar" => spec.cellar,
}
end
end
end
class PourBottleCheck
include OnSystem::MacOSAndLinux
def initialize(formula)
@formula = formula
end
def reason(reason)
@formula.pour_bottle_check_unsatisfied_reason = reason
end
def satisfy(&block)
@formula.send(:define_method, :pour_bottle?, &block)
end
end
require "extend/os/software_spec"

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "software_spec" require "bottle_specification"
require "test/support/fixtures/testball_bottle" require "test/support/fixtures/testball_bottle"
RSpec.describe Bottle do RSpec.describe Bottle do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "software_spec" require "bottle_specification"
RSpec.describe BottleSpecification do RSpec.describe BottleSpecification do
subject(:bottle_spec) { described_class.new } subject(:bottle_spec) { described_class.new }

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "software_spec" require "head_software_spec"
RSpec.describe HeadSoftwareSpec do RSpec.describe HeadSoftwareSpec do
subject(:head_spec) { described_class.new } subject(:head_spec) { described_class.new }