# typed: true # frozen_string_literal: true require "download_strategy" require "checksum" require "version" require "mktemp" require "livecheck" require "extend/on_system" # Resource is the fundamental representation of an external resource. The # primary formula download, along with other declared resources, are instances # of this class. # # @api private class Resource extend T::Sig include Context include FileUtils include OnSystem::MacOSAndLinux attr_reader :mirrors, :specs, :using, :source_modified_time, :patches, :owner attr_writer :version attr_accessor :download_strategy, :checksum # Formula name must be set after the DSL, as we have no access to the # formula name before initialization of the formula. attr_accessor :name def initialize(name = nil, &block) # Ensure this is synced with `initialize_dup` and `freeze` (excluding simple objects like integers and booleans) @name = name @url = nil @version = nil @mirrors = [] @specs = {} @checksum = nil @using = nil @patches = [] @livecheck = Livecheck.new(self) @livecheckable = false instance_eval(&block) if block end def initialize_dup(other) super @name = @name.dup @version = @version.dup @mirrors = @mirrors.dup @specs = @specs.dup @checksum = @checksum.dup @using = @using.dup @patches = @patches.dup @livecheck = @livecheck.dup end def freeze @name.freeze @version.freeze @mirrors.freeze @specs.freeze @checksum.freeze @using.freeze @patches.freeze @livecheck.freeze super end 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? @specs[:insecure] = !specs[:bottle] && !DevelopmentTools.ca_file_handles_most_https_certificates? end def downloader return @downloader if @downloader.present? url, *mirrors = determine_url_mirrors @downloader = download_strategy.new(url, download_name, version, mirrors: mirrors, **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 def download_name return owner.name if name.nil? return escaped_name if owner.nil? "#{owner.name}--#{escaped_name}" end def downloaded? cached_download.exist? end def cached_download downloader.cached_location end def clear_cache downloader.clear_cache end # Verifies download and unpacks it. # 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. # # @api public def stage(target = nil, debug_symbols: false, &block) raise ArgumentError, "Target directory or block is required" if !target && block.blank? prepare_patches fetch_patches(skip_downloaded: true) fetch unless downloaded? unpack(target, debug_symbols: debug_symbols, &block) end def prepare_patches patches.grep(DATAPatch) { |p| p.path = owner.owner.path } end def fetch_patches(skip_downloaded: false) external_patches = patches.select(&:external?) external_patches.reject!(&:downloaded?) if skip_downloaded external_patches.each(&:fetch) end def apply_patches return if patches.empty? 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 stage_resource(download_name, debug_symbols: 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 def fetch(verify_download_integrity: true) HOMEBREW_CACHE.mkpath fetch_patches begin downloader.fetch rescue ErrorDuringExecution, CurlDownloadStrategyError => e raise DownloadError.new(self, e) end download = cached_download verify_download_integrity(download) if verify_download_integrity download end def verify_download_integrity(filename) if filename.file? ohai "Verifying checksum for '#{filename.basename}'" if verbose? filename.verify_checksum(checksum) end rescue ChecksumMissingError opoo <<~EOS Cannot verify integrity of '#{filename.basename}'. No checksum was provided for this resource. For your reference, the checksum is: sha256 "#{filename.sha256}" EOS end # @!attribute [w] livecheck # {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. # #
livecheck do
  #   url "https://example.com/foo/releases"
  #   regex /foo-(\d+(?:\.\d+)+)\.tar/
  # end
def livecheck(&block) return @livecheck unless block @livecheckable = true @livecheck.instance_eval(&block) end # Whether a livecheck specification is defined or not. # 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) end def url(val = nil, **specs) return @url if val.nil? specs = specs.dup # Don't allow this to be set. specs.delete(:insecure) @url = val @using = specs.delete(:using) @download_strategy = DownloadStrategyDetector.detect(url, using) @specs.merge!(specs) @downloader = nil @version = detect_version(@version) end def version(val = nil) return @version if val.nil? @version = detect_version(val) end def mirror(val) mirrors << val end def patch(strip = :p1, src = nil, &block) p = Patch.create(strip, src, &block) patches << p end protected def stage_resource(prefix, debug_symbols: false, &block) Mktemp.new(prefix, retain_in_cache: debug_symbols).run(&block) end private def detect_version(val) version = case val when nil then url.nil? ? Version::NULL : Version.detect(url, **specs) when String then Version.create(val) when Version then val else raise TypeError, "version '#{val.inspect}' should be a string" end version unless version.null? end def determine_url_mirrors extra_urls = [] # glibc-bootstrap if url.start_with?("https://github.com/Homebrew/glibc-bootstrap/releases/download") if Homebrew::EnvConfig.artifact_domain.present? extra_urls << url.sub("https://github.com", Homebrew::EnvConfig.artifact_domain) 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 if Homebrew::EnvConfig.pip_index_url.present? pip_index_base_url = T.must(Homebrew::EnvConfig.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, url, *mirrors].uniq end # A resource containing a Go package. class Go < Resource def stage(target, &block) super(target/name, &block) end end # A resource containing a patch. class PatchResource < 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. # # @api private class ResourceStageContext extend T::Sig extend Forwardable # The {Resource} that is being staged. attr_reader :resource # 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 sig { returns(String) } def to_s "<#{self.class}: resource=#{resource} staging=#{staging}>" end end