brew/Library/Homebrew/requirement.rb

304 lines
7.4 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "attrable"
require "dependable"
require "dependency"
require "dependencies"
require "build_environment"
# A base class for non-formula requirements needed by formulae.
2020-08-18 03:02:33 +02:00
# A fatal requirement is one that will fail the build if it is not present.
# By default, requirements are non-fatal.
class Requirement
include Dependable
extend Cachable
2024-11-21 18:10:20 -08:00
extend T::Helpers
# This base class enforces no constraints on its own.
# Individual subclasses use the `satisfy` DSL to define those constraints.
abstract!
2023-01-22 17:03:27 -08:00
attr_reader :name, :cask, :download
def initialize(tags = [])
2019-01-17 09:57:50 +00:00
@cask = self.class.cask
@download = self.class.download
tags.each do |tag|
next unless tag.is_a? Hash
2018-09-17 02:45:00 +02:00
@cask ||= tag[:cask]
@download ||= tag[:download]
end
@tags = tags
@tags << :build if self.class.build
@name ||= infer_name
end
def option_names
[name]
end
# The message to show when the requirement is not met.
2020-10-20 12:03:48 +02:00
sig { returns(String) }
def message
_, _, class_name = self.class.to_s.rpartition "::"
s = "#{class_name} unsatisfied!\n"
if cask
2017-10-15 02:28:32 +02:00
s += <<~EOS
2020-11-18 08:10:21 +01:00
You can install the necessary cask with:
brew install --cask #{cask}
EOS
end
if download
2017-10-15 02:28:32 +02:00
s += <<~EOS
You can download from:
#{Formatter.url(download)}
EOS
end
s
end
# Overriding {#satisfied?} is unsupported.
# Pass a block or boolean to the satisfy DSL method instead.
sig {
params(
env: T.nilable(String),
cc: T.nilable(String),
build_bottle: T::Boolean,
bottle_arch: T.nilable(String),
).returns(T::Boolean)
}
def satisfied?(env: nil, cc: nil, build_bottle: false, bottle_arch: nil)
satisfy = self.class.satisfy
return true unless satisfy
2018-09-17 02:45:00 +02:00
@satisfied_result =
2024-03-07 16:20:20 +00:00
satisfy.yielder(env:, cc:, build_bottle:, bottle_arch:) do |p|
instance_eval(&p)
end
return false unless @satisfied_result
2018-09-17 02:45:00 +02:00
true
end
# Overriding {#fatal?} is unsupported.
# Pass a boolean to the fatal DSL method instead.
sig { returns(T::Boolean) }
def fatal?
self.class.fatal || false
end
def satisfied_result_parent
return unless @satisfied_result.is_a?(Pathname)
2018-09-17 02:45:00 +02:00
parent = @satisfied_result.resolved_path.parent
2020-11-16 22:18:56 +01:00
if parent.to_s =~ %r{^#{Regexp.escape(HOMEBREW_CELLAR)}/([\w+-.@]+)/[^/]+/(s?bin)/?$}o
parent = HOMEBREW_PREFIX/"opt/#{Regexp.last_match(1)}/#{Regexp.last_match(2)}"
end
parent
end
# Pass a block to the env DSL method instead of overriding.
sig(:final) {
params(
env: T.nilable(String),
cc: T.nilable(String),
build_bottle: T::Boolean,
bottle_arch: T.nilable(String),
2024-02-19 13:29:49 -08:00
).void
}
def modify_build_environment(env: nil, cc: nil, build_bottle: false, bottle_arch: nil)
2024-03-07 16:20:20 +00:00
satisfied?(env:, cc:, build_bottle:, bottle_arch:)
instance_eval(&env_proc) if env_proc
# XXX If the satisfy block returns a Pathname, then make sure that it
# remains available on the PATH. This makes requirements like
# satisfy { which("executable") }
# work, even under superenv where "executable" wouldn't normally be on the
# PATH.
parent = satisfied_result_parent
return unless parent
return if ["#{HOMEBREW_PREFIX}/bin", "#{HOMEBREW_PREFIX}/bin"].include?(parent.to_s)
return if PATH.new(ENV.fetch("PATH")).include?(parent.to_s)
2018-09-17 02:45:00 +02:00
ENV.prepend_path("PATH", parent)
end
def env
2014-07-07 21:32:35 -05:00
self.class.env
end
def env_proc
self.class.env_proc
end
2014-11-12 17:35:16 -06:00
def ==(other)
instance_of?(other.class) && name == other.name && tags == other.tags
end
2016-09-23 18:13:48 +02:00
alias eql? ==
def hash
2022-11-09 01:19:46 +00:00
[self.class, name, tags].hash
end
2020-10-20 12:03:48 +02:00
sig { returns(String) }
2013-06-07 22:24:36 -05:00
def inspect
"#<#{self.class.name}: #{tags.inspect}>"
2013-06-07 22:24:36 -05:00
end
def display_s
name.capitalize
end
2020-08-19 17:12:32 +01:00
def mktemp(&block)
Mktemp.new(name).run(&block)
end
private
def infer_name
klass = self.class.name
klass = klass&.sub(/(Dependency|Requirement)$/, "")
&.sub(/^(\w+::)*/, "")
return klass.downcase if klass.present?
return @cask if @cask.present?
""
end
def which(cmd)
2017-04-27 10:44:44 +02:00
super(cmd, PATH.new(ORIGINAL_PATHS))
end
def which_all(cmd)
2017-04-27 10:44:44 +02:00
super(cmd, PATH.new(ORIGINAL_PATHS))
end
class << self
2017-05-09 23:00:51 +02:00
include BuildEnvironment::DSL
2014-07-07 20:16:51 -05:00
attr_reader :env_proc, :build
2025-02-23 13:18:49 -08:00
sig { params(val: T.nilable(String)).returns(T.nilable(String)) }
def cask(val = nil)
val.nil? ? @cask : @cask = val
end
sig { params(val: T.nilable(String)).returns(T.nilable(String)) }
def download(val = nil)
val.nil? ? @download : @download = val
end
sig { params(val: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
def fatal(val = nil)
val.nil? ? @fatal : @fatal = val
end
def satisfy(options = nil, &block)
2020-11-16 22:18:56 +01:00
return @satisfied if options.nil? && !block
2018-09-17 02:45:00 +02:00
options = {} if options.nil?
2020-08-18 03:02:33 +02:00
@satisfied = Satisfier.new(options, &block)
end
def env(*settings, &block)
2020-11-16 22:18:56 +01:00
if block
@env_proc = block
else
super
end
end
end
2020-08-18 03:02:33 +02:00
# Helper class for evaluating whether a requirement is satisfied.
class Satisfier
def initialize(options, &block)
case options
when Hash
@options = { build_env: true }
@options.merge!(options)
else
@satisfied = options
end
@proc = block
end
def yielder(env: nil, cc: nil, build_bottle: false, bottle_arch: nil)
if instance_variable_defined?(:@satisfied)
@satisfied
elsif @options[:build_env]
require "extend/ENV"
ENV.with_build_environment(
2024-03-07 16:20:20 +00:00
env:, cc:, build_bottle:, bottle_arch:,
) do
yield @proc
end
else
yield @proc
end
end
end
2020-08-18 03:02:33 +02:00
private_constant :Satisfier
2013-06-03 15:08:46 -05:00
class << self
# Expand the requirements of dependent recursively, optionally yielding
# `[dependent, req]` pairs to allow callers to apply arbitrary filters to
2013-06-03 15:08:46 -05:00
# the list.
# The default filter, which is applied when a block is not given, omits
2023-07-17 12:30:12 -07:00
# optionals and recommends based on what the dependent has asked for.
def expand(dependent, cache_key: nil, &block)
if cache_key.present?
cache[cache_key] ||= {}
return cache[cache_key][cache_id dependent].dup if cache[cache_key][cache_id dependent]
end
reqs = Requirements.new
2013-06-03 15:08:46 -05:00
formulae = dependent.recursive_dependencies.map(&:to_formula)
formulae.unshift(dependent)
formulae.each do |f|
f.requirements.each do |req|
2016-09-23 22:02:23 +02:00
next if prune?(f, req, &block)
2018-09-17 02:45:00 +02:00
2016-09-23 22:02:23 +02:00
reqs << req
end
2013-06-03 15:08:46 -05:00
end
2023-09-05 16:30:30 -04:00
if cache_key.present?
2023-09-05 22:40:23 -04:00
# Even though we setup the cache above
# 'dependent.recursive_dependencies.map(&:to_formula)'
# is invalidating the singleton cache
2023-09-05 16:30:30 -04:00
cache[cache_key] ||= {}
cache[cache_key][cache_id dependent] = reqs.dup
end
2013-06-03 15:08:46 -05:00
reqs
end
2020-11-16 22:18:56 +01:00
def prune?(dependent, req, &block)
2013-06-03 15:08:46 -05:00
catch(:prune) do
2020-11-16 22:18:56 +01:00
if block
2013-06-03 15:08:46 -05:00
yield dependent, req
elsif req.optional? || req.recommended?
prune unless dependent.build.with?(req)
2013-06-03 15:08:46 -05:00
end
end
end
2013-06-03 15:08:46 -05:00
# Used to prune requirements when calling expand with a block.
2020-10-20 12:03:48 +02:00
sig { void }
2013-06-03 15:08:46 -05:00
def prune
throw(:prune, true)
end
private
def cache_id(dependent)
"#{dependent.full_name}_#{dependent.class}"
end
end
end