diff --git a/Library/Homebrew/cask/url.rb b/Library/Homebrew/cask/url.rb index b347208aa8..115e0e9f69 100644 --- a/Library/Homebrew/cask/url.rb +++ b/Library/Homebrew/cask/url.rb @@ -20,7 +20,7 @@ module Cask sig { returns(T.nilable(T.any(Symbol, String))) } attr_reader :user_agent - sig { returns(T.any(T::Class[T.anything], Symbol, NilClass)) } + sig { returns(T.any(T::Class[AbstractDownloadStrategy], Symbol, NilClass)) } attr_reader :using sig { returns(T.nilable(String)) } @@ -35,7 +35,7 @@ module Cask # @api public verified: T.nilable(String), # @api public - using: T.any(Class, Symbol, NilClass), + using: T.any(T::Class[AbstractDownloadStrategy], Symbol, NilClass), # @api public tag: T.nilable(String), # @api public @@ -186,7 +186,7 @@ module Cask params( uri: T.nilable(T.any(URI::Generic, String)), verified: T.nilable(String), - using: T.any(Class, Symbol, NilClass), + using: T.any(T::Class[AbstractDownloadStrategy], Symbol, NilClass), tag: T.nilable(String), branch: T.nilable(String), revisions: T.nilable(T::Array[String]), diff --git a/Library/Homebrew/dev-cmd/create.rb b/Library/Homebrew/dev-cmd/create.rb index 830d995c8f..6c5414f2d4 100644 --- a/Library/Homebrew/dev-cmd/create.rb +++ b/Library/Homebrew/dev-cmd/create.rb @@ -115,7 +115,7 @@ module Homebrew strategy = DownloadStrategyDetector.detect(url) downloader = strategy.new(url, token, version.to_s, cache: Cask::Cache.path) downloader.fetch - downloader.cached_location.sha256 + downloader.cached_location!.sha256 end [url.gsub(version.to_s, "\#{version}"), sha256] diff --git a/Library/Homebrew/download_strategy.rb b/Library/Homebrew/download_strategy.rb index 24bd1b7cde..ef3c3548a9 100644 --- a/Library/Homebrew/download_strategy.rb +++ b/Library/Homebrew/download_strategy.rb @@ -1,4 +1,4 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true require "json" @@ -24,17 +24,21 @@ require "github_packages" # @abstract Abstract superclass for all download strategies. class AbstractDownloadStrategy + extend T::Helpers include FileUtils include Context include SystemCommand::Mixin + abstract! + # Extension for bottle downloads. module Pourable extend T::Helpers requires_ancestor { AbstractDownloadStrategy } - def stage + sig { params(block: T.nilable(T.proc.params(arg0: String).returns(T.anything))).returns(T.nilable(T.anything)) } + def stage(&block) ohai "Pouring #{basename}" super end @@ -49,38 +53,57 @@ class AbstractDownloadStrategy # Location of the cached download. # # @api public - sig { returns(Pathname) } + sig { returns(T.nilable(Pathname)) } attr_reader :cached_location - attr_reader :cache, :meta, :name, :version + sig { returns(Pathname) } + attr_reader :cache + + sig { returns(T::Hash[Symbol, T.untyped]) } + attr_reader :meta + + sig { returns(String) } + attr_reader :name + + sig { returns(T.any(NilClass, String, Version)) } + attr_reader :version private :meta, :name, :version + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) + @cached_location = T.let(nil, T.nilable(Pathname)) + @ref_type = T.let(nil, T.nilable(Symbol)) + @ref = T.let(nil, T.untyped) @url = url @name = name @version = version - @cache = meta.fetch(:cache, HOMEBREW_CACHE) - @meta = meta - @quiet = false + @cache = T.let(meta.fetch(:cache, HOMEBREW_CACHE), Pathname) + @meta = T.let(meta, T::Hash[Symbol, T.untyped]) + @quiet = T.let(false, T.nilable(T::Boolean)) extend Pourable if meta[:bottle] end # Download and cache the resource at {#cached_location}. # # @api public + sig { overridable.params(timeout: T.any(Float, Integer, NilClass)).void } def fetch(timeout: nil); end + sig { returns(Pathname) } + def cached_location! = T.must(cached_location) + # Disable any output during downloading. # # @api public sig { void } def quiet! - @quiet = true + @quiet = T.let(true, T.nilable(T::Boolean)) end + sig { returns(T::Boolean) } def quiet? - Context.current.quiet? || @quiet + Context.current.quiet? || @quiet || false end # Unpack {#cached_location} into the current working directory. @@ -92,8 +115,9 @@ class AbstractDownloadStrategy # directory. # # @api public + sig { overridable.params(block: T.untyped).void } def stage(&block) - UnpackStrategy.detect(cached_location, + UnpackStrategy.detect(cached_location!, prioritize_extension: true, ref_type: @ref_type, ref: @ref) .extract_nestedly(basename:, @@ -102,6 +126,7 @@ class AbstractDownloadStrategy chdir(&block) if block end + sig { params(block: T.untyped).void } def chdir(&block) entries = Dir["*"] raise "Empty archive" if entries.empty? @@ -122,7 +147,7 @@ class AbstractDownloadStrategy # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { overridable.returns(Time) } def source_modified_time Pathname.pwd.to_enum(:find).select(&:file?).map(&:mtime).max end @@ -131,28 +156,34 @@ class AbstractDownloadStrategy # from the cache. # # @api public + sig { overridable.void } def clear_cache - rm_rf(cached_location) + rm_rf(cached_location!) end + sig { returns(Pathname) } def basename - cached_location.basename + cached_location!.basename end private + sig { params(args: T.anything).void } def puts(*args) super unless quiet? end + sig { params(args: T.anything).void } def ohai(*args) super unless quiet? end + sig { params(args: String, options: T.untyped).returns(SystemCommand::Result) } def silent_command(*args, **options) system_command(*args, print_stderr: false, env:, **options) end + sig { params(args: String, options: T.untyped).returns(SystemCommand::Result) } def command!(*args, **options) system_command!( *args, @@ -162,6 +193,7 @@ class AbstractDownloadStrategy ) end + sig { returns(T::Hash[Symbol, T::Boolean]) } def command_output_options { print_stdout: !quiet?, @@ -170,6 +202,7 @@ class AbstractDownloadStrategy } end + sig { overridable.returns(T::Hash[String, String]) } def env {} end @@ -177,18 +210,27 @@ end # @abstract Abstract superclass for all download strategies downloading from a version control system. class VCSDownloadStrategy < AbstractDownloadStrategy + abstract! + + sig { returns(Pathname) } + attr_reader :cached_location + REF_TYPES = [:tag, :branch, :revisions, :revision].freeze + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) super - @ref_type, @ref = extract_ref(meta) - @revision = meta[:revision] - @cached_location = @cache/Utils.safe_filename("#{name}--#{cache_tag}") + extracted_ref = extract_ref(meta) + @ref_type = T.let(extracted_ref.fetch(0), T.nilable(Symbol)) + @ref = T.let(extracted_ref.fetch(1), T.untyped) + @revision = T.let(meta[:revision], T.nilable(String)) + @cached_location = T.let(@cache/Utils.safe_filename("#{name}--#{cache_tag}"), Pathname) end # Download and cache the repository at {#cached_location}. # # @api public + sig { override.params(timeout: T.any(Float, Integer, NilClass)).void } def fetch(timeout: nil) end_time = Time.now + timeout if timeout @@ -205,7 +247,8 @@ class VCSDownloadStrategy < AbstractDownloadStrategy clone_repo(timeout: end_time) end - version.update_commit(last_commit) if head? + v = version + v.update_commit(last_commit) if v.is_a?(Version) && head? return if @ref_type != :tag || @revision.blank? || current_revision.blank? || current_revision == @revision @@ -215,46 +258,50 @@ class VCSDownloadStrategy < AbstractDownloadStrategy EOS end + sig { returns(String) } def fetch_last_commit fetch last_commit end + sig { overridable.params(commit: T.nilable(String)).returns(T::Boolean) } def commit_outdated?(commit) - @last_commit ||= fetch_last_commit + @last_commit ||= T.let(fetch_last_commit, T.nilable(String)) commit != @last_commit end + sig { returns(T::Boolean) } def head? - version.respond_to?(:head?) && version.head? + v = version + v.is_a?(Version) ? v.head? : false end # Return the most recent modified timestamp. # # @api public - sig { returns(String) } + sig { overridable.returns(String) } def last_commit source_modified_time.to_i.to_s end private - def cache_tag - raise NotImplementedError - end + sig { abstract.returns(String) } + def cache_tag; end - def repo_valid? - raise NotImplementedError - end + sig { abstract.returns(T::Boolean) } + def repo_valid?; end - sig { params(timeout: T.nilable(Time)).void } + sig { abstract.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil); end - sig { params(timeout: T.nilable(Time)).void } + sig { abstract.params(timeout: T.nilable(Time)).void } def update(timeout: nil); end + sig { overridable.returns(T.nilable(String)) } def current_revision; end + sig { params(specs: T::Hash[T.nilable(Symbol), T.untyped]).returns([T.nilable(Symbol), T.untyped]) } def extract_ref(specs) key = REF_TYPES.find { |type| specs.key?(type) } [key, specs[key]] @@ -263,61 +310,74 @@ end # @abstract Abstract superclass for all download strategies downloading a single file. class AbstractFileDownloadStrategy < AbstractDownloadStrategy + abstract! + # Path for storing an incomplete download while the download is still in progress. # # @api public + sig { returns(Pathname) } def temporary_path - @temporary_path ||= Pathname.new("#{cached_location}.incomplete") + @temporary_path ||= T.let(Pathname.new("#{cached_location}.incomplete"), T.nilable(Pathname)) end # Path of the symlink (whose name includes the resource name, version and extension) # pointing to {#cached_location}. # # @api public + sig { returns(Pathname) } def symlink_location - return @symlink_location if defined?(@symlink_location) + return T.must(@symlink_location) if defined?(@symlink_location) ext = Pathname(parse_basename(url)).extname - @symlink_location = @cache/Utils.safe_filename("#{name}--#{version}#{ext}") + @symlink_location = T.let(@cache/Utils.safe_filename("#{name}--#{version}#{ext}"), T.nilable(Pathname)) + T.must(@symlink_location) end # Path for storing the completed download. # # @api public + sig { returns(Pathname) } def cached_location - return @cached_location if defined?(@cached_location) + return @cached_location if @cached_location url_sha256 = Digest::SHA256.hexdigest(url) downloads = Pathname.glob(HOMEBREW_CACHE/"downloads/#{url_sha256}--*") .reject { |path| path.extname.end_with?(".incomplete") } - @cached_location = if downloads.count == 1 - downloads.first - else - HOMEBREW_CACHE/"downloads/#{url_sha256}--#{Utils.safe_filename(resolved_basename)}" - end + @cached_location = T.let( + if downloads.count == 1 + downloads.fetch(0) + else + HOMEBREW_CACHE/"downloads/#{url_sha256}--#{Utils.safe_filename(resolved_basename)}" + end, T.nilable(Pathname) + ) + T.must(@cached_location) end + sig { returns(Pathname) } def basename cached_location.basename.sub(/^[\da-f]{64}--/, "") end private + sig { returns(String) } def resolved_url resolved_url, = resolved_url_and_basename resolved_url end + sig { returns(String) } def resolved_basename _, resolved_basename = resolved_url_and_basename resolved_basename end + sig { returns([String, String]) } def resolved_url_and_basename - return @resolved_url_and_basename if defined?(@resolved_url_and_basename) + return T.must(@resolved_url_and_basename) if defined?(@resolved_url_and_basename) - @resolved_url_and_basename = [url, parse_basename(url)] + T.must(@resolved_url_and_basename = T.let([url, parse_basename(url)], T.nilable([String, String]))) end sig { params(url: String, search_query: T::Boolean).returns(String) } @@ -370,11 +430,16 @@ end class CurlDownloadStrategy < AbstractFileDownloadStrategy include Utils::Curl + # url, basename, time, file_size, is_redirection + URLMetadata = T.type_alias { [String, String, T.nilable(Time), T.nilable(Integer), T::Boolean] } + + sig { returns(T::Array[String]) } attr_reader :mirrors + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) - @try_partial = true - @mirrors = meta.fetch(:mirrors, []) + @try_partial = T.let(true, T::Boolean) + @mirrors = T.let(meta.fetch(:mirrors, []), T::Array[String]) # Merge `:header` with `:headers`. if (header = meta.delete(:header)) @@ -388,6 +453,7 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy # Download and cache the file at {#cached_location}. # # @api public + sig { override.params(timeout: T.any(Float, Integer, NilClass)).void } def fetch(timeout: nil) end_time = Time.now + timeout if timeout @@ -398,7 +464,7 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy urls = [url, *mirrors] begin - url = urls.shift + url = T.must(urls.shift) if (domain = Homebrew::EnvConfig.artifact_domain) url = url.sub(%r{^https?://#{GitHubPackages::URL_DOMAIN}/}o, "#{domain.chomp("/")}/") @@ -408,7 +474,8 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy ohai "Downloading #{url}" use_cached_location = cached_location.exist? - use_cached_location = false if version.respond_to?(:latest?) && version.latest? + v = version + use_cached_location = false if v.is_a?(Cask::DSL::Version) && v.latest? resolved_url, _, last_modified, _, is_redirection = begin resolve_url_basename_time_file_size(url, timeout: Utils::Timer.remaining!(end_time)) @@ -428,12 +495,12 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy puts "Already downloaded: #{cached_location}" else begin - _fetch(url:, resolved_url:, timeout: Utils::Timer.remaining!(end_time)) + _fetch(url:, resolved_url: T.must(resolved_url), timeout: Utils::Timer.remaining!(end_time)) rescue ErrorDuringExecution raise CurlDownloadStrategyError, url end cached_location.dirname.mkpath - temporary_path.rename(cached_location) + temporary_path.rename(cached_location.to_s) end symlink_location.dirname.mkpath @@ -451,26 +518,30 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy end end + sig { override.void } def clear_cache super rm_rf(temporary_path) end + sig { params(timeout: T.any(Float, Integer, NilClass)).returns([T.nilable(Time), Integer]) } def resolved_time_file_size(timeout: nil) _, _, time, file_size = resolve_url_basename_time_file_size(url, timeout:) - [time, file_size] + [time, T.must(file_size)] end private + sig { params(timeout: T.any(Float, Integer, NilClass)).returns([String, String]) } def resolved_url_and_basename(timeout: nil) resolved_url, basename, = resolve_url_basename_time_file_size(url, timeout: nil) [resolved_url, basename] end + sig { overridable.params(url: String, timeout: T.any(Float, Integer, NilClass)).returns(URLMetadata) } def resolve_url_basename_time_file_size(url, timeout: nil) - @resolved_info_cache ||= {} - return @resolved_info_cache[url] if @resolved_info_cache.include?(url) + @resolved_info_cache ||= T.let({}, T.nilable(T::Hash[String, URLMetadata])) + return @resolved_info_cache.fetch(url) if @resolved_info_cache.include?(url) begin parsed_output = curl_headers(url.to_s, wanted_headers: ["content-disposition"], timeout:) @@ -533,6 +604,10 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy @resolved_info_cache[url] = [final_url, basename, time.last, file_size, is_redirection] end + sig { + overridable.params(url: String, resolved_url: String, timeout: T.any(Float, Integer, NilClass)) + .returns(T.nilable(SystemCommand::Result)) + } def _fetch(url:, resolved_url:, timeout:) ohai "Downloading from #{resolved_url}" if url != resolved_url @@ -545,12 +620,17 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy _curl_download resolved_url, temporary_path, timeout end + sig { + params(resolved_url: String, to: T.any(Pathname, String), timeout: T.any(Float, Integer, NilClass)) + .returns(T.nilable(SystemCommand::Result)) + } def _curl_download(resolved_url, to, timeout) curl_download resolved_url, to:, try_partial: @try_partial, timeout: end # Curl options to be always passed to curl, # with raw head calls (`curl --head`) or with actual `fetch`. + sig { returns(T::Array[String]) } def _curl_args args = [] @@ -565,17 +645,23 @@ class CurlDownloadStrategy < AbstractFileDownloadStrategy args end + sig { returns(T::Hash[Symbol, String]) } def _curl_opts return { user_agent: meta.fetch(:user_agent) } if meta.key?(:user_agent) {} end + sig { override.params(args: String, options: T.untyped).returns(SystemCommand::Result) } def curl_output(*args, **options) super(*_curl_args, *args, **_curl_opts, **options) end - def curl(*args, **options) + sig { + override.params(args: String, print_stdout: T.any(T::Boolean, Symbol), options: T.untyped) + .returns(SystemCommand::Result) + } + def curl(*args, print_stdout: true, **options) options[:connect_timeout] = 15 unless mirrors.empty? super(*_curl_args, *args, **_curl_opts, **command_output_options, **options) end @@ -587,12 +673,17 @@ end class HomebrewCurlDownloadStrategy < CurlDownloadStrategy private + sig { + params(resolved_url: String, to: T.any(Pathname, String), timeout: T.any(Float, Integer, NilClass)) + .returns(T.nilable(SystemCommand::Result)) + } def _curl_download(resolved_url, to, timeout) raise HomebrewCurlDownloadStrategyError, url unless Formula["curl"].any_version_installed? curl_download resolved_url, to:, try_partial: @try_partial, timeout:, use_homebrew_curl: true end + sig { override.params(args: String, options: T.untyped).returns(SystemCommand::Result) } def curl_output(*args, **options) raise HomebrewCurlDownloadStrategyError, url unless Formula["curl"].any_version_installed? @@ -605,8 +696,10 @@ end # # @api public class CurlGitHubPackagesDownloadStrategy < CurlDownloadStrategy + sig { params(resolved_basename: String).returns(T.nilable(String)) } attr_writer :resolved_basename + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) meta[:headers] ||= [] # GitHub Packages authorization header. @@ -617,6 +710,7 @@ class CurlGitHubPackagesDownloadStrategy < CurlDownloadStrategy private + sig { override.params(url: String, timeout: T.any(Float, Integer, NilClass)).returns(URLMetadata) } def resolve_url_basename_time_file_size(url, timeout: nil) return super if @resolved_basename.blank? @@ -628,23 +722,26 @@ end # # @api public class CurlApacheMirrorDownloadStrategy < CurlDownloadStrategy + sig { returns(T::Array[String]) } def mirrors combined_mirrors end private + sig { returns(T::Array[String]) } def combined_mirrors - return @combined_mirrors if defined?(@combined_mirrors) + return T.must(@combined_mirrors) if defined?(@combined_mirrors) backup_mirrors = unless apache_mirrors["in_attic"] apache_mirrors.fetch("backup", []) .map { |mirror| "#{mirror}#{apache_mirrors["path_info"]}" } end - @combined_mirrors = [*@mirrors, *backup_mirrors] + T.must(@combined_mirrors = T.let([*@mirrors, *backup_mirrors], T.nilable(T::Array[String]))) end + sig { override.params(url: String, timeout: T.any(Float, Integer, NilClass)).returns(URLMetadata) } def resolve_url_basename_time_file_size(url, timeout: nil) if url == self.url preferred = if apache_mirrors["in_attic"] @@ -658,11 +755,12 @@ class CurlApacheMirrorDownloadStrategy < CurlDownloadStrategy end end + sig { returns(T::Hash[String, T.untyped]) } def apache_mirrors - return @apache_mirrors if defined?(@apache_mirrors) + return T.must(@apache_mirrors) if defined?(@apache_mirrors) - json, = curl_output("--silent", "--location", "#{url}&asjson=1") - @apache_mirrors = JSON.parse(json) + json = curl_output("--silent", "--location", "#{url}&asjson=1").stdout + T.must(@apache_mirrors = T.let(JSON.parse(json), T.nilable(T::Hash[String, T.untyped]))) rescue JSON::ParserError raise CurlDownloadStrategyError, "Couldn't determine mirror, try again later." end @@ -675,6 +773,10 @@ end class CurlPostDownloadStrategy < CurlDownloadStrategy private + sig { + override.params(url: String, resolved_url: String, timeout: T.any(Float, Integer, NilClass)) + .returns(T.nilable(SystemCommand::Result)) + } def _fetch(url:, resolved_url:, timeout:) args = if meta.key?(:data) escape_data = ->(d) { ["-d", URI.encode_www_form([d])] } @@ -693,7 +795,8 @@ end # # @api public class NoUnzipCurlDownloadStrategy < CurlDownloadStrategy - def stage + sig { override.params(_block: T.untyped).void } + def stage(&_block) UnpackStrategy::Uncompressed.new(cached_location) .extract(basename:, verbose: verbose? && !quiet?) @@ -704,11 +807,15 @@ end # Strategy for extracting local binary packages. class LocalBottleDownloadStrategy < AbstractFileDownloadStrategy # TODO: Call `super` here - def initialize(path) # rubocop:disable Lint/MissingSuper - @cached_location = path + # rubocop:disable Lint/MissingSuper + sig { params(path: Pathname).void } + def initialize(path) + @cached_location = T.let(path, Pathname) extend Pourable end + # rubocop:enable Lint/MissingSuper + sig { override.void } def clear_cache # Path is used directly and not cached. end @@ -718,6 +825,7 @@ end # # @api public class SubversionDownloadStrategy < VCSDownloadStrategy + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) super @url = @url.sub("svn+http://", "") @@ -726,6 +834,7 @@ class SubversionDownloadStrategy < VCSDownloadStrategy # Download and cache the repository at {#cached_location}. # # @api public + sig { override.params(timeout: T.any(Float, Integer, NilClass)).void } def fetch(timeout: nil) if @url.chomp("/") != repo_url || !silent_command("svn", args: ["switch", @url, cached_location]).success? clear_cache @@ -736,41 +845,39 @@ class SubversionDownloadStrategy < VCSDownloadStrategy # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { override.returns(Time) } def source_modified_time require "utils/svn" time = if Version.new(T.must(Utils::Svn.version)) >= Version.new("1.9") - out, = silent_command("svn", args: ["info", "--show-item", "last-changed-date"], chdir: cached_location) - out + silent_command("svn", args: ["info", "--show-item", "last-changed-date"], chdir: cached_location).stdout else - out, = silent_command("svn", args: ["info"], chdir: cached_location) - out[/^Last Changed Date: (.+)$/, 1] + silent_command("svn", args: ["info"], chdir: cached_location).stdout[/^Last Changed Date: (.+)$/, 1] end - Time.parse time + Time.parse T.must(time) end # Return last commit's unique identifier for the repository. # # @api public - sig { returns(String) } + sig { override.returns(String) } def last_commit - out, = silent_command("svn", args: ["info", "--show-item", "revision"], chdir: cached_location) - out.strip + silent_command("svn", args: ["info", "--show-item", "revision"], chdir: cached_location).stdout.strip end private + sig { returns(T.nilable(String)) } def repo_url - out, = silent_command("svn", args: ["info"], chdir: cached_location) - out.strip[/^URL: (.+)$/, 1] + silent_command("svn", args: ["info"], chdir: cached_location).stdout.strip[/^URL: (.+)$/, 1] end - def externals - out, = silent_command("svn", args: ["propget", "svn:externals", @url]) + sig { params(_block: T.proc.params(arg0: String, arg1: String).void).void } + def externals(&_block) + out = silent_command("svn", args: ["propget", "svn:externals", @url]).stdout out.chomp.split("\n").each do |line| name, url = line.split(/\s+/) - yield name, url + yield T.must(name), T.must(url) end end @@ -802,16 +909,17 @@ class SubversionDownloadStrategy < VCSDownloadStrategy end end - sig { returns(String) } + sig { override.returns(String) } def cache_tag head? ? "svn-HEAD" : "svn" end + sig { override.returns(T::Boolean) } def repo_valid? (cached_location/".svn").directory? end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil) case @ref_type when :revision @@ -836,6 +944,7 @@ end # # @api public class GitDownloadStrategy < VCSDownloadStrategy + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) # Needs to be before the call to `super`, as the VCSDownloadStrategy's # constructor calls `cache_tag` and sets the cache path. @@ -843,36 +952,34 @@ class GitDownloadStrategy < VCSDownloadStrategy if @only_path.present? # "Cone" mode of sparse checkout requires patterns to be directories - @only_path = "/#{@only_path}" unless @only_path.start_with?("/") - @only_path = "#{@only_path}/" unless @only_path.end_with?("/") + @only_path = T.let("/#{@only_path}", String) unless @only_path.start_with?("/") + @only_path = T.let("#{@only_path}/", String) unless @only_path.end_with?("/") end super - @ref_type ||= :branch - @ref ||= "master" + @ref_type ||= T.let(:branch, T.nilable(Symbol)) + @ref ||= T.let("master", T.untyped) end # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { override.returns(Time) } def source_modified_time - out, = silent_command("git", args: ["--git-dir", git_dir, "show", "-s", "--format=%cD"]) - Time.parse(out) + Time.parse(silent_command("git", args: ["--git-dir", git_dir, "show", "-s", "--format=%cD"]).stdout) end # Return last commit's unique identifier for the repository. # # @api public - sig { returns(String) } + sig { override.returns(String) } def last_commit - out, = silent_command("git", args: ["--git-dir", git_dir, "rev-parse", "--short=7", "HEAD"]) - out.chomp + silent_command("git", args: ["--git-dir", git_dir, "rev-parse", "--short=7", "HEAD"]).stdout.chomp end private - sig { returns(String) } + sig { override.returns(String) } def cache_tag if partial_clone_sparse_checkout? "git-sparse" @@ -886,7 +993,7 @@ class GitDownloadStrategy < VCSDownloadStrategy 0 end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def update(timeout: nil) config_repo update_repo(timeout:) @@ -895,33 +1002,39 @@ class GitDownloadStrategy < VCSDownloadStrategy update_submodules(timeout:) if submodules? end + sig { returns(T::Boolean) } def shallow_dir? (git_dir/"shallow").exist? end + sig { returns(Pathname) } def git_dir cached_location/".git" end + sig { returns(T::Boolean) } def ref? silent_command("git", args: ["--git-dir", git_dir, "rev-parse", "-q", "--verify", "#{@ref}^{commit}"]) .success? end + sig { override.returns(String) } def current_revision - out, = silent_command("git", args: ["--git-dir", git_dir, "rev-parse", "-q", "--verify", "HEAD"]) - out.strip + silent_command("git", args: ["--git-dir", git_dir, "rev-parse", "-q", "--verify", "HEAD"]).stdout.strip end + sig { override.returns(T::Boolean) } def repo_valid? silent_command("git", args: ["--git-dir", git_dir, "status", "-s"]).success? end + sig { returns(T::Boolean) } def submodules? (cached_location/".gitmodules").exist? end + sig { returns(T::Boolean) } def partial_clone_sparse_checkout? return false if @only_path.blank? @@ -1004,7 +1117,7 @@ class GitDownloadStrategy < VCSDownloadStrategy end end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil) command! "git", args: clone_args, timeout: Utils::Timer.remaining(timeout) @@ -1021,7 +1134,7 @@ class GitDownloadStrategy < VCSDownloadStrategy sig { params(timeout: T.nilable(Time)).void } def checkout(timeout: nil) - ohai "Checking out #{@ref_type} #{@ref}" if @ref_type && @ref + ohai "Checking out #{@ref_type} #{@ref}" if @ref command! "git", args: ["checkout", "-f", @ref, "--"], chdir: cached_location, timeout: Utils::Timer.remaining(timeout) end @@ -1062,6 +1175,7 @@ class GitDownloadStrategy < VCSDownloadStrategy # copied to a new location. (This bug was introduced in Git 2.7.0 and fixed # in 2.8.3. Clones created with affected version remain broken.) # See https://github.com/Homebrew/homebrew-core/pull/1520 for an example. + sig { void } def fix_absolute_submodule_gitdir_references! submodule_dirs = command!("git", args: ["submodule", "--quiet", "foreach", "--recursive", "pwd"], @@ -1090,6 +1204,7 @@ class GitDownloadStrategy < VCSDownloadStrategy end end + sig { void } def configure_sparse_checkout command! "git", args: ["config", "core.sparseCheckout", "true"], @@ -1107,16 +1222,19 @@ end # # @api public class GitHubGitDownloadStrategy < GitDownloadStrategy + sig { params(url: String, name: String, version: T.nilable(Version), meta: T::Hash[Symbol, T.untyped]).void } def initialize(url, name, version, **meta) super + @version = T.let(version, T.nilable(Version)) match_data = %r{^https?://github\.com/(?[^/]+)/(?[^/]+)\.git$}.match(@url) return unless match_data - @user = match_data[:user] - @repo = match_data[:repo] + @user = T.let(match_data[:user], T.nilable(String)) + @repo = T.let(match_data[:repo], T.nilable(String)) end + sig { override.params(commit: T.nilable(String)).returns(T::Boolean) } def commit_outdated?(commit) @last_commit ||= GitHub.last_commit(@user, @repo, @ref, version) if @last_commit @@ -1126,7 +1244,7 @@ class GitHubGitDownloadStrategy < GitDownloadStrategy if GitHub.multiple_short_commits_exist?(@user, @repo, commit) true else - version.update_commit(commit) + T.must(@version).update_commit(commit) false end else @@ -1155,7 +1273,7 @@ class GitHubGitDownloadStrategy < GitDownloadStrategy args: ["symbolic-ref", "refs/remotes/origin/HEAD"], chdir: cached_location - @default_branch = result.stdout[%r{^refs/remotes/origin/(.*)$}, 1] + @default_branch = T.let(result.stdout[%r{^refs/remotes/origin/(.*)$}, 1], T.nilable(String)) end end @@ -1163,23 +1281,28 @@ end # # @api public class CVSDownloadStrategy < VCSDownloadStrategy + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) super - @url = @url.sub(%r{^cvs://}, "") + @url = T.let(@url.sub(%r{^cvs://}, ""), String) - if meta.key?(:module) - @module = meta.fetch(:module) - elsif !@url.match?(%r{:[^/]+$}) - @module = name - else - @module, @url = split_url(@url) - end + @module = T.let( + if meta.key?(:module) + meta.fetch(:module) + elsif !@url.match?(%r{:[^/]+$}) + name + else + mod, url = split_url(@url) + @url = T.let(url, String) + mod + end, String + ) end # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { override.returns(Time) } def source_modified_time # Filter CVS's files because the timestamp for each of them is the moment # of clone. @@ -1196,24 +1319,27 @@ class CVSDownloadStrategy < VCSDownloadStrategy private + sig { override.returns(T::Hash[String, String]) } def env { "PATH" => PATH.new("/usr/bin", Formula["cvs"].opt_bin, ENV.fetch("PATH")) } end - sig { returns(String) } + sig { override.returns(String) } def cache_tag "cvs" end + sig { override.returns(T::Boolean) } def repo_valid? (cached_location/"CVS").directory? end + sig { returns(T.nilable(String)) } def quiet_flag "-Q" unless verbose? end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil) # Login is only needed (and allowed) with pserver; skip for anoncvs. if @url.include? "pserver" @@ -1227,7 +1353,7 @@ class CVSDownloadStrategy < VCSDownloadStrategy timeout: Utils::Timer.remaining(timeout) end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def update(timeout: nil) command! "cvs", args: [*quiet_flag, "update"], @@ -1235,9 +1361,10 @@ class CVSDownloadStrategy < VCSDownloadStrategy timeout: Utils::Timer.remaining(timeout) end + sig { params(in_url: String).returns([String, String]) } def split_url(in_url) parts = in_url.split(":") - mod = parts.pop + mod = T.must(parts.pop) url = parts.join(":") [mod, url] end @@ -1247,47 +1374,49 @@ end # # @api public class MercurialDownloadStrategy < VCSDownloadStrategy + sig { + params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T::Hash[Symbol, T.untyped]) + .void + } def initialize(url, name, version, **meta) super - @url = @url.sub(%r{^hg://}, "") + @url = T.let(@url.sub(%r{^hg://}, ""), String) end # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { override.returns(Time) } def source_modified_time - out, = silent_command("hg", - args: ["tip", "--template", "{date|isodate}", "-R", cached_location]) - - Time.parse(out) + Time.parse(silent_command("hg", args: ["tip", "--template", "{date|isodate}", "-R", cached_location]).stdout) end # Return last commit's unique identifier for the repository. # # @api public - sig { returns(String) } + sig { override.returns(String) } def last_commit - out, = silent_command("hg", args: ["parent", "--template", "{node|short}", "-R", cached_location]) - out.chomp + silent_command("hg", args: ["parent", "--template", "{node|short}", "-R", cached_location]).stdout.chomp end private + sig { override.returns(T::Hash[String, String]) } def env { "PATH" => PATH.new(Formula["mercurial"].opt_bin, ENV.fetch("PATH")) } end - sig { returns(String) } + sig { override.returns(String) } def cache_tag "hg" end + sig { override.returns(T::Boolean) } def repo_valid? (cached_location/".hg").directory? end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil) clone_args = %w[clone] @@ -1302,7 +1431,7 @@ class MercurialDownloadStrategy < VCSDownloadStrategy command! "hg", args: clone_args, timeout: Utils::Timer.remaining(timeout) end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def update(timeout: nil) pull_args = %w[pull] @@ -1326,9 +1455,9 @@ class MercurialDownloadStrategy < VCSDownloadStrategy command! "hg", args: ["--cwd", cached_location, *update_args], timeout: Utils::Timer.remaining(timeout) end + sig { override.returns(String) } def current_revision - out, = silent_command("hg", args: ["--cwd", cached_location, "identify", "--id"]) - out.strip + silent_command("hg", args: ["--cwd", cached_location, "identify", "--id"]).stdout.strip end end @@ -1336,18 +1465,21 @@ end # # @api public class BazaarDownloadStrategy < VCSDownloadStrategy + sig { + params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T::Hash[Symbol, T.untyped]) + .void + } def initialize(url, name, version, **meta) super - @url = @url.sub(%r{^bzr://}, "") + @url = T.let(@url.sub(%r{^bzr://}, ""), String) end # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { override.returns(Time) } def source_modified_time - out, = silent_command("bzr", args: ["log", "-l", "1", "--timezone=utc", cached_location]) - timestamp = out.chomp + timestamp = silent_command("bzr", args: ["log", "-l", "1", "--timezone=utc", cached_location]).stdout.chomp raise "Could not get any timestamps from bzr!" if timestamp.blank? Time.parse(timestamp) @@ -1356,14 +1488,14 @@ class BazaarDownloadStrategy < VCSDownloadStrategy # Return last commit's unique identifier for the repository. # # @api public - sig { returns(String) } + sig { override.returns(String) } def last_commit - out, = silent_command("bzr", args: ["revno", cached_location]) - out.chomp + silent_command("bzr", args: ["revno", cached_location]).stdout.chomp end private + sig { override.returns(T::Hash[String, String]) } def env { "PATH" => PATH.new(Formula["breezy"].opt_bin, ENV.fetch("PATH")), @@ -1371,16 +1503,17 @@ class BazaarDownloadStrategy < VCSDownloadStrategy } end - sig { returns(String) } + sig { override.returns(String) } def cache_tag "bzr" end + sig { override.returns(T::Boolean) } def repo_valid? (cached_location/".bzr").directory? end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil) # "lightweight" means history-less command! "bzr", @@ -1388,7 +1521,7 @@ class BazaarDownloadStrategy < VCSDownloadStrategy timeout: Utils::Timer.remaining(timeout) end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def update(timeout: nil) command! "bzr", args: ["update"], @@ -1401,50 +1534,53 @@ end # # @api public class FossilDownloadStrategy < VCSDownloadStrategy + sig { params(url: String, name: String, version: T.any(NilClass, String, Version), meta: T.untyped).void } def initialize(url, name, version, **meta) super - @url = @url.sub(%r{^fossil://}, "") + @url = T.let(@url.sub(%r{^fossil://}, ""), String) end # Returns the most recent modified time for all files in the current working directory after stage. # # @api public - sig { returns(Time) } + sig { override.returns(Time) } def source_modified_time - out, = silent_command("fossil", args: ["info", "tip", "-R", cached_location]) - Time.parse(out[/^uuid: +\h+ (.+)$/, 1]) + out = silent_command("fossil", args: ["info", "tip", "-R", cached_location]).stdout + Time.parse(T.must(out[/^uuid: +\h+ (.+)$/, 1])) end # Return last commit's unique identifier for the repository. # # @api public - sig { returns(String) } + sig { override.returns(String) } def last_commit - out, = silent_command("fossil", args: ["info", "tip", "-R", cached_location]) - out[/^uuid: +(\h+) .+$/, 1] + out = silent_command("fossil", args: ["info", "tip", "-R", cached_location]).stdout + T.must(out[/^uuid: +(\h+) .+$/, 1]) end + sig { override.returns(T::Boolean) } def repo_valid? silent_command("fossil", args: ["branch", "-R", cached_location]).success? end private + sig { override.returns(T::Hash[String, String]) } def env { "PATH" => PATH.new(Formula["fossil"].opt_bin, ENV.fetch("PATH")) } end - sig { returns(String) } + sig { override.returns(String) } def cache_tag "fossil" end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def clone_repo(timeout: nil) command! "fossil", args: ["clone", @url, cached_location], timeout: Utils::Timer.remaining(timeout) end - sig { params(timeout: T.nilable(Time)).void } + sig { override.params(timeout: T.nilable(Time)).void } def update(timeout: nil) command! "fossil", args: ["pull", "-R", cached_location], timeout: Utils::Timer.remaining(timeout) end @@ -1452,6 +1588,10 @@ end # Helper class for detecting a download strategy from a URL. class DownloadStrategyDetector + sig { + params(url: String, using: T.any(NilClass, Symbol, T::Class[AbstractDownloadStrategy])) + .returns(T::Class[AbstractDownloadStrategy]) + } def self.detect(url, using = nil) if using.nil? detect_from_url(url) @@ -1465,6 +1605,7 @@ class DownloadStrategyDetector end end + sig { params(url: String).returns(T::Class[AbstractDownloadStrategy]) } def self.detect_from_url(url) case url when GitHubPackages::URL_REGEX @@ -1501,6 +1642,7 @@ class DownloadStrategyDetector end end + sig { params(symbol: Symbol).returns(T::Class[AbstractDownloadStrategy]) } def self.detect_from_symbol(symbol) case symbol when :hg then MercurialDownloadStrategy diff --git a/Library/Homebrew/downloadable.rb b/Library/Homebrew/downloadable.rb index 037e9ad0d6..3f917e983b 100644 --- a/Library/Homebrew/downloadable.rb +++ b/Library/Homebrew/downloadable.rb @@ -56,7 +56,7 @@ module Downloadable sig { overridable.returns(Pathname) } def cached_download - downloader.cached_location + downloader.cached_location! end sig { overridable.void } diff --git a/Library/Homebrew/sorbet/rbi/dsl/cask/url.rbi b/Library/Homebrew/sorbet/rbi/dsl/cask/url.rbi index 2cd61a1d6f..d1205cf376 100644 --- a/Library/Homebrew/sorbet/rbi/dsl/cask/url.rbi +++ b/Library/Homebrew/sorbet/rbi/dsl/cask/url.rbi @@ -56,7 +56,7 @@ class Cask::URL sig { returns(T.nilable(T.any(::String, ::Symbol))) } def user_agent; end - sig { returns(T.nilable(T.any(::Symbol, T::Class[T.anything]))) } + sig { returns(T.nilable(T.any(::Symbol, T::Class[::AbstractDownloadStrategy]))) } def using; end sig { returns(T.nilable(::String)) } diff --git a/Library/Homebrew/test/download_strategies/abstract_spec.rb b/Library/Homebrew/test/download_strategies/abstract_spec.rb index c56451daed..b6c60ef6a3 100644 --- a/Library/Homebrew/test/download_strategies/abstract_spec.rb +++ b/Library/Homebrew/test/download_strategies/abstract_spec.rb @@ -3,7 +3,7 @@ require "download_strategy" RSpec.describe AbstractDownloadStrategy do - subject(:strategy) { described_class.new(url, name, version, **specs) } + subject(:strategy) { Class.new(described_class).new(url, name, version, **specs) } let(:specs) { {} } let(:name) { "foo" } diff --git a/Library/Homebrew/test/download_strategies/detector_spec.rb b/Library/Homebrew/test/download_strategies/detector_spec.rb index aaf84f2784..5e87822e5a 100644 --- a/Library/Homebrew/test/download_strategies/detector_spec.rb +++ b/Library/Homebrew/test/download_strategies/detector_spec.rb @@ -6,7 +6,7 @@ RSpec.describe DownloadStrategyDetector do describe "::detect" do subject(:strategy_detector) { described_class.detect(url, strategy) } - let(:url) { Object.new } + let(:url) { "invalidurl" } let(:strategy) { nil } context "when given Git URL" do diff --git a/Library/Homebrew/test/download_strategies/subversion_spec.rb b/Library/Homebrew/test/download_strategies/subversion_spec.rb index c1f08caf16..79cb20dc6a 100644 --- a/Library/Homebrew/test/download_strategies/subversion_spec.rb +++ b/Library/Homebrew/test/download_strategies/subversion_spec.rb @@ -17,6 +17,8 @@ RSpec.describe SubversionDownloadStrategy do it "adds the appropriate svn args" do expect(strategy).to receive(:system_command!) .with("svn", hash_including(args: array_including("--trust-server-cert", "--non-interactive"))) + .and_return(instance_double(SystemCommand::Result)) + strategy.fetch end end @@ -27,6 +29,7 @@ RSpec.describe SubversionDownloadStrategy do it "adds svn arguments for :revision" do expect(strategy).to receive(:system_command!) .with("svn", hash_including(args: array_including_cons("-r", "10"))) + .and_return(instance_double(SystemCommand::Result)) strategy.fetch end diff --git a/Library/Homebrew/test/download_strategies/vcs_spec.rb b/Library/Homebrew/test/download_strategies/vcs_spec.rb index 4f564a6133..5bc2100e48 100644 --- a/Library/Homebrew/test/download_strategies/vcs_spec.rb +++ b/Library/Homebrew/test/download_strategies/vcs_spec.rb @@ -9,7 +9,7 @@ RSpec.describe VCSDownloadStrategy do describe "#cached_location" do it "returns the path of the cached resource" do allow_any_instance_of(described_class).to receive(:cache_tag).and_return("foo") - downloader = described_class.new(url, "baz", version) + downloader = Class.new(described_class).new(url, "baz", version) expect(downloader.cached_location).to eq(HOMEBREW_CACHE/"baz--foo") end end diff --git a/Library/Homebrew/url.rb b/Library/Homebrew/url.rb index e0ccdb9ee8..c31dc0d840 100644 --- a/Library/Homebrew/url.rb +++ b/Library/Homebrew/url.rb @@ -1,16 +1,20 @@ -# typed: true # rubocop:todo Sorbet/StrictSigil +# typed: strict # frozen_string_literal: true require "version" class URL - attr_reader :specs, :using + sig { returns(T::Hash[Symbol, T.untyped]) } + attr_reader :specs + + sig { returns(T.any(NilClass, Symbol, T::Class[AbstractDownloadStrategy])) } + attr_reader :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) + @url = T.let(url.freeze, String) + @specs = T.let(specs.dup, T::Hash[Symbol, T.untyped]) + @using = T.let(@specs.delete(:using), T.any(NilClass, Symbol, T::Class[AbstractDownloadStrategy])) @specs.freeze end @@ -19,13 +23,14 @@ class URL @url end - sig { returns(T.class_of(AbstractDownloadStrategy)) } + sig { returns(T::Class[AbstractDownloadStrategy]) } def download_strategy - @download_strategy ||= DownloadStrategyDetector.detect(@url, @using) + @download_strategy ||= + T.let(DownloadStrategyDetector.detect(@url, @using), T.nilable(T::Class[AbstractDownloadStrategy])) end sig { returns(Version) } def version - @version ||= Version.detect(@url, **@specs) + @version ||= T.let(Version.detect(@url, **@specs), T.nilable(Version)) end end diff --git a/Library/Homebrew/utils/curl.rb b/Library/Homebrew/utils/curl.rb index 0fda86f324..0fb62364a1 100644 --- a/Library/Homebrew/utils/curl.rb +++ b/Library/Homebrew/utils/curl.rb @@ -211,7 +211,7 @@ module Utils end sig { - params( + overridable.params( args: String, print_stdout: T.any(T::Boolean, Symbol), options: T.untyped, @@ -264,7 +264,7 @@ module Utils curl(*args, **options) end - sig { params(args: String, options: T.untyped).returns(SystemCommand::Result) } + sig { overridable.params(args: String, options: T.untyped).returns(SystemCommand::Result) } def curl_output(*args, **options) curl_with_workarounds(*args, print_stderr: false, show_output: true, **options) end diff --git a/Library/Homebrew/utils/github/artifacts.rb b/Library/Homebrew/utils/github/artifacts.rb index 607ee521f9..1bfc967d4f 100644 --- a/Library/Homebrew/utils/github/artifacts.rb +++ b/Library/Homebrew/utils/github/artifacts.rb @@ -31,14 +31,14 @@ class GitHubArtifactDownloadStrategy < AbstractFileDownloadStrategy @token = T.let(token, String) end - sig { params(timeout: T.nilable(Integer)).void } + sig { override.params(timeout: T.any(Float, Integer, NilClass)).void } def fetch(timeout: nil) ohai "Downloading #{url}" if cached_location.exist? puts "Already downloaded: #{cached_location}" else begin - Utils::Curl.curl("--location", "--create-dirs", "--output", temporary_path, url, + Utils::Curl.curl("--location", "--create-dirs", "--output", temporary_path.to_s, url, "--header", "Authorization: token #{@token}", secrets: [@token], timeout:) @@ -46,7 +46,7 @@ class GitHubArtifactDownloadStrategy < AbstractFileDownloadStrategy raise CurlDownloadStrategyError, url end cached_location.dirname.mkpath - temporary_path.rename(cached_location) + temporary_path.rename(cached_location.to_s) end symlink_location.dirname.mkpath