From 44f058edb514c0cb10deefe62397963a57a94334 Mon Sep 17 00:00:00 2001 From: Bo Anderson Date: Tue, 18 Apr 2023 00:22:13 +0100 Subject: [PATCH] Refactor formula, cask and Ruby source downloads to use shared code --- Library/Homebrew/api.rb | 56 +-- Library/Homebrew/api/cask.rb | 26 +- Library/Homebrew/api/download.rb | 45 ++ Library/Homebrew/api/formula.rb | 19 + Library/Homebrew/cask/cask_loader.rb | 51 ++- Library/Homebrew/cask/download.rb | 86 ++-- Library/Homebrew/cask/installer.rb | 8 +- Library/Homebrew/cask/url.rb | 420 +++++++++--------- Library/Homebrew/cask/url.rbi | 6 +- Library/Homebrew/cleanup.rb | 74 ++- Library/Homebrew/dev-cmd/bump-formula-pr.rb | 2 +- Library/Homebrew/downloadable.rb | 133 ++++++ Library/Homebrew/exceptions.rb | 9 +- Library/Homebrew/formula.rb | 32 +- Library/Homebrew/formula_installer.rb | 11 +- Library/Homebrew/formulary.rb | 86 ++-- Library/Homebrew/patch.rb | 4 +- Library/Homebrew/resource.rb | 128 ++---- Library/Homebrew/software_spec.rb | 21 +- Library/Homebrew/tap_constants.rb | 5 +- Library/Homebrew/test/api/cask_spec.rb | 10 - Library/Homebrew/test/api_spec.rb | 15 - Library/Homebrew/test/cask/cask_spec.rb | 2 +- Library/Homebrew/test/cask/download_spec.rb | 1 + Library/Homebrew/test/cask/installer_spec.rb | 6 +- Library/Homebrew/test/formula_spec.rb | 4 +- Library/Homebrew/test/formulary_spec.rb | 4 +- Library/Homebrew/test/resource_spec.rb | 2 +- ...testball_bottle-0.1.yosemite.bottle.tar.gz | Bin 1991 -> 2015 bytes .../test/support/fixtures/failball.rb | 2 +- .../test/support/fixtures/testball.rb | 2 +- .../test/support/fixtures/testball_bottle.rb | 4 +- .../fixtures/testball_bottle_cellar.rb | 4 +- Library/Homebrew/url.rb | 33 ++ 34 files changed, 769 insertions(+), 542 deletions(-) create mode 100644 Library/Homebrew/api/download.rb create mode 100644 Library/Homebrew/downloadable.rb create mode 100644 Library/Homebrew/url.rb diff --git a/Library/Homebrew/api.rb b/Library/Homebrew/api.rb index ebdb3d77df..2efe1bd7fe 100644 --- a/Library/Homebrew/api.rb +++ b/Library/Homebrew/api.rb @@ -14,6 +14,7 @@ module Homebrew extend Cachable HOMEBREW_CACHE_API = (HOMEBREW_CACHE/"api").freeze + HOMEBREW_CACHE_API_SOURCE = (HOMEBREW_CACHE/"api-source").freeze sig { params(endpoint: String).returns(Hash) } def self.fetch(endpoint) @@ -114,50 +115,6 @@ module Homebrew end end - sig { - params(name: String, path: T.any(Pathname, String), git_head: String, - sha256: T.nilable(String)).returns(String) - } - def self.fetch_homebrew_cask_source(name, path:, git_head:, sha256: nil) - # TODO: unify with formula logic (https://github.com/Homebrew/brew/issues/14746) - raw_endpoint = "#{git_head}/#{path}" - return cache[raw_endpoint] if cache.present? && cache.key?(raw_endpoint) - - # This API sometimes returns random 404s so needs a fallback at formulae.brew.sh. - raw_source_url = "https://raw.githubusercontent.com/Homebrew/homebrew-cask/#{raw_endpoint}" - api_source_url = "#{HOMEBREW_API_DEFAULT_DOMAIN}/cask-source/#{name}.rb" - - url = raw_source_url - output = Utils::Curl.curl_output("--fail", url) - - if !output.success? || output.blank? - url = api_source_url - output = Utils::Curl.curl_output("--fail", url) - if !output.success? || output.blank? - raise ArgumentError, <<~EOS - No valid file found at either of: - #{Tty.underline}#{raw_source_url}#{Tty.reset} - #{Tty.underline}#{api_source_url}#{Tty.reset} - EOS - end - end - - cask_source = output.stdout - actual_sha256 = Digest::SHA256.hexdigest(cask_source) - if sha256 && actual_sha256 != sha256 - raise ArgumentError, <<~EOS - SHA256 mismatch - Expected: #{Formatter.success(sha256.to_s)} - Actual: #{Formatter.error(actual_sha256.to_s)} - URL: #{url} - Check if you can access the URL in your browser. - Regardless, try again in a few minutes. - EOS - end - - cache[raw_endpoint] = cask_source - end - sig { params(json: Hash).returns(Hash) } def self.merge_variations(json) bottle_tag = ::Utils::Bottles::Tag.new(system: Homebrew::SimulateSystem.current_os, @@ -207,5 +164,16 @@ module Homebrew [true, JSON.parse(json_data["payload"])] end + + sig { params(path: Pathname).returns(T.nilable(Tap)) } + def self.tap_from_source_download(path) + source_relative_path = path.relative_path_from(Homebrew::API::HOMEBREW_CACHE_API_SOURCE) + return if source_relative_path.to_s.start_with?("../") + + org, repo = source_relative_path.each_filename.first(2) + return if org.blank? || repo.blank? + + Tap.fetch(org, repo) + end end end diff --git a/Library/Homebrew/api/cask.rb b/Library/Homebrew/api/cask.rb index 50c22b981d..f3c6a96f81 100644 --- a/Library/Homebrew/api/cask.rb +++ b/Library/Homebrew/api/cask.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "extend/cachable" +require "api/download" module Homebrew module API @@ -19,12 +20,25 @@ module Homebrew Homebrew::API.fetch "cask/#{token}.json" end - sig { - params(token: String, path: T.any(String, Pathname), git_head: String, - sha256: T.nilable(String)).returns(String) - } - def fetch_source(token, path:, git_head:, sha256: nil) - Homebrew::API.fetch_homebrew_cask_source token, path: path, git_head: git_head, sha256: sha256 + sig { params(cask: ::Cask::Cask).returns(::Cask::Cask) } + def source_download(cask) + path = cask.ruby_source_path.to_s || "Casks/#{cask.token}.rb" + sha256 = cask.ruby_source_checksum[:sha256] + checksum = Checksum.new(sha256) if sha256 + git_head = cask.tap_git_head || "HEAD" + tap = cask.tap&.full_name || "Homebrew/homebrew-cask" + + download = Homebrew::API::Download.new( + "https://raw.githubusercontent.com/#{tap}/#{git_head}/#{path}", + checksum, + mirrors: [ + "#{HOMEBREW_API_DEFAULT_DOMAIN}/cask-source/#{File.basename(path)}", + ], + cache: HOMEBREW_CACHE_API_SOURCE/"#{tap}/#{git_head}/Cask", + ) + download.fetch + ::Cask::CaskLoader::FromPathLoader.new(download.symlink_location) + .load(config: cask.config) end sig { returns(T::Boolean) } diff --git a/Library/Homebrew/api/download.rb b/Library/Homebrew/api/download.rb new file mode 100644 index 0000000000..8dee0067ac --- /dev/null +++ b/Library/Homebrew/api/download.rb @@ -0,0 +1,45 @@ +# typed: true +# frozen_string_literal: true + +require "downloadable" + +module Homebrew + module API + # @api private + class DownloadStrategy < CurlDownloadStrategy + sig { override.returns(Pathname) } + def symlink_location + cache/name + end + end + + # @api private + class Download < Downloadable + sig { + params( + url: String, + checksum: T.nilable(Checksum), + mirrors: T::Array[String], + cache: T.nilable(Pathname), + ).void + } + def initialize(url, checksum, mirrors: [], cache: nil) + super() + @url = URL.new(url, using: API::DownloadStrategy) + @checksum = checksum + @mirrors = mirrors + @cache = cache + end + + sig { override.returns(Pathname) } + def cache + @cache || super + end + + sig { returns(Pathname) } + def symlink_location + T.cast(downloader, API::DownloadStrategy).symlink_location + end + end + end +end diff --git a/Library/Homebrew/api/formula.rb b/Library/Homebrew/api/formula.rb index 7a0a390827..1de2bf2378 100644 --- a/Library/Homebrew/api/formula.rb +++ b/Library/Homebrew/api/formula.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "extend/cachable" +require "api/download" module Homebrew module API @@ -19,6 +20,24 @@ module Homebrew Homebrew::API.fetch "formula/#{name}.json" end + sig { params(formula: ::Formula).returns(::Formula) } + def source_download(formula) + path = formula.ruby_source_path || "Formula/#{formula.name}.rb" + git_head = formula.tap_git_head || "HEAD" + tap = formula.tap&.full_name || "Homebrew/homebrew-core" + + download = Homebrew::API::Download.new( + "https://raw.githubusercontent.com/#{tap}/#{git_head}/#{path}", + formula.ruby_source_checksum, + cache: HOMEBREW_CACHE_API_SOURCE/"#{tap}/#{git_head}/Formula", + ) + download.fetch + Formulary.factory(download.symlink_location, + formula.active_spec_sym, + alias_path: formula.alias_path, + flags: formula.class.build_flags) + end + sig { returns(T::Boolean) } def download_and_cache_data! json_formulae, updated = Homebrew::API.fetch_json_api_file "formula.jws.json", diff --git a/Library/Homebrew/cask/cask_loader.rb b/Library/Homebrew/cask/cask_loader.rb index 00d5a5dd2d..4c2302a8c8 100644 --- a/Library/Homebrew/cask/cask_loader.rb +++ b/Library/Homebrew/cask/cask_loader.rb @@ -21,10 +21,33 @@ module Cask end # Loads a cask from a string. - class FromContentLoader + class AbstractContentLoader include ILoader - attr_reader :content, :tap + extend T::Helpers + abstract! + sig { returns(String) } + attr_reader :content + + sig { returns(T.nilable(Tap)) } + attr_reader :tap + + private + + sig { + overridable.params( + header_token: String, + options: T.untyped, + block: T.nilable(T.proc.bind(DSL).void), + ).returns(Cask) + } + def cask(header_token, **options, &block) + Cask.new(header_token, source: content, tap: tap, **options, config: @config, &block) + end + end + + # Loads a cask from a string. + class FromContentLoader < AbstractContentLoader def self.can_load?(ref) return false unless ref.respond_to?(:to_str) @@ -42,6 +65,8 @@ module Cask end def initialize(content, tap: nil) + super() + @content = content.force_encoding("UTF-8") @tap = tap end @@ -51,16 +76,10 @@ module Cask instance_eval(content, __FILE__, __LINE__) end - - private - - def cask(header_token, **options, &block) - Cask.new(header_token, source: content, tap: tap, **options, config: @config, &block) - end end # Loads a cask from a path. - class FromPathLoader < FromContentLoader + class FromPathLoader < AbstractContentLoader def self.can_load?(ref) path = Pathname(ref) %w[.rb .json].include?(path.extname) && path.expand_path.exist? @@ -68,11 +87,15 @@ module Cask attr_reader :token, :path - def initialize(path) # rubocop:disable Lint/MissingSuper + def initialize(path, token: nil) + super() + path = Pathname(path).expand_path @token = path.basename(path.extname).to_s + @path = path + @tap = Homebrew::API.tap_from_source_download(path) end def load(config:) @@ -153,8 +176,8 @@ module Cask end def initialize(path) - @tap = Tap.from_path(path) super(path) + @tap = Tap.from_path(path) end end @@ -172,7 +195,7 @@ module Cask end def load(config:) - raise TapCaskUnavailableError.new(tap, token) unless tap.installed? + raise TapCaskUnavailableError.new(tap, token) unless T.must(tap).installed? super end @@ -215,12 +238,12 @@ module Cask return false unless ref.is_a?(String) return false unless ref.match?(HOMEBREW_MAIN_TAP_CASK_REGEX) - token = ref.delete_prefix("homebrew/cask/") + token = ref.sub(%r{^homebrew/(?:homebrew-)?cask/}i, "") Homebrew::API::Cask.all_casks.key?(token) end def initialize(token, from_json: nil) - @token = token.delete_prefix("homebrew/cask/") + @token = token.sub(%r{^homebrew/(?:homebrew-)?cask/}i, "") @path = CaskLoader.default_path(token) @from_json = from_json end diff --git a/Library/Homebrew/cask/download.rb b/Library/Homebrew/cask/download.rb index c902fbc933..f6c2f3ff5c 100644 --- a/Library/Homebrew/cask/download.rb +++ b/Library/Homebrew/cask/download.rb @@ -1,6 +1,7 @@ # typed: true # frozen_string_literal: true +require "downloadable" require "fileutils" require "cask/cache" require "cask/quarantine" @@ -9,71 +10,75 @@ module Cask # A download corresponding to a {Cask}. # # @api private - class Download + class Download < ::Downloadable include Context attr_reader :cask def initialize(cask, quarantine: nil) + super() + @cask = cask @quarantine = quarantine end + sig { override.returns(T.nilable(::URL)) } + def url + @url ||= ::URL.new(cask.url.to_s, cask.url.specs) + end + + sig { override.returns(T.nilable(::Checksum)) } + def checksum + @checksum ||= cask.sha256 if cask.sha256 != :no_check + end + + sig { override.returns(T.nilable(::Version)) } + def version + @version ||= ::Version.create(cask.version) + end + + sig { + override + .params(quiet: T.nilable(T::Boolean), + verify_download_integrity: T::Boolean, + timeout: T.nilable(T.any(Integer, Float))) + .returns(Pathname) + } def fetch(quiet: nil, verify_download_integrity: true, timeout: nil) - downloaded_path = begin - downloader.shutup! if quiet - downloader.fetch(timeout: timeout) - downloader.cached_location - rescue => e - error = CaskError.new("Download failed on Cask '#{cask}' with message: #{e}") + downloader.shutup! if quiet + + begin + super(verify_download_integrity: false, timeout: timeout) + rescue DownloadError => e + error = CaskError.new("Download failed on Cask '#{cask}' with message: #{e.cause}") error.set_backtrace e.backtrace raise error end + + downloaded_path = cached_download quarantine(downloaded_path) self.verify_download_integrity(downloaded_path) if verify_download_integrity downloaded_path end - def downloader - @downloader ||= begin - strategy = DownloadStrategyDetector.detect(cask.url.to_s, cask.url.using) - strategy.new(cask.url.to_s, cask.token, cask.version, cache: Cache.path, **cask.url.specs) - end - end - def time_file_size(timeout: nil) - downloader.resolved_time_file_size(timeout: timeout) - end + raise ArgumentError, "not supported for this download strategy" unless downloader.is_a?(CurlDownloadStrategy) - def clear_cache - downloader.clear_cache - end - - def cached_download - downloader.cached_location + T.cast(downloader, CurlDownloadStrategy).resolved_time_file_size(timeout: timeout) end def basename downloader.basename end + sig { override.params(filename: Pathname).void } def verify_download_integrity(filename) if @cask.sha256 == :no_check opoo "No checksum defined for cask '#{@cask}', skipping verification." return end - begin - ohai "Verifying checksum for cask '#{@cask}'" if verbose? - filename.verify_checksum(@cask.sha256) - rescue ChecksumMissingError - opoo <<~EOS - Cannot verify integrity of '#{filename.basename}'. - No checksum was provided for this cask. - For your reference, the checksum is: - sha256 "#{filename.sha256}" - EOS - end + super end private @@ -88,5 +93,20 @@ module Cask Quarantine.release!(download_path: path) end end + + sig { override.returns(String) } + def download_name + cask.token + end + + sig { override.returns(T.nilable(::URL)) } + def determine_url + url + end + + sig { override.returns(Pathname) } + def cache + Cache.path + end end end diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index e52f83cb17..d5664b6b6e 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -562,13 +562,7 @@ on_request: true) end def load_cask_from_source_api! - cask_source = Homebrew::API::Cask.fetch_source( - @cask.token, - path: @cask.ruby_source_path || "Casks/#{@cask.token}.rb", - git_head: @cask.tap_git_head, - sha256: @cask.ruby_source_checksum["sha256"], - ) - @cask = CaskLoader::FromContentLoader.new(cask_source, tap: @cask.tap).load(config: @cask.config) + @cask = Homebrew::API::Cask.source_download(@cask) end end end diff --git a/Library/Homebrew/cask/url.rb b/Library/Homebrew/cask/url.rb index f7ef0197d3..e85241f4b8 100644 --- a/Library/Homebrew/cask/url.rb +++ b/Library/Homebrew/cask/url.rb @@ -1,42 +1,166 @@ # typed: true # frozen_string_literal: true -# Class corresponding to the `url` stanza. -# -# @api private -class URL < Delegator +module Cask + # Class corresponding to the `url` stanza. + # # @api private - class DSL - attr_reader :uri, :specs, - :verified, :using, - :tag, :branch, :revisions, :revision, - :trust_cert, :cookies, :referer, :header, :user_agent, - :data, :only_path + class URL < Delegator + # @api private + class DSL + attr_reader :uri, :specs, + :verified, :using, + :tag, :branch, :revisions, :revision, + :trust_cert, :cookies, :referer, :header, :user_agent, + :data, :only_path - extend Forwardable - def_delegators :uri, :path, :scheme, :to_s + extend Forwardable + def_delegators :uri, :path, :scheme, :to_s + + # @api public + sig { + params( + uri: T.any(URI::Generic, String), + verified: T.nilable(String), + using: T.nilable(Symbol), + tag: T.nilable(String), + branch: T.nilable(String), + revisions: T.nilable(T::Array[String]), + revision: T.nilable(String), + trust_cert: T.nilable(T::Boolean), + cookies: T.nilable(T::Hash[String, String]), + referer: T.nilable(T.any(URI::Generic, String)), + header: T.nilable(String), + user_agent: T.nilable(T.any(Symbol, String)), + data: T.nilable(T::Hash[String, String]), + only_path: T.nilable(String), + ).void + } + def initialize( + uri, + verified: nil, + using: nil, + tag: nil, + branch: nil, + revisions: nil, + revision: nil, + trust_cert: nil, + cookies: nil, + referer: nil, + header: nil, + user_agent: nil, + data: nil, + only_path: nil + ) + + @uri = URI(uri) + + specs = {} + specs[:verified] = @verified = verified + specs[:using] = @using = using + specs[:tag] = @tag = tag + specs[:branch] = @branch = branch + specs[:revisions] = @revisions = revisions + specs[:revision] = @revision = revision + specs[:trust_cert] = @trust_cert = trust_cert + specs[:cookies] = @cookies = cookies + specs[:referer] = @referer = referer + specs[:header] = @header = header + specs[:user_agent] = @user_agent = user_agent || :default + specs[:data] = @data = data + specs[:only_path] = @only_path = only_path + + @specs = specs.compact + end + end + + # @api private + class BlockDSL + # To access URL associated with page contents. + module PageWithURL + # @api public + sig { returns(URI::Generic) } + attr_accessor :url + end + + sig { + params( + uri: T.nilable(T.any(URI::Generic, String)), + dsl: T.nilable(::Cask::DSL), + block: T.proc.params(arg0: T.all(String, PageWithURL)).returns(T.untyped), + ).void + } + def initialize(uri, dsl: nil, &block) + @uri = uri + @dsl = dsl + @block = block + end + + sig { returns(T.untyped) } + def call + if @uri + result = curl_output("--fail", "--silent", "--location", @uri) + result.assert_success! + + page = result.stdout + page.extend PageWithURL + page.url = URI(@uri) + + instance_exec(page, &@block) + else + instance_exec(&@block) + end + end + + # @api public + sig { + params( + uri: T.any(URI::Generic, String), + block: T.proc.params(arg0: T.all(String, PageWithURL)).returns(T.untyped), + ).void + } + def url(uri, &block) + self.class.new(uri, &block).call + end + private :url + + # @api public + def method_missing(method, *args, &block) + if @dsl.respond_to?(method) + T.unsafe(@dsl).public_send(method, *args, &block) + else + super + end + end + + def respond_to_missing?(method, include_all) + @dsl.respond_to?(method, include_all) || super + end + end - # @api public sig { params( - uri: T.any(URI::Generic, String), - verified: T.nilable(String), - using: T.nilable(Symbol), - tag: T.nilable(String), - branch: T.nilable(String), - revisions: T.nilable(T::Array[String]), - revision: T.nilable(String), - trust_cert: T.nilable(T::Boolean), - cookies: T.nilable(T::Hash[String, String]), - referer: T.nilable(T.any(URI::Generic, String)), - header: T.nilable(String), - user_agent: T.nilable(T.any(Symbol, String)), - data: T.nilable(T::Hash[String, String]), - only_path: T.nilable(String), + uri: T.nilable(T.any(URI::Generic, String)), + verified: T.nilable(String), + using: T.nilable(Symbol), + tag: T.nilable(String), + branch: T.nilable(String), + revisions: T.nilable(T::Array[String]), + revision: T.nilable(String), + trust_cert: T.nilable(T::Boolean), + cookies: T.nilable(T::Hash[String, String]), + referer: T.nilable(T.any(URI::Generic, String)), + header: T.nilable(String), + user_agent: T.nilable(T.any(Symbol, String)), + data: T.nilable(T::Hash[String, String]), + only_path: T.nilable(String), + caller_location: Thread::Backtrace::Location, + dsl: T.nilable(::Cask::DSL), + block: T.nilable(T.proc.params(arg0: T.all(String, BlockDSL::PageWithURL)).returns(T.untyped)), ).void } def initialize( - uri, + uri = nil, verified: nil, using: nil, tag: nil, @@ -49,198 +173,76 @@ class URL < Delegator header: nil, user_agent: nil, data: nil, - only_path: nil + only_path: nil, + caller_location: T.must(caller_locations).fetch(0), + dsl: nil, + &block ) - - @uri = URI(uri) - - specs = {} - specs[:verified] = @verified = verified - specs[:using] = @using = using - specs[:tag] = @tag = tag - specs[:branch] = @branch = branch - specs[:revisions] = @revisions = revisions - specs[:revision] = @revision = revision - specs[:trust_cert] = @trust_cert = trust_cert - specs[:cookies] = @cookies = cookies - specs[:referer] = @referer = referer - specs[:header] = @header = header - specs[:user_agent] = @user_agent = user_agent || :default - specs[:data] = @data = data - specs[:only_path] = @only_path = only_path - - @specs = specs.compact - end - end - - # @api private - class BlockDSL - # To access URL associated with page contents. - module PageWithURL - # @api public - sig { returns(URI::Generic) } - attr_accessor :url - end - - sig { - params( - uri: T.nilable(T.any(URI::Generic, String)), - dsl: T.nilable(Cask::DSL), - block: T.proc.params(arg0: T.all(String, PageWithURL)).returns(T.untyped), - ).void - } - def initialize(uri, dsl: nil, &block) - @uri = uri - @dsl = dsl - @block = block - end - - sig { returns(T.untyped) } - def call - if @uri - result = curl_output("--fail", "--silent", "--location", @uri) - result.assert_success! - - page = result.stdout - page.extend PageWithURL - page.url = URI(@uri) - - instance_exec(page, &@block) - else - instance_exec(&@block) - end - end - - # @api public - sig { - params( - uri: T.any(URI::Generic, String), - block: T.proc.params(arg0: T.all(String, PageWithURL)).returns(T.untyped), - ).void - } - def url(uri, &block) - self.class.new(uri, &block).call - end - private :url - - # @api public - def method_missing(method, *args, &block) - if @dsl.respond_to?(method) - T.unsafe(@dsl).public_send(method, *args, &block) - else - super - end - end - - def respond_to_missing?(method, include_all) - @dsl.respond_to?(method, include_all) || super - end - end - - sig { - params( - uri: T.nilable(T.any(URI::Generic, String)), - verified: T.nilable(String), - using: T.nilable(Symbol), - tag: T.nilable(String), - branch: T.nilable(String), - revisions: T.nilable(T::Array[String]), - revision: T.nilable(String), - trust_cert: T.nilable(T::Boolean), - cookies: T.nilable(T::Hash[String, String]), - referer: T.nilable(T.any(URI::Generic, String)), - header: T.nilable(String), - user_agent: T.nilable(T.any(Symbol, String)), - data: T.nilable(T::Hash[String, String]), - only_path: T.nilable(String), - caller_location: Thread::Backtrace::Location, - dsl: T.nilable(Cask::DSL), - block: T.nilable(T.proc.params(arg0: T.all(String, BlockDSL::PageWithURL)).returns(T.untyped)), - ).void - } - def initialize( - uri = nil, - verified: nil, - using: nil, - tag: nil, - branch: nil, - revisions: nil, - revision: nil, - trust_cert: nil, - cookies: nil, - referer: nil, - header: nil, - user_agent: nil, - data: nil, - only_path: nil, - caller_location: T.must(caller_locations).fetch(0), - dsl: nil, - &block - ) - super( - if block - LazyObject.new do - *args = BlockDSL.new(uri, dsl: dsl, &block).call - options = args.last.is_a?(Hash) ? args.pop : {} - uri = T.let(args.first, T.any(URI::Generic, String)) - DSL.new(uri, **options) + super( + if block + LazyObject.new do + *args = BlockDSL.new(uri, dsl: dsl, &block).call + options = args.last.is_a?(Hash) ? args.pop : {} + uri = T.let(args.first, T.any(URI::Generic, String)) + DSL.new(uri, **options) + end + else + DSL.new( + T.must(uri), + verified: verified, + using: using, + tag: tag, + branch: branch, + revisions: revisions, + revision: revision, + trust_cert: trust_cert, + cookies: cookies, + referer: referer, + header: header, + user_agent: user_agent, + data: data, + only_path: only_path, + ) end - else - DSL.new( - T.must(uri), - verified: verified, - using: using, - tag: tag, - branch: branch, - revisions: revisions, - revision: revision, - trust_cert: trust_cert, - cookies: cookies, - referer: referer, - header: header, - user_agent: user_agent, - data: data, - only_path: only_path, - ) - end - ) + ) - @from_block = !block.nil? - @caller_location = caller_location - end + @from_block = !block.nil? + @caller_location = caller_location + end - def __getobj__ - @dsl - end + def __getobj__ + @dsl + end - def __setobj__(dsl) - @dsl = dsl - end + def __setobj__(dsl) + @dsl = dsl + end - sig { returns(T.nilable(String)) } - def raw_interpolated_url - return @raw_interpolated_url if defined?(@raw_interpolated_url) + sig { returns(T.nilable(String)) } + def raw_interpolated_url + return @raw_interpolated_url if defined?(@raw_interpolated_url) - @raw_interpolated_url = - Pathname(@caller_location.path) - .each_line.drop(@caller_location.lineno - 1) - .first&.then { |line| line[/url\s+"([^"]+)"/, 1] } - end - private :raw_interpolated_url + @raw_interpolated_url = + Pathname(@caller_location.path) + .each_line.drop(@caller_location.lineno - 1) + .first&.then { |line| line[/url\s+"([^"]+)"/, 1] } + end + private :raw_interpolated_url - sig { params(ignore_major_version: T::Boolean).returns(T::Boolean) } - def unversioned?(ignore_major_version: false) - interpolated_url = raw_interpolated_url + sig { params(ignore_major_version: T::Boolean).returns(T::Boolean) } + def unversioned?(ignore_major_version: false) + interpolated_url = raw_interpolated_url - return false unless interpolated_url + return false unless interpolated_url - interpolated_url = interpolated_url.gsub(/\#{\s*version\s*\.major\s*}/, "") if ignore_major_version + interpolated_url = interpolated_url.gsub(/\#{\s*version\s*\.major\s*}/, "") if ignore_major_version - interpolated_url.exclude?('#{') - end + interpolated_url.exclude?('#{') + end - sig { returns(T::Boolean) } - def from_block? - @from_block + sig { returns(T::Boolean) } + def from_block? + @from_block + end end end diff --git a/Library/Homebrew/cask/url.rbi b/Library/Homebrew/cask/url.rbi index 9070b2fc01..7675ab841e 100644 --- a/Library/Homebrew/cask/url.rbi +++ b/Library/Homebrew/cask/url.rbi @@ -1,5 +1,7 @@ # typed: strict -class URL - include Kernel +module Cask + class URL + include Kernel + end end diff --git a/Library/Homebrew/cleanup.rb b/Library/Homebrew/cleanup.rb index 36a5ddafe9..416b7351bc 100644 --- a/Library/Homebrew/cleanup.rb +++ b/Library/Homebrew/cleanup.rb @@ -51,11 +51,15 @@ module Homebrew pathname.mtime < days_ago && pathname.ctime < days_ago end - sig { params(pathname: Pathname, scrub: T::Boolean).returns(T::Boolean) } - def stale?(pathname, scrub: false) + sig { params(entry: { path: Pathname, type: T.nilable(Symbol) }, scrub: T::Boolean).returns(T::Boolean) } + def stale?(entry, scrub: false) + pathname = entry[:path] return false unless pathname.resolved_path.file? - if pathname.dirname.basename.to_s == "Cask" + case entry[:type] + when :api_source + stale_api_source?(pathname, scrub) + when :cask stale_cask?(pathname, scrub) else stale_formula?(pathname, scrub) @@ -64,6 +68,31 @@ module Homebrew private + sig { params(pathname: Pathname, scrub: T::Boolean).returns(T::Boolean) } + def stale_api_source?(pathname, scrub) + return true if scrub + + org, repo, git_head, type, basename = pathname.each_filename.to_a.last(5) + + name = "#{org}/#{repo}/#{File.basename(T.must(basename), ".rb")}" + package = if type == "Cask" + begin + Cask::CaskLoader.load(name) + rescue Cask::CaskError + nil + end + else + begin + Formulary.factory(name) + rescue FormulaUnavailableError + nil + end + end + return true if package.nil? + + package.tap_git_head != git_head + end + sig { params(pathname: Pathname, scrub: T::Boolean).returns(T::Boolean) } def stale_formula?(pathname, scrub) return false unless HOMEBREW_CELLAR.directory? @@ -235,6 +264,7 @@ module Homebrew Cleanup.autoremove(dry_run: dry_run?) if Homebrew::EnvConfig.autoremove? cleanup_cache + cleanup_empty_api_source_directories cleanup_logs cleanup_lockfiles cleanup_python_site_packages @@ -287,14 +317,14 @@ module Homebrew def cleanup_formula(formula, quiet: false, ds_store: true, cache_db: true) formula.eligible_kegs_for_cleanup(quiet: quiet) .each(&method(:cleanup_keg)) - cleanup_cache(Pathname.glob(cache/"#{formula.name}--*")) + cleanup_cache(Pathname.glob(cache/"#{formula.name}--*").map { |path| { path: path, type: nil } }) rm_ds_store([formula.rack]) if ds_store cleanup_cache_db(formula.rack) if cache_db cleanup_lockfiles(FormulaLock.new(formula.name).path) end def cleanup_cask(cask, ds_store: true) - cleanup_cache(Pathname.glob(cache/"Cask/#{cask.token}--*")) + cleanup_cache(Pathname.glob(cache/"Cask/#{cask.token}--*").map { |path| { path: path, type: :cask } }) rm_ds_store([cask.caskroom_path]) if ds_store cleanup_lockfiles(CaskLock.new(cask.token).path) end @@ -316,16 +346,35 @@ module Homebrew end end + def cache_files + files = cache.directory? ? cache.children : [] + cask_files = (cache/"Cask").directory? ? (cache/"Cask").children : [] + api_source_files = (cache/"api-source").glob("*/*/*/*/*") # org/repo/git_head/type/file.rb + + files.map { |path| { path: path, type: nil } } + + cask_files.map { |path| { path: path, type: :cask } } + + api_source_files.map { |path| { path: path, type: :api_source } } + end + + def cleanup_empty_api_source_directories(directory = cache/"api-source") + return if dry_run? + return unless directory.directory? + + directory.each_child do |child| + next unless child.directory? + + cleanup_empty_api_source_directories(child) + child.rmdir if child.empty? + end + end + def cleanup_unreferenced_downloads return if dry_run? return unless (cache/"downloads").directory? downloads = (cache/"downloads").children - referenced_downloads = [cache, cache/"Cask"].select(&:directory?) - .flat_map(&:children) - .select(&:symlink?) - .map(&:resolved_path) + referenced_downloads = cache_files.map { |file| file[:path] }.select(&:symlink?).map(&:resolved_path) (downloads - referenced_downloads).each do |download| if self.class.incomplete?(download) @@ -346,9 +395,10 @@ module Homebrew end def cleanup_cache(entries = nil) - entries ||= [cache, cache/"Cask"].select(&:directory?).flat_map(&:children) + entries ||= cache_files - entries.each do |path| + entries.each do |entry| + path = entry[:path] next if path == PERIODIC_CLEAN_FILE FileUtils.chmod_R 0755, path if self.class.go_cache_directory?(path) && !dry_run? @@ -365,7 +415,7 @@ module Homebrew end # If we've specified --prune don't do the (expensive) .stale? check. - cleanup_path(path) { path.unlink } if !prune? && self.class.stale?(path, scrub: scrub?) + cleanup_path(path) { path.unlink } if !prune? && self.class.stale?(entry, scrub: scrub?) end cleanup_unreferenced_downloads diff --git a/Library/Homebrew/dev-cmd/bump-formula-pr.rb b/Library/Homebrew/dev-cmd/bump-formula-pr.rb index c963291903..7f7905672c 100644 --- a/Library/Homebrew/dev-cmd/bump-formula-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-formula-pr.rb @@ -413,7 +413,7 @@ module Homebrew resource.url(url, specs) resource.owner = Resource.new(formula.name) forced_version = new_version && new_version != resource.version.to_s - resource.version = new_version if forced_version + resource.version(new_version) if forced_version odie "Couldn't identify version, specify it using `--version=`." if resource.version.blank? [resource.fetch, forced_version] end diff --git a/Library/Homebrew/downloadable.rb b/Library/Homebrew/downloadable.rb new file mode 100644 index 0000000000..76e34c29cf --- /dev/null +++ b/Library/Homebrew/downloadable.rb @@ -0,0 +1,133 @@ +# typed: true +# frozen_string_literal: true + +require "url" +require "checksum" + +# @api private +class Downloadable + include Context + extend T::Helpers + + abstract! + + sig { returns(T.nilable(URL)) } + attr_reader :url + + sig { returns(T.nilable(Checksum)) } + attr_reader :checksum + + sig { returns(T::Array[String]) } + attr_reader :mirrors + + sig { void } + def initialize + @mirrors = T.let([], T::Array[String]) + end + + def initialize_dup(other) + super + @checksum = @checksum.dup + @mirrors = @mirrors.dup + @version = @version.dup + end + + sig { override.returns(T.self_type) } + def freeze + @checksum.freeze + @mirrors.freeze + @version.freeze + super + end + + sig { returns(T::Boolean) } + def downloaded? + cached_download.exist? + end + + sig { returns(Pathname) } + def cached_download + downloader.cached_location + end + + sig { void } + def clear_cache + downloader.clear_cache + end + + sig { returns(T.nilable(Version)) } + def version + return @version if @version && !@version.null? + + version = determine_url&.version + version unless version&.null? + end + + sig { returns(T.class_of(AbstractDownloadStrategy)) } + def download_strategy + @download_strategy ||= determine_url&.download_strategy + end + + sig { returns(AbstractDownloadStrategy) } + def downloader + @downloader ||= begin + primary_url, *mirrors = determine_url_mirrors + raise ArgumentError, "attempted to use a Downloadable without a URL!" if primary_url.blank? + + download_strategy.new(primary_url, download_name, version, + mirrors: mirrors, cache: cache, **T.must(@url).specs) + end + end + + sig { params(verify_download_integrity: T::Boolean, timeout: T.nilable(T.any(Integer, Float))).returns(Pathname) } + def fetch(verify_download_integrity: true, timeout: nil) + cache.mkpath + + begin + downloader.fetch(timeout: timeout) + rescue ErrorDuringExecution, CurlDownloadStrategyError => e + raise DownloadError.new(self, e) + end + + download = cached_download + verify_download_integrity(download) if verify_download_integrity + download + end + + sig { params(filename: Pathname).void } + 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 your reference, the checksum is: + sha256 "#{filename.sha256}" + EOS + end + + private + + sig { overridable.returns(String) } + def download_name + File.basename(determine_url.to_s) + end + + sig { overridable.returns(T.nilable(URL)) } + def determine_url + @url + end + + sig { overridable.returns(T::Array[String]) } + def determine_url_mirrors + [determine_url.to_s, *mirrors].uniq + end + + sig { overridable.returns(Pathname) } + def cache + HOMEBREW_CACHE + end +end diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index ff627360c9..1d0f56ab14 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -603,13 +603,16 @@ class CompilerSelectionError < RuntimeError end end -# Raised in {Resource#fetch}. +# Raised in {Downloadable#fetch}. class DownloadError < RuntimeError - def initialize(resource, cause) + attr_reader :cause + + def initialize(downloadable, cause) super <<~EOS - Failed to download resource #{resource.download_name.inspect} + Failed to download resource #{downloadable.download_name.inspect} #{cause.message} EOS + @cause = cause set_backtrace(cause.backtrace) end end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index e90907c1cb..cd0e48d073 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -181,7 +181,7 @@ class Formula attr_accessor :force_bottle # @private - def initialize(name, path, spec, alias_path: nil, force_bottle: false) + def initialize(name, path, spec, alias_path: nil, tap: nil, force_bottle: false) # Only allow instances of subclasses. The base class does not hold any spec information (URLs etc). raise "Do not call `Formula.new' directly without a subclass." unless self.class < Formula @@ -191,7 +191,8 @@ class Formula self.class.freeze @name = name - @path = path + @unresolved_path = path + @path = path.resolved_path @alias_path = alias_path @alias_name = (File.basename(alias_path) if alias_path) @revision = self.class.revision || 0 @@ -199,7 +200,8 @@ class Formula @force_bottle = force_bottle - @tap = if path == Formulary.core_path(name) + @tap = tap + @tap ||= if path == Formulary.core_path(name) CoreTap.instance else Tap.from_path(path) @@ -320,7 +322,7 @@ class Formula # The path that was specified to find this formula. def specified_path default_specified_path = Pathname(alias_path) if alias_path.present? - default_specified_path ||= path + default_specified_path ||= @unresolved_path return default_specified_path if default_specified_path.presence&.exist? return local_bottle_path if local_bottle_path.presence&.exist? @@ -2094,6 +2096,18 @@ class Formula [] end + # @private + sig { returns(T.nilable(String)) } + def ruby_source_path + path.relative_path_from(tap.path).to_s if tap && path.exist? + end + + # @private + sig { returns(T.nilable(Checksum)) } + def ruby_source_checksum + Checksum.new(Digest::SHA256.file(path).hexdigest) if path.exist? + end + # @private def to_hash dependencies = deps @@ -2157,6 +2171,7 @@ class Formula "disable_reason" => disable_reason, "service" => service&.serialize, "tap_git_head" => tap_git_head, + "ruby_source_path" => ruby_source_path, "ruby_source_checksum" => {}, } @@ -2208,14 +2223,9 @@ class Formula } end - if self.class.loaded_from_api && active_spec.resource_defined?("ruby-source") + if (source_checksum = ruby_source_checksum) hsh["ruby_source_checksum"] = { - "sha256" => resource("ruby-source").checksum.hexdigest, - } - elsif !self.class.loaded_from_api && path.exist? - hsh["ruby_source_path"] = (path.relative_path_from(tap.path).to_s if tap) - hsh["ruby_source_checksum"] = { - "sha256" => Digest::SHA256.file(path).hexdigest, + "sha256" => source_checksum.hexdigest, } end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index ce03352e9e..fea502e41e 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -1191,16 +1191,7 @@ on_request: installed_on_request?, options: options) if pour_bottle?(output_warning: true) formula.fetch_bottle_tab else - if formula.class.loaded_from_api - # TODO: unify with cask logic (https://github.com/Homebrew/brew/issues/14746) - resource = formula.resource("ruby-source") - resource.fetch - @formula = Formulary.factory(resource.cached_download, - formula.active_spec_sym, - alias_path: formula.alias_path, - flags: formula.class.build_flags, - from: :formula_installer) - end + @formula = Homebrew::API::Formula.source_download(formula) if formula.class.loaded_from_api formula.fetch_patches formula.resources.each(&:fetch) diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index 24b3359b21..e293510307 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -252,15 +252,6 @@ module Formulary link_overwrite overwrite_path end - resource "ruby-source" do - tap_git_head = json_formula.fetch("tap_git_head", "HEAD") - ruby_source_path = json_formula.fetch("ruby_source_path", "Formula/#{name}.rb") - ruby_source_sha256 = json_formula.dig("ruby_source_checksum", "sha256") - - url "https://raw.githubusercontent.com/Homebrew/homebrew-core/#{tap_git_head}/#{ruby_source_path}" - sha256 ruby_source_sha256 if ruby_source_sha256 - end - def install raise "Cannot build from source from abstract formula." end @@ -307,6 +298,17 @@ module Formulary def versioned_formulae_names self.class.instance_variable_get(:@versioned_formulae_array) end + + @ruby_source_path_string = json_formula["ruby_source_path"] + def ruby_source_path + self.class.instance_variable_get(:@ruby_source_path_string) + end + + @ruby_source_checksum_hash = json_formula["ruby_source_checksum"] + def ruby_source_checksum + checksum_hash = self.class.instance_variable_get(:@ruby_source_checksum_hash) + Checksum.new(checksum_hash["sha256"]) if checksum_hash&.key?("sha256") + end end T.cast(klass, T.class_of(Formula)).loaded_from_api = true @@ -384,10 +386,13 @@ module Formulary attr_reader :path # The name used to install the formula attr_reader :alias_path + # The formula's tap (nil if it should be implicitly determined) + attr_reader :tap - def initialize(name, path) + def initialize(name, path, tap: nil) @name = name - @path = path.resolved_path + @path = path + @tap = tap end # Gets the formula instance. @@ -396,7 +401,7 @@ module Formulary def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false) alias_path ||= self.alias_path klass(flags: flags, ignore_errors: ignore_errors) - .new(name, path, spec, alias_path: alias_path, force_bottle: force_bottle) + .new(name, path, spec, alias_path: alias_path, tap: tap, force_bottle: force_bottle) end def klass(flags:, ignore_errors:) @@ -473,12 +478,7 @@ module Formulary def initialize(path) path = Pathname.new(path).expand_path name = path.basename(".rb").to_s - - # For files we've downloaded, they will be prefixed with `{URL MD5}--`. - # Remove that prefix to get the original filename. - name = name.split("--", 2).last if path.dirname == HOMEBREW_CACHE/"downloads" - - super name, path + super name, path, tap: Homebrew::API.tap_from_source_download(path) end end @@ -498,18 +498,16 @@ module Formulary end def load_file(flags:, ignore_errors:) - if @from != :formula_installer - match = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?[\w+-.@]+).rb}) - if match - raise UnsupportedInstallationMethod, - "Installation of #{match[:name]} from a GitHub commit URL is unsupported! " \ - "`brew extract #{match[:name]}` to a stable tap on GitHub instead." - elsif url.match?(%r{^(https?|ftp)://}) - raise UnsupportedInstallationMethod, - "Non-checksummed download of #{name} formula file from an arbitrary URL is unsupported! " \ - "`brew extract` or `brew create` and `brew tap-new` to create a formula file in a tap " \ - "on GitHub instead." - end + match = url.match(%r{githubusercontent.com/[\w-]+/[\w-]+/[a-f0-9]{40}(?:/Formula)?/(?[\w+-.@]+).rb}) + if match + raise UnsupportedInstallationMethod, + "Installation of #{match[:name]} from a GitHub commit URL is unsupported! " \ + "`brew extract #{match[:name]}` to a stable tap on GitHub instead." + elsif url.match?(%r{^(https?|ftp)://}) + raise UnsupportedInstallationMethod, + "Non-checksummed download of #{name} formula file from an arbitrary URL is unsupported! " \ + "`brew extract` or `brew create` and `brew tap-new` to create a formula file in a tap " \ + "on GitHub instead." end HOMEBREW_CACHE_FORMULA.mkpath FileUtils.rm_f(path) @@ -525,30 +523,28 @@ module Formulary # Loads tapped formulae. class TapLoader < FormulaLoader - attr_reader :tap - def initialize(tapped_name, from: nil) warn = [:keg, :rack].exclude?(from) - name, path = formula_name_path(tapped_name, warn: warn) - super name, path + name, path, tap = formula_name_path(tapped_name, warn: warn) + super name, path, tap: tap end def formula_name_path(tapped_name, warn: true) user, repo, name = tapped_name.split("/", 3).map(&:downcase) - @tap = Tap.fetch user, repo - path = find_formula_from_name(name) + tap = Tap.fetch user, repo + path = find_formula_from_name(name, tap) unless path.file? - if (possible_alias = @tap.alias_dir/name).file? + if (possible_alias = tap.alias_dir/name).file? path = possible_alias.resolved_path name = path.basename(".rb").to_s - elsif (new_name = @tap.formula_renames[name]) && - (new_path = find_formula_from_name(new_name)).file? + elsif (new_name = tap.formula_renames[name]) && + (new_path = find_formula_from_name(new_name, tap)).file? old_name = name path = new_path name = new_name - new_name = @tap.core_tap? ? name : "#{@tap}/#{name}" - elsif (new_tap_name = @tap.tap_migrations[name]) + new_name = tap.core_tap? ? name : "#{tap}/#{name}" + elsif (new_tap_name = tap.tap_migrations[name]) new_tap_user, new_tap_repo, = new_tap_name.split("/") new_tap_name = "#{new_tap_user}/#{new_tap_repo}" new_tap = Tap.fetch new_tap_name @@ -562,7 +558,7 @@ module Formulary opoo "Use #{new_name} instead of deprecated #{old_name}" if warn && old_name && new_name end - [name, path] + [name, path, tap] end def get_formula(spec, alias_path: nil, force_bottle: false, flags: [], ignore_errors: false) @@ -584,8 +580,8 @@ module Formulary private - def find_formula_from_name(name) - Formulary.find_formula_in_tap(name, @tap) + def find_formula_from_name(name, tap) + Formulary.find_formula_in_tap(name, tap) end end @@ -768,7 +764,7 @@ module Formulary when URL_START_REGEX return FromUrlLoader.new(ref, from: from) when HOMEBREW_TAP_FORMULA_REGEX - if ref.start_with?("homebrew/core/") && !Homebrew::EnvConfig.no_install_from_api? + if ref.match?(%r{^homebrew/(?:homebrew-)?core/}i) && !Homebrew::EnvConfig.no_install_from_api? name = ref.split("/", 3).last return FormulaAPILoader.new(name) if Homebrew::API::Formula.all_formulae.key?(name) return AliasAPILoader.new(name) if Homebrew::API::Formula.all_aliases.key?(name) diff --git a/Library/Homebrew/patch.rb b/Library/Homebrew/patch.rb index 5d470f89c8..3a96d12eca 100644 --- a/Library/Homebrew/patch.rb +++ b/Library/Homebrew/patch.rb @@ -125,8 +125,8 @@ class ExternalPatch end def owner=(owner) - resource.owner = owner - resource.version = resource.checksum || ERB::Util.url_encode(resource.url) + resource.owner = owner + resource.version(resource.checksum&.hexdigest || ERB::Util.url_encode(resource.url)) end def apply diff --git a/Library/Homebrew/resource.rb b/Library/Homebrew/resource.rb index 16579c8002..73fc5b4a26 100644 --- a/Library/Homebrew/resource.rb +++ b/Library/Homebrew/resource.rb @@ -1,9 +1,7 @@ # typed: true # frozen_string_literal: true -require "download_strategy" -require "checksum" -require "version" +require "downloadable" require "mktemp" require "livecheck" require "extend/on_system" @@ -13,14 +11,13 @@ require "extend/on_system" # of this class. # # @api private -class Resource - include Context +class Resource < Downloadable include FileUtils include OnSystem::MacOSAndLinux - attr_reader :mirrors, :specs, :using, :source_modified_time, :patches, :owner - attr_writer :version - attr_accessor :download_strategy, :checksum + attr_reader :source_modified_time, :patches, :owner + attr_writer :checksum + attr_accessor :download_strategy # Formula name must be set after the DSL, as we have no access to the # formula name before initialization of the formula. @@ -28,39 +25,25 @@ class Resource 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 - @url = nil - @version = nil - @mirrors = [] - @specs = {} - @checksum = nil - @using = nil @patches = [] @livecheck = Livecheck.new(self) @livecheckable = false + @insecure = 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 @@ -73,15 +56,15 @@ class Resource 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 + @insecure = !specs[:bottle] && !DevelopmentTools.ca_file_handles_most_https_certificates? + return if @url.nil? - def downloader - return @downloader if @downloader.present? - - url, *mirrors = determine_url_mirrors - @downloader = download_strategy.new(url, download_name, version, - mirrors: mirrors, **specs) + 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 @@ -98,18 +81,6 @@ class Resource "#{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 @@ -171,33 +142,9 @@ class Resource 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 + super(verify_download_integrity: verify_download_integrity) end # @!attribute [w] livecheck @@ -230,24 +177,29 @@ class Resource end def url(val = nil, **specs) - return @url if val.nil? + return @url&.to_s 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) + specs[:insecure] = true if @insecure + + @url = URL.new(val, specs) @downloader = nil - @version = detect_version(@version) + @download_strategy = @url.download_strategy end def version(val = nil) - return @version if val.nil? + return super() if val.nil? - @version = detect_version(val) + @version = case val + when String then Version.create(val) + when Version then val + else + # TODO: This can probably go if/when typechecking is enforced in taps. + raise TypeError, "version '#{val.inspect}' should be a string" + end end def mirror(val) @@ -259,6 +211,14 @@ class Resource patches << p end + def using + @url&.using + end + + def specs + @url&.specs || {}.freeze + end + protected def stage_resource(prefix, debug_symbols: false, &block) @@ -267,18 +227,6 @@ class Resource 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 = [] @@ -301,7 +249,7 @@ class Resource end end - [*extra_urls, url, *mirrors].uniq + [*extra_urls, *super].uniq end # A resource containing a Go package. diff --git a/Library/Homebrew/software_spec.rb b/Library/Homebrew/software_spec.rb index 2b6a958e1f..b9f83b6c5f 100644 --- a/Library/Homebrew/software_spec.rb +++ b/Library/Homebrew/software_spec.rb @@ -89,15 +89,11 @@ class SoftwareSpec @resource.owner = self resources.each_value do |r| r.owner = self - r.version ||= begin - raise "#{full_name}: version missing for \"#{r.name}\" resource!" if version.nil? + next if r.version - if version.head? - Version.create("HEAD") - else - version.dup - end - end + raise "#{full_name}: version missing for \"#{r.name}\" resource!" if version.nil? + + r.version(version.head? ? Version.create("HEAD") : version.dup) end patches.each { |p| p.owner = self } end @@ -281,7 +277,7 @@ end class HeadSoftwareSpec < SoftwareSpec def initialize(flags: []) super - @resource.version = Version.create("HEAD") + @resource.version(Version.create("HEAD")) end def verify_download_integrity(_filename) @@ -340,7 +336,6 @@ class Bottle def initialize(formula, spec, tag = nil) @name = formula.name @resource = Resource.new - @resource.specs[:bottle] = true @resource.owner = formula @spec = spec @@ -350,7 +345,7 @@ class Bottle @cellar = tag_spec.cellar @rebuild = spec.rebuild - @resource.version = formula.pkg_version.to_s + @resource.version(formula.pkg_version.to_s) @resource.checksum = tag_spec.checksum @fetch_tab_retried = false @@ -468,13 +463,15 @@ class Bottle using: CurlGitHubPackagesDownloadStrategy, headers: ["Accept: application/vnd.oci.image.index.v1+json"], ) - resource.downloader.resolved_basename = "#{name}-#{version_rebuild}.bottle_manifest.json" + T.cast(resource.downloader, CurlGitHubPackagesDownloadStrategy).resolved_basename = + "#{name}-#{version_rebuild}.bottle_manifest.json" resource end end def select_download_strategy(specs) specs[:using] ||= DownloadStrategyDetector.detect(@root_url) + specs[:bottle] = true specs end diff --git a/Library/Homebrew/tap_constants.rb b/Library/Homebrew/tap_constants.rb index b4801563b4..b5195f3949 100644 --- a/Library/Homebrew/tap_constants.rb +++ b/Library/Homebrew/tap_constants.rb @@ -6,11 +6,12 @@ HOMEBREW_TAP_FORMULA_REGEX = %r{^([\w-]+)/([\w-]+)/([\w+-.@]+)$}.freeze # Match taps' casks, e.g. `someuser/sometap/somecask` HOMEBREW_TAP_CASK_REGEX = %r{^([\w-]+)/([\w-]+)/([a-z0-9\-_]+)$}.freeze # Match main cask taps' casks, e.g. `homebrew/cask/somecask` or `somecask` -HOMEBREW_MAIN_TAP_CASK_REGEX = %r{^(homebrew/cask/)?[a-z0-9\-_]+$}.freeze +HOMEBREW_MAIN_TAP_CASK_REGEX = %r{^([Hh]omebrew/(?:homebrew-)?cask/)?[a-z0-9\-_]+$}.freeze # Match taps' directory paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap` HOMEBREW_TAP_DIR_REGEX = %r{#{Regexp.escape(HOMEBREW_LIBRARY.to_s)}/Taps/(?[\w-]+)/(?[\w-]+)}.freeze # Match taps' formula paths, e.g. `HOMEBREW_LIBRARY/Taps/someuser/sometap/someformula` HOMEBREW_TAP_PATH_REGEX = Regexp.new(HOMEBREW_TAP_DIR_REGEX.source + %r{(?:/.*)?$}.source).freeze # Match official taps' casks, e.g. `homebrew/cask/somecask or homebrew/cask-versions/somecask` -HOMEBREW_CASK_TAP_CASK_REGEX = %r{^(?:([Cc]askroom)/(cask|versions)|(homebrew)/(cask|cask-[\w-]+))/([\w+-.]+)$}.freeze +HOMEBREW_CASK_TAP_CASK_REGEX = + %r{^(?:([Cc]askroom)/(cask|versions)|([Hh]omebrew)/(?:homebrew-)?(cask|cask-[\w-]+))/([\w+-.]+)$}.freeze HOMEBREW_OFFICIAL_REPO_PREFIXES_REGEX = /^(home|linux)brew-/.freeze diff --git a/Library/Homebrew/test/api/cask_spec.rb b/Library/Homebrew/test/api/cask_spec.rb index 15f0f64170..8ab701a9d8 100644 --- a/Library/Homebrew/test/api/cask_spec.rb +++ b/Library/Homebrew/test/api/cask_spec.rb @@ -45,14 +45,4 @@ describe Homebrew::API::Cask do expect(casks_output).to eq casks_hash end end - - describe "::fetch_source" do - it "fetches the source of a cask (defaulting to master when no `git_head` is passed)" do - curl_output = instance_double(SystemCommand::Result, stdout: "foo", success?: true) - expect(Utils::Curl).to receive(:curl_output) - .with("--fail", "https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/foo.rb") - .and_return(curl_output) - described_class.fetch_source("foo", path: "Casks/foo.rb", git_head: "HEAD") - end - end end diff --git a/Library/Homebrew/test/api_spec.rb b/Library/Homebrew/test/api_spec.rb index 2be080ad28..3bfece9f5e 100644 --- a/Library/Homebrew/test/api_spec.rb +++ b/Library/Homebrew/test/api_spec.rb @@ -67,19 +67,4 @@ describe Homebrew::API do end.to raise_error(SystemExit) end end - - describe "::fetch_file_source" do - it "fetches a file" do - mock_curl_output stdout: json - fetched_json = described_class.fetch_homebrew_cask_source("foo", path: "Casks/foo.rb", git_head: "HEAD") - expect(fetched_json).to eq json - end - - it "raises an error if the file does not exist" do - mock_curl_output success: false - expect do - described_class.fetch_homebrew_cask_source("bar", path: "Casks/bar.rb", git_head: "HEAD") - end.to raise_error(ArgumentError, /No valid file found/) - end - end end diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index 148596a234..fab20eb310 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -231,7 +231,7 @@ describe Cask::Cask, :cask do context "when loaded from json file" do it "returns expected hash" do - expect(Homebrew::API::Cask).not_to receive(:fetch_source) + expect(Homebrew::API::Cask).not_to receive(:source_download) hash = Cask::CaskLoader::FromAPILoader.new( "everything", from_json: JSON.parse(expected_json) ).load(config: nil).to_h diff --git a/Library/Homebrew/test/cask/download_spec.rb b/Library/Homebrew/test/cask/download_spec.rb index d49410d9bb..ad699ab1ca 100644 --- a/Library/Homebrew/test/cask/download_spec.rb +++ b/Library/Homebrew/test/cask/download_spec.rb @@ -12,6 +12,7 @@ module Cask let(:downloaded_path) { Pathname.new("cask.zip") } before do + allow(downloaded_path).to receive(:file?).and_return(true) allow(downloaded_path).to receive(:sha256).and_return(computed_sha256) end diff --git a/Library/Homebrew/test/cask/installer_spec.rb b/Library/Homebrew/test/cask/installer_spec.rb index d63a8c1d46..9640e7f60f 100644 --- a/Library/Homebrew/test/cask/installer_spec.rb +++ b/Library/Homebrew/test/cask/installer_spec.rb @@ -240,7 +240,8 @@ describe Cask::Installer, :cask do let(:content) { File.read(path) } it "installs cask" do - expect(Homebrew::API::Cask).to receive(:fetch_source).once.and_return(content) + source_caffeine = Cask::CaskLoader.load(path) + expect(Homebrew::API::Cask).to receive(:source_download).once.and_return(source_caffeine) caffeine = Cask::CaskLoader.load(path) expect(caffeine).to receive(:loaded_from_api?).once.and_return(true) @@ -307,7 +308,8 @@ describe Cask::Installer, :cask do end it "uninstalls cask" do - expect(Homebrew::API::Cask).to receive(:fetch_source).twice.and_return(content) + source_caffeine = Cask::CaskLoader.load(path) + expect(Homebrew::API::Cask).to receive(:source_download).twice.and_return(source_caffeine) caffeine = Cask::CaskLoader.load(path) expect(caffeine).to receive(:loaded_from_api?).twice.and_return(true) diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index 9a489a537d..511d45baf4 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -206,14 +206,14 @@ describe Formula do example "installed alias with tap" do tap = Tap.new("user", "repo") name = "foo" - path = "#{tap.path}/Formula/#{name}.rb" + path = tap.path/"Formula/#{name}.rb" f = formula name, path: path do url "foo-1.0" end build_values_with_no_installed_alias = [ BuildOptions.new(Options.new, f.options), - Tab.new(source: { "path" => f.path }), + Tab.new(source: { "path" => f.path.to_s }), ] build_values_with_no_installed_alias.each do |build| f.build = build diff --git a/Library/Homebrew/test/formulary_spec.rb b/Library/Homebrew/test/formulary_spec.rb index 5e3ac4950d..16292bea7a 100644 --- a/Library/Homebrew/test/formulary_spec.rb +++ b/Library/Homebrew/test/formulary_spec.rb @@ -15,7 +15,7 @@ describe Formulary do bottle do root_url "file://#{bottle_dir}" - sha256 cellar: :any_skip_relocation, #{Utils::Bottles.tag}: "8f9aecd233463da6a4ea55f5f88fc5841718c013f3e2a7941350d6130f1dc149" + sha256 cellar: :any_skip_relocation, #{Utils::Bottles.tag}: "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97" end def install @@ -243,7 +243,7 @@ describe Formulary do Utils::Bottles.tag.to_s => { "cellar" => ":any", "url" => "file://#{bottle_dir}/#{formula_name}", - "sha256" => "8f9aecd233463da6a4ea55f5f88fc5841718c013f3e2a7941350d6130f1dc149", + "sha256" => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97", }, }, }, diff --git a/Library/Homebrew/test/resource_spec.rb b/Library/Homebrew/test/resource_spec.rb index 9b618980fb..57be3a9fd2 100644 --- a/Library/Homebrew/test/resource_spec.rb +++ b/Library/Homebrew/test/resource_spec.rb @@ -143,7 +143,7 @@ describe Resource do describe "#download_strategy" do it "returns the download strategy" do - strategy = Object.new + strategy = Class.new(AbstractDownloadStrategy) expect(DownloadStrategyDetector) .to receive(:detect).with("foo", nil).and_return(strategy) resource.url("foo") diff --git a/Library/Homebrew/test/support/fixtures/bottles/testball_bottle-0.1.yosemite.bottle.tar.gz b/Library/Homebrew/test/support/fixtures/bottles/testball_bottle-0.1.yosemite.bottle.tar.gz index ee5eb8b4b4d53133d004a594455e1585fe627d5f..20706d5869d2831b34c5ac55927f9ea623806213 100644 GIT binary patch literal 2015 zcmV<52O#(#iwFQ_s!3!31MOVjYa7WOAIDCU>e7p$p_KNxo7;qvfGX{8IijJKW2F|A zV^`KSPIK2~wL7vlS?w!!?Wl<6_FUX=IAgqkrkU@xoQ1`)UID{ zq1A>5L6$Bn1x4YNEVooHEpkOpB}PM;Qw>AnrMad_OsSz<7KNsM`Ld}PO`a4A=G>fO za9J)0P1|a4MJKZDEN8fF&$5XRG7MXHHYfq;H$csO&eOvd6wJ>rtt^)pYULZ+YOQ>& zvOYhbuUZXX_qXy(ZksfGayL&KeZH^d=lll93w3W#Ti_7`1VZy@K&>7;$UJXXeIgjt z{~MMw)-m8n-+uwV{DPnqQ~~*b!^eRm2a&6s!DWndqF&7Y<7Hn6{zS2j#X$2O{&MJ;cItU39X)vEI zjOzcZdS{8~Ch_yn_8mC1|MPwQU*si;N4PV-Y7WOS@BfN`tCfXP?dnqH+vQwG4?{mU zqLW=&E@fmDuK{`W<`KyG46N-BQ}A?;zq0(Tj663|MIVFk>0Fvk1f%-@ct-yt%>P8C zfBcseHvUfp=UJ(Sp#ooDI#kLgMRoTADFzJqU0;Z5Ru2 zyTD(=*Id6H*?IQ(?OLT; z*0f7G((!c1)F@FF(|uSVEhZbW*$nOn=+c9jxQHFg&f-w_(p1y+4U%dt;&@~FQY;{d zJmN!&IA#W-#kH!oT3fjadOY7Xu<6EBk#A$JX#sCOfBt^GyjBMj*XwW8%GzS3_8<=} zhgK`t?<-%0?vo1}_qaq!OBWRtQ@#cI|CF_*5i;!OJTVP@3KSx$@i;5qZ68$#;o8e<%10uhx7 zZ-;E^_QkL4NxRg2DTyN)O}gaG8|vQOs8_uh;u{X1i&s_ay3W4ry9QI$@W*pTG4QLO-DK z*}e^>8T7ErV)%In&{SgMzL3UE&yQv0V-ul=ST>b>?K5*S}d_tn6{mr@BE#cIs)p6tpg1Om)yU_;ZMV3h_%2_SPfQ zX`s9rG`#*4KjsIQzl@UiM|I?*z^9AdeEQ12oZ9fK<@sp*Fm7V?SZgi7+L_62{6rS4B(lWau4vVZ3Q7|=N9&v7#J8B z7#J8B7#J9y3vQ#G_qYG~;r0i!yKl|xym;S>W1PynvqEgf+)UPpFXfkAH1IwEK{*Za;o6$=iK+BYt{g=l!+q56;Dz zx8B+Q5@nyc@B{<*Z)o23AJ5(T)8i$&uY+&Ujh)vA8XB){-}}*N2sC{L!}<=w41{>y z$iTqBz`(%3z`(%3z`(%p!r`NRJa0=F7#J8B7#J8B7#J8B7#Kb&(6hv^P9gNm*OK$k zH2phB=OF#^G(xjDIl6szIynoll&0yKfYmfj&jq}R=-KSq`22vE=F>9+TQm(a{|qv} zpQh=#=;VIERDAq-wZw^L%%MUN(btyQR?^N>NDm>4~#x`L2UNisc}BDI|o< zjs<_cC=4d%kO^bx{U2*gmw(V<_kZ?&|EK5vk0=*p6=7i#my?e9{hwj4c*Awg#um{G xKO(mFZ10*s%I|-rzyAm=1^h1wf`qU*PCpOBG50?M1HD6WiDcGbaZTG zE_7jX0PURLYa_=Uz(;3a@9JE0h8(4|A(?FMApv!@KeQ{`G}O9USBu(bBP*QKOSjSVTi>f5$^O7uKK~zLhMpzklLi!N}q3%OcR(1bI6bufhTMgSCq9@Hq+kQ+4`;>qA z%0{)gvZ7T=-z$~ZtNiuA8;i&(Ou%u8ZTpP#oBvnG&2!J3;>OfJg> zq2<^Hx8O#OljTgW(X<`ngAUWt-7Ojb;x{3hTU=8Q>)^4txV*Mnx>PA$)z&Me%jM0* z#oUT*_`1K7TlN~n@X5^_t@OE`n$P`;a zzmS&;Z2UJ7PJjIjI)%`xZsN5KyopojJ@i#D5}4nDq1W$?rm|SAR#GRGcqf(a=2Ut= zgU~ZDtm7NH2}4L7Y9k*IhnRHaS%D!QNyERJgnhav?6w`)Fgh!{7HJ_QPNc$oxfs^| z$J6>pc>haEZ~K=-*8V5L^UuY@eM1lGGsLWWcwB|fcq|w^{^zx$GvqH82VMWk=ZEzF zl6ZLhFTq=Ywg1U5O8@^ml=RsD98G0c|4$`oKYzL>$U*-g8v=Yj z2Dkrpy|qkqi}-z3%06El$F+Y!R(smNtV%HdZ#|qWALfL&e_6l{x>Ea+sLU&cyeiJA zyr8IpswjE+%emyq=wfjDA6M^xeETnma!>mgRfYBc6Tx)!Ai&kMtS`kyjlY90dwwHw z^cjpTQp2`uhqmt6uag&Cy+QCxnCqz>To%(0FyONe77fI^Q~2s*twL>dTdqwp6a%&Ot4p`3c(f{@c(L_|SNr zJFF+D>yn~ks^Aj+NV>6HgzGen*=i-9tTeMu%o~uy8+J4HBd(RCH);}pCb@1uT~O^f zv3JuaHM_;9w=f5DGp?^kI!EVHRn&Z;@*nj5r%oKlyXpCk_26|3+JiE9{a+UHO1}O5 zM}+`_L?u}Jp9Jsy_dowT1NIb32Ic3#+=YubP(0v9b6_-Wz4-mo4@;G`F2UKsI;q^< zSgu;ytoF#vcHA`H)2OpJsKJc06?TI6%GaMo=%>^^-?N}NjqY@0%pmk_cWZ!-k3q-R zsD1u$n!ELN(!W%!7P~xfS02*zPoeJgTY9K>r~jz@g*5NI3!)tvYp1)-_3F3)I-aKX z5FIrAKaOR~tLtl(PVR*??YNWvGimrFuIo{wsm1R(G(dhP&Bs@|;yWA9Lr$_LVmY22VAn>idWh34fOX&kf;{s!z{f&B!SgY~GX)KJ+OG&uj1UkLsEpG3*? zqjBV-z^99yx%82LD^0^6R{!??FMssiS7h?%?WcbIv*PJA4H-1F+8=?|*M88r^J!d3 z8Z)tJrw7cIX=(v;xfc@R!;`30Z{C4MG@(C?p3_CHhiU%<_S`^hPUW^uB0`}|b07hpMc z(>(#}shjQ#coosT*|YKffo6K2?itvjZqWI6(D{?pP4`77=L<6N_UHXBH{I9$M68l- z%GQD~GBm?>E!*AF>cnXhKiwj))gsr_9M8KEHIuMfLpSRXh@M$R+Lg_szh$QOHVA1= z-`k=mlfd1mYxML?6q0s$;%?i%=OWNzyNDhN3E^hThTmos4mlX|cr)<)k3FWtKj^sg zKL`K+)P4R(QAGh^aT14<#M|-emEsXO2Z|VPkLQSFPzac!r Z(m2a`5GTC+84SkB<^OHLGxGpk005q77SsR$ diff --git a/Library/Homebrew/test/support/fixtures/failball.rb b/Library/Homebrew/test/support/fixtures/failball.rb index 4995d94f55..a675f8b8ed 100644 --- a/Library/Homebrew/test/support/fixtures/failball.rb +++ b/Library/Homebrew/test/support/fixtures/failball.rb @@ -3,7 +3,7 @@ class Failball < Formula def initialize(name = "failball", path = Pathname.new(__FILE__).expand_path, spec = :stable, - alias_path: nil, force_bottle: false) + alias_path: nil, tap: nil, force_bottle: false) super end diff --git a/Library/Homebrew/test/support/fixtures/testball.rb b/Library/Homebrew/test/support/fixtures/testball.rb index a2f23923a0..94988d5692 100644 --- a/Library/Homebrew/test/support/fixtures/testball.rb +++ b/Library/Homebrew/test/support/fixtures/testball.rb @@ -3,7 +3,7 @@ class Testball < Formula def initialize(name = "testball", path = Pathname.new(__FILE__).expand_path, spec = :stable, - alias_path: nil, force_bottle: false) + alias_path: nil, tap: nil, force_bottle: false) super end diff --git a/Library/Homebrew/test/support/fixtures/testball_bottle.rb b/Library/Homebrew/test/support/fixtures/testball_bottle.rb index 33d4093ef9..986b5f02b0 100644 --- a/Library/Homebrew/test/support/fixtures/testball_bottle.rb +++ b/Library/Homebrew/test/support/fixtures/testball_bottle.rb @@ -3,7 +3,7 @@ class TestballBottle < Formula def initialize(name = "testball_bottle", path = Pathname.new(__FILE__).expand_path, spec = :stable, - alias_path: nil, force_bottle: false) + alias_path: nil, tap: nil, force_bottle: false) super end @@ -13,7 +13,7 @@ class TestballBottle < Formula bottle do root_url "file://#{TEST_FIXTURE_DIR}/bottles" - sha256 cellar: :any_skip_relocation, Utils::Bottles.tag.to_sym => "8f9aecd233463da6a4ea55f5f88fc5841718c013f3e2a7941350d6130f1dc149" + sha256 cellar: :any_skip_relocation, Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97" end cxxstdlib_check :skip diff --git a/Library/Homebrew/test/support/fixtures/testball_bottle_cellar.rb b/Library/Homebrew/test/support/fixtures/testball_bottle_cellar.rb index 85ea087b19..b0973b8a51 100644 --- a/Library/Homebrew/test/support/fixtures/testball_bottle_cellar.rb +++ b/Library/Homebrew/test/support/fixtures/testball_bottle_cellar.rb @@ -3,7 +3,7 @@ class TestballBottleCellar < Formula def initialize(name = "testball_bottle", path = Pathname.new(__FILE__).expand_path, spec = :stable, - alias_path: nil, force_bottle: false) + alias_path: nil, tap: nil, force_bottle: false) super end @@ -13,7 +13,7 @@ class TestballBottleCellar < Formula bottle do root_url "file://#{TEST_FIXTURE_DIR}/bottles" - sha256 cellar: :any_skip_relocation, Utils::Bottles.tag.to_sym => "8f9aecd233463da6a4ea55f5f88fc5841718c013f3e2a7941350d6130f1dc149" + sha256 cellar: :any_skip_relocation, Utils::Bottles.tag.to_sym => "d7b9f4e8bf83608b71fe958a99f19f2e5e68bb2582965d32e41759c24f1aef97" end cxxstdlib_check :skip diff --git a/Library/Homebrew/url.rb b/Library/Homebrew/url.rb new file mode 100644 index 0000000000..0a029e686b --- /dev/null +++ b/Library/Homebrew/url.rb @@ -0,0 +1,33 @@ +# typed: true +# frozen_string_literal: true + +require "download_strategy" +require "version" + +# @api private +class URL + attr_reader :specs, :using + + sig { params(url: String, specs: T::Hash[Symbol, T.untyped]).void } + def initialize(url, specs = {}) + @url = url.freeze + @specs = specs.dup + @using = @specs.delete(:using) + @specs.freeze + end + + sig { returns(String) } + def to_s + @url + end + + sig { returns(T.class_of(AbstractDownloadStrategy)) } + def download_strategy + @download_strategy ||= DownloadStrategyDetector.detect(@url, @using) + end + + sig { returns(Version) } + def version + @version ||= Version.detect(@url, **@specs) + end +end