brew/Library/Homebrew/resource.rb

399 lines
11 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "downloadable"
require "mktemp"
require "livecheck"
require "extend/on_system"
2013-09-17 21:25:38 -05:00
# Resource is the fundamental representation of an external resource. The
# primary formula download, along with other declared resources, are instances
# of this class.
2024-07-14 21:03:08 -04:00
class Resource
include Downloadable
include FileUtils
include OnSystem::MacOSAndLinux
attr_reader :source_modified_time, :patches, :owner
attr_writer :checksum
attr_accessor :download_strategy
2013-09-17 21:25:38 -05:00
# Formula name must be set after the DSL, as we have no access to the
# formula name before initialization of the formula.
2018-01-21 08:29:38 -08:00
attr_accessor :name
2023-03-25 08:36:56 -07:00
sig { params(name: T.nilable(String), block: T.nilable(T.proc.bind(Resource).void)).void }
def initialize(name = nil, &block)
super()
# Ensure this is synced with `initialize_dup` and `freeze` (excluding simple objects like integers and booleans)
@name = name
2018-01-21 08:29:38 -08:00
@patches = []
@livecheck = Livecheck.new(self)
@livecheckable = false
@insecure = false
2020-11-16 22:18:56 +01:00
instance_eval(&block) if block
end
def initialize_dup(other)
super
@name = @name.dup
@patches = @patches.dup
@livecheck = @livecheck.dup
end
def freeze
@name.freeze
@patches.freeze
@livecheck.freeze
super
end
2018-01-21 08:29:38 -08:00
def owner=(owner)
@owner = owner
patches.each { |p| p.owner = owner }
return if !owner.respond_to?(:full_name) || owner.full_name != "ca-certificates"
return if Homebrew::EnvConfig.no_insecure_redirect?
@insecure = !specs[:bottle] && (DevelopmentTools.ca_file_substitution_required? ||
DevelopmentTools.curl_substitution_required?)
return if @url.nil?
specs = if @insecure
@url.specs.merge({ insecure: true })
else
@url.specs.except(:insecure)
end
@url = URL.new(@url.to_s, specs)
end
# Removes /s from resource names; this allows Go package names
# to be used as resource names without confusing software that
# interacts with {download_name}, e.g. `github.com/foo/bar`.
def escaped_name
name.tr("/", "-")
end
2013-09-17 21:25:40 -05:00
def download_name
return owner.name if name.nil?
return escaped_name if owner.nil?
2018-09-17 02:45:00 +02:00
"#{owner.name}--#{escaped_name}"
2013-09-17 21:25:40 -05:00
end
# Verifies download and unpacks it.
2020-08-19 06:58:36 +02:00
# The block may call `|resource, staging| staging.retain!` to retain the staging
# directory. Subclasses that override stage should implement the tmp
# dir using {Mktemp} so that works with all subtypes.
2020-08-19 06:58:36 +02:00
#
# @api public
def stage(target = nil, debug_symbols: false, &block)
2023-02-10 23:15:40 -05:00
raise ArgumentError, "Target directory or block is required" if !target && block.blank?
2014-12-13 22:51:21 -05:00
prepare_patches
fetch_patches(skip_downloaded: true)
fetch unless downloaded?
2024-03-07 16:20:20 +00:00
unpack(target, debug_symbols:, &block)
end
def prepare_patches
2018-01-21 08:29:38 -08:00
patches.grep(DATAPatch) { |p| p.path = owner.owner.path }
end
def fetch_patches(skip_downloaded: false)
2020-05-14 09:20:58 +01:00
external_patches = patches.select(&:external?)
external_patches.reject!(&:downloaded?) if skip_downloaded
external_patches.each(&:fetch)
2018-01-21 08:29:38 -08:00
end
def apply_patches
return if patches.empty?
2018-09-17 02:45:00 +02:00
2018-01-21 08:29:38 -08:00
ohai "Patching #{name}"
patches.each(&:apply)
end
# If a target is given, unpack there; else unpack to a temp folder.
# If block is given, yield to that block with `|stage|`, where stage
# is a {ResourceStageContext}.
# A target or a block must be given, but not both.
def unpack(target = nil, debug_symbols: false)
current_working_directory = Pathname.pwd
2024-03-07 16:20:20 +00:00
stage_resource(download_name, debug_symbols:) do |staging|
downloader.stage do
@source_modified_time = downloader.source_modified_time
apply_patches
if block_given?
yield ResourceStageContext.new(self, staging)
elsif target
target = Pathname(target)
target = current_working_directory/target if target.relative?
target.install Pathname.pwd.children
end
end
end
end
Partial = Struct.new(:resource, :files)
def files(*files)
Partial.new(self, files)
end
2024-07-14 11:42:22 -04:00
sig {
override
.params(
verify_download_integrity: T::Boolean,
timeout: T.nilable(T.any(Integer, Float)),
quiet: T::Boolean,
).returns(Pathname)
}
def fetch(verify_download_integrity: true, timeout: nil, quiet: false)
fetch_patches
2024-05-23 17:08:41 +01:00
super
2013-09-17 21:25:38 -05:00
end
# {Livecheck} can be used to check for newer versions of the software.
# This method evaluates the DSL specified in the livecheck block of the
# {Resource} (if it exists) and sets the instance variables of a {Livecheck}
# object accordingly. This is used by `brew livecheck` to check for newer
# versions of the software.
#
# ### Example
#
# ```ruby
# livecheck do
# url "https://example.com/foo/releases"
# regex /foo-(\d+(?:\.\d+)+)\.tar/
# end
# ```
#
# @!attribute [w] livecheck
def livecheck(&block)
return @livecheck unless block
@livecheckable = true
@livecheck.instance_eval(&block)
end
# Whether a livecheck specification is defined or not.
2024-04-30 11:10:23 +02:00
# It returns true when a `livecheck` block is present in the {Resource} and
# false otherwise and is used by livecheck.
def livecheckable?
@livecheckable == true
end
def sha256(val)
@checksum = Checksum.new(val)
2013-09-17 21:25:38 -05:00
end
def url(val = nil, **specs)
return @url&.to_s if val.nil?
2018-09-17 02:45:00 +02:00
specs = specs.dup
# Don't allow this to be set.
specs.delete(:insecure)
specs[:insecure] = true if @insecure
@url = URL.new(val, specs)
@downloader = nil
@download_strategy = @url.download_strategy
2013-09-17 21:25:38 -05:00
end
2024-07-14 22:51:54 -04:00
sig { override.params(val: T.nilable(T.any(String, Version))).returns(T.nilable(Version)) }
def version(val = nil)
return super() if val.nil?
@version = case val
2023-05-01 07:59:36 +02:00
when String
val.blank? ? Version::NULL : Version.new(val)
when Version
val
end
2013-09-17 21:25:38 -05:00
end
def mirror(val)
2013-09-17 21:25:38 -05:00
mirrors << val
end
2018-01-21 08:29:38 -08:00
def patch(strip = :p1, src = nil, &block)
2024-07-14 11:42:22 -04:00
p = ::Patch.create(strip, src, &block)
2018-01-21 08:29:38 -08:00
patches << p
end
def using
@url&.using
end
def specs
@url&.specs || {}.freeze
end
protected
def stage_resource(prefix, debug_symbols: false, &block)
Mktemp.new(prefix, retain_in_cache: debug_symbols).run(&block)
end
2013-09-17 21:25:38 -05:00
private
def determine_url_mirrors
extra_urls = []
# glibc-bootstrap
if url.start_with?("https://github.com/Homebrew/glibc-bootstrap/releases/download")
2023-03-13 02:40:03 +01:00
if (artifact_domain = Homebrew::EnvConfig.artifact_domain.presence)
artifact_url = url.sub("https://github.com", artifact_domain)
return [artifact_url] if Homebrew::EnvConfig.artifact_domain_no_fallback?
extra_urls << artifact_url
end
if Homebrew::EnvConfig.bottle_domain != HOMEBREW_BOTTLE_DEFAULT_DOMAIN
tag, filename = url.split("/").last(2)
extra_urls << "#{Homebrew::EnvConfig.bottle_domain}/glibc-bootstrap/#{tag}/#{filename}"
end
end
# PyPI packages: PEP 503 Simple Repository API <https://peps.python.org/pep-0503>
2023-03-13 02:38:03 +01:00
if (pip_index_url = Homebrew::EnvConfig.pip_index_url.presence)
pip_index_base_url = pip_index_url.chomp("/").chomp("/simple")
%w[https://files.pythonhosted.org https://pypi.org].each do |base_url|
extra_urls << url.sub(base_url, pip_index_base_url) if url.start_with?("#{base_url}/packages")
end
end
[*extra_urls, *super].uniq
end
# A local resource that doesn't need to be downloaded.
class Local < Resource
def initialize(path)
super(File.basename(path))
2024-09-04 23:12:58 +02:00
@downloader = LocalBottleDownloadStrategy.new(path)
end
end
2024-07-14 11:42:22 -04:00
# A resource for a formula.
class Formula < Resource
sig { override.returns(String) }
def name
T.must(owner).name
end
sig { override.returns(String) }
def download_name
name
end
end
2020-08-19 06:58:36 +02:00
# A resource containing a Go package.
class Go < Resource
2020-11-24 15:46:47 +01:00
def stage(target, &block)
super(target/name, &block)
end
end
2024-07-13 16:28:21 -04:00
# A resource for a bottle manifest.
class BottleManifest < Resource
class Error < RuntimeError; end
2024-07-13 16:28:21 -04:00
attr_reader :bottle
def initialize(bottle)
super("#{bottle.name}_bottle_manifest")
@bottle = bottle
end
def verify_download_integrity(_filename)
# We don't have a checksum, but we can at least try parsing it.
tab
2024-07-13 16:28:21 -04:00
end
def tab
json = begin
JSON.parse(cached_download.read)
rescue JSON::ParserError
raise Error, "The downloaded GitHub Packages manifest was corrupted or modified (it is not valid JSON): " \
"\n#{cached_download}"
2024-07-13 16:28:21 -04:00
end
manifests = json["manifests"]
raise Error, "Missing 'manifests' section." if manifests.blank?
2024-07-13 16:28:21 -04:00
manifests_annotations = manifests.filter_map { |m| m["annotations"] }
raise Error, "Missing 'annotations' section." if manifests_annotations.blank?
2024-07-13 16:28:21 -04:00
bottle_digest = bottle.resource.checksum.hexdigest
image_ref = GitHubPackages.version_rebuild(bottle.resource.version, bottle.rebuild, bottle.tag.to_s)
manifest_annotations = manifests_annotations.find do |m|
next if m["sh.brew.bottle.digest"] != bottle_digest
m["org.opencontainers.image.ref.name"] == image_ref
end
raise Error, "Couldn't find manifest matching bottle checksum." if manifest_annotations.blank?
2024-07-13 16:28:21 -04:00
tab = manifest_annotations["sh.brew.tab"]
raise Error, "Couldn't find tab from manifest." if tab.blank?
2024-07-13 16:28:21 -04:00
begin
JSON.parse(tab)
rescue JSON::ParserError
raise Error, "Couldn't parse tab JSON."
2024-07-13 16:28:21 -04:00
end
end
end
2020-08-19 06:58:36 +02:00
# A resource containing a patch.
2024-07-14 11:42:22 -04:00
class Patch < Resource
attr_reader :patch_files
def initialize(&block)
@patch_files = []
@directory = nil
super "patch", &block
end
def apply(*paths)
paths.flatten!
@patch_files.concat(paths)
@patch_files.uniq!
end
def directory(val = nil)
return @directory if val.nil?
@directory = val
end
end
end
# The context in which a {Resource#stage} occurs. Supports access to both
# the {Resource} and associated {Mktemp} in a single block argument. The interface
# is back-compatible with {Resource} itself as used in that context.
class ResourceStageContext
extend Forwardable
# The {Resource} that is being staged.
attr_reader :resource
2024-04-26 14:04:55 +02:00
# The {Mktemp} in which {#resource} is staged.
attr_reader :staging
def_delegators :@resource, :version, :url, :mirrors, :specs, :using, :source_modified_time
def_delegators :@staging, :retain!
def initialize(resource, staging)
@resource = resource
@staging = staging
end
2020-10-20 12:03:48 +02:00
sig { returns(String) }
def to_s
"<#{self.class}: resource=#{resource} staging=#{staging}>"
end
end