538 lines
16 KiB
Ruby
Raw Permalink 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 "cxxstdlib"
require "options"
require "json"
require "development_tools"
require "cachable"
2023-03-14 22:47:25 -07:00
# Rather than calling `new` directly, use one of the class methods like {Tab.create}.
2024-06-22 13:31:50 -04:00
class AbstractTab
extend Cachable
extend T::Helpers
abstract!
FILENAME = "INSTALL_RECEIPT.json"
2013-01-23 00:26:20 -06:00
2024-06-22 13:31:50 -04:00
# Check whether the formula or cask was installed as a dependency.
#
2024-04-22 21:05:48 +02:00
# @api internal
sig { returns(T.nilable(T::Boolean)) } # TODO: change this to always return a boolean
2024-04-22 21:05:48 +02:00
attr_accessor :installed_as_dependency
2024-06-22 13:31:50 -04:00
# Check whether the formula or cask was installed on request.
#
2024-04-22 21:05:48 +02:00
# @api internal
sig { returns(T.nilable(T::Boolean)) } # TODO: change this to always return a boolean
2024-04-22 21:05:48 +02:00
attr_accessor :installed_on_request
sig { returns(T.nilable(String)) }
attr_accessor :homebrew_version
attr_accessor :tabfile, :loaded_from_api, :time, :arch, :source, :built_on
2024-04-22 21:05:48 +02:00
2024-06-22 13:31:50 -04:00
# Returns the formula or cask runtime dependencies.
#
2024-04-22 21:05:48 +02:00
# @api internal
2024-06-22 13:31:50 -04:00
attr_accessor :runtime_dependencies
2023-03-14 22:47:25 -07:00
sig { params(attributes: T::Hash[String, T.untyped]).void }
def initialize(attributes = {})
@installed_as_dependency = T.let(nil, T.nilable(T::Boolean))
@installed_on_request = T.let(nil, T.nilable(T::Boolean))
@homebrew_version = T.let(nil, T.nilable(String))
@tabfile = T.let(nil, T.nilable(Pathname))
@loaded_from_api = T.let(nil, T.nilable(T::Boolean))
@time = T.let(nil, T.nilable(Integer))
@arch = T.let(nil, T.nilable(String))
@source = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
@built_on = T.let(nil, T.nilable(T::Hash[String, T.untyped]))
@runtime_dependencies = T.let(nil, T.nilable(T::Array[T.untyped]))
attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
end
2024-07-04 11:18:26 -04:00
# Instantiates a {Tab} for a new installation of a formula or cask.
sig { params(formula_or_cask: T.any(Formula, Cask::Cask)).returns(T.attached_class) }
2024-07-04 11:18:26 -04:00
def self.create(formula_or_cask)
attributes = {
2018-11-02 17:18:07 +00:00
"homebrew_version" => HOMEBREW_VERSION,
"installed_as_dependency" => false,
"installed_on_request" => false,
2024-06-22 13:31:50 -04:00
"loaded_from_api" => formula_or_cask.loaded_from_api?,
2018-11-02 17:18:07 +00:00
"time" => Time.now.to_i,
2020-04-06 13:04:48 +01:00
"arch" => Hardware::CPU.arch,
2024-07-04 11:18:26 -04:00
"source" => {
"tap" => formula_or_cask.tap&.name,
"tap_git_head" => formula_or_cask.tap_git_head,
2024-07-04 11:18:26 -04:00
},
2020-04-06 13:04:48 +01:00
"built_on" => DevelopmentTools.build_system_info,
}
2024-07-04 11:18:26 -04:00
new(attributes)
end
2024-06-22 13:31:50 -04:00
# Returns the {Tab} for a formula or cask install receipt at `path`.
#
# NOTE: Results are cached.
sig { params(path: T.any(Pathname, String)).returns(T.attached_class) }
def self.from_file(path)
cache.fetch(path) do |p|
content = File.read(p)
return empty if content.blank?
cache[p] = from_file_content(content, p)
end
2015-05-23 18:08:07 +08:00
end
# Like {from_file}, but bypass the cache.
sig { params(content: String, path: T.any(Pathname, String)).returns(T.attached_class) }
def self.from_file_content(content, path)
attributes = begin
JSON.parse(content)
rescue JSON::ParserError => e
raise e, "Cannot parse #{path}: #{e}", e.backtrace
end
attributes["tabfile"] = path
2024-06-22 13:31:50 -04:00
new(attributes)
end
sig { returns(T.attached_class) }
2024-06-22 13:31:50 -04:00
def self.empty
attributes = {
"homebrew_version" => HOMEBREW_VERSION,
"installed_as_dependency" => false,
"installed_on_request" => false,
"loaded_from_api" => false,
"time" => nil,
"runtime_dependencies" => nil,
"arch" => nil,
"source" => {
"path" => nil,
"tap" => nil,
"tap_git_head" => nil,
},
"built_on" => DevelopmentTools.build_system_info,
2024-06-22 13:31:50 -04:00
}
new(attributes)
end
def self.formula_to_dep_hash(formula, declared_deps)
{
"full_name" => formula.full_name,
"version" => formula.version.to_s,
"revision" => formula.revision,
"bottle_rebuild" => formula.bottle&.rebuild,
"pkg_version" => formula.pkg_version.to_s,
"declared_directly" => declared_deps.include?(formula.full_name),
}.compact
end
private_class_method :formula_to_dep_hash
sig { returns(Version) }
2024-06-22 13:31:50 -04:00
def parsed_homebrew_version
homebrew_version = self.homebrew_version
2024-06-22 13:31:50 -04:00
return Version::NULL if homebrew_version.nil?
Version.new(homebrew_version)
end
sig { returns(T.nilable(Tap)) }
def tap
tap_name = source["tap"]
Tap.fetch(tap_name) if tap_name
end
sig { params(tap: T.nilable(T.any(Tap, String))).void }
2024-06-22 13:31:50 -04:00
def tap=(tap)
tap_name = tap.is_a?(Tap) ? tap.name : tap
2024-06-22 13:31:50 -04:00
source["tap"] = tap_name
end
sig { void }
2024-06-22 13:31:50 -04:00
def write
self.class.cache[tabfile] = self
tabfile.atomic_write(to_json)
end
end
class Tab < AbstractTab
# Check whether the formula was poured from a bottle.
#
# @api internal
attr_accessor :poured_from_bottle
attr_accessor :built_as_bottle, :changed_files, :stdlib, :aliases
attr_writer :used_options, :unused_options, :compiler, :source_modified_time
attr_reader :tapped_from
sig { params(attributes: T::Hash[String, T.untyped]).void }
def initialize(attributes = {})
@poured_from_bottle = T.let(nil, T.nilable(T::Boolean))
@built_as_bottle = T.let(nil, T.nilable(T::Boolean))
@changed_files = T.let(nil, T.nilable(T::Array[Pathname]))
@stdlib = T.let(nil, T.nilable(String))
@aliases = T.let(nil, T.nilable(T::Array[String]))
@used_options = T.let(nil, T.nilable(T::Array[String]))
@unused_options = T.let(nil, T.nilable(T::Array[String]))
@compiler = T.let(nil, T.nilable(String))
@source_modified_time = T.let(nil, T.nilable(Integer))
@tapped_from = T.let(nil, T.nilable(String))
super
end
2024-06-22 13:31:50 -04:00
# Instantiates a {Tab} for a new installation of a formula.
sig {
override.params(formula_or_cask: T.any(Formula, Cask::Cask), compiler: T.any(Symbol, String),
stdlib: T.nilable(T.any(String, Symbol))).returns(T.attached_class)
}
def self.create(formula_or_cask, compiler = DevelopmentTools.default_compiler, stdlib = nil)
formula = T.cast(formula_or_cask, Formula)
2024-07-04 11:18:26 -04:00
tab = super(formula)
2024-06-22 13:31:50 -04:00
build = formula.build
runtime_deps = formula.runtime_dependencies(undeclared: false)
2024-07-04 11:18:26 -04:00
tab.used_options = build.used_options.as_flags
tab.unused_options = build.unused_options.as_flags
tab.tabfile = formula.prefix/FILENAME
tab.built_as_bottle = build.bottle?
tab.poured_from_bottle = false
tab.source_modified_time = formula.source_modified_time.to_i
tab.compiler = compiler
tab.stdlib = stdlib
tab.aliases = formula.aliases
tab.runtime_dependencies = Tab.runtime_deps_hash(formula, runtime_deps)
tab.source["spec"] = formula.active_spec_sym.to_s
2024-07-04 11:18:30 -04:00
tab.source["path"] = formula.specified_path.to_s
2024-07-04 11:18:26 -04:00
tab.source["versions"] = {
"stable" => formula.stable&.version&.to_s,
"head" => formula.head&.version&.to_s,
"version_scheme" => formula.version_scheme,
}
2024-06-22 13:31:50 -04:00
2024-07-04 11:18:26 -04:00
tab
2024-06-22 13:31:50 -04:00
end
# Like {from_file}, but bypass the cache.
sig { params(content: String, path: T.any(Pathname, String)).returns(T.attached_class) }
2024-06-22 13:31:50 -04:00
def self.from_file_content(content, path)
tab = super
tab.source ||= {}
tab.tap = tab.tapped_from if !tab.tapped_from.nil? && tab.tapped_from != "path or URL"
2025-01-10 11:00:16 -08:00
tab.tap = "homebrew/core" if ["mxcl/master", "Homebrew/homebrew"].include?(tab.tap)
2024-06-22 13:31:50 -04:00
if tab.source["spec"].nil?
version = PkgVersion.parse(File.basename(File.dirname(path)))
2024-06-22 13:31:50 -04:00
tab.source["spec"] = if version.head?
2020-03-13 21:15:06 +00:00
"head"
2015-07-28 15:33:07 +08:00
else
2020-03-13 21:15:06 +00:00
"stable"
2015-07-28 15:33:07 +08:00
end
end
2024-07-05 10:07:30 -04:00
tab.source["versions"] ||= empty_source_versions
2016-07-04 12:39:08 +03:00
2023-05-11 02:25:04 +01:00
# Tabs created with Homebrew 1.5.13 through 4.0.17 inclusive created empty string versions in some cases.
["stable", "head"].each do |spec|
2024-06-22 13:31:50 -04:00
tab.source["versions"][spec] = tab.source["versions"][spec].presence
2023-05-11 02:25:04 +01:00
end
2024-06-22 13:31:50 -04:00
tab
end
# Get the {Tab} for the given {Keg},
# or a fake one if the formula is not installed.
#
2024-04-22 21:05:48 +02:00
# @api internal
sig { params(keg: T.any(Keg, Pathname)).returns(T.attached_class) }
def self.for_keg(keg)
2017-06-01 16:06:51 +02:00
path = keg/FILENAME
tab = if path.exist?
2014-06-29 22:26:14 -05:00
from_file(path)
else
2015-02-21 12:15:39 -05:00
empty
end
2023-03-14 22:47:25 -07:00
tab.tabfile = path
tab
end
# Returns a {Tab} for the named formula's installation,
2016-09-07 23:17:19 +01:00
# or a fake one if the formula is not installed.
sig { params(name: String).returns(T.attached_class) }
def self.for_name(name)
for_formula(Formulary.factory(name))
end
def self.remap_deprecated_options(deprecated_options, options)
deprecated_options.each do |deprecated_option|
option = options.find { |o| o.name == deprecated_option.old }
next unless option
2018-09-17 02:45:00 +02:00
options -= [option]
options << Option.new(deprecated_option.current, option.description)
end
options
end
# Returns a {Tab} for an already installed formula,
# or a fake one if the formula is not installed.
sig { params(formula: Formula).returns(T.attached_class) }
def self.for_formula(formula)
paths = []
paths << formula.opt_prefix.resolved_path if formula.opt_prefix.symlink? && formula.opt_prefix.directory?
paths << formula.linked_keg.resolved_path if formula.linked_keg.symlink? && formula.linked_keg.directory?
if (dirs = formula.installed_prefixes).length == 1
paths << dirs.first
end
paths << formula.latest_installed_prefix
path = paths.map { |pathname| pathname/FILENAME }.find(&:file?)
if path
tab = from_file(path)
used_options = remap_deprecated_options(formula.deprecated_options, tab.used_options)
tab.used_options = used_options.as_flags
else
# Formula is not installed. Return a fake tab.
2015-02-21 12:15:39 -05:00
tab = empty
tab.unused_options = formula.options.as_flags
2015-12-06 22:33:41 +08:00
tab.source = {
2024-07-04 11:18:26 -04:00
"path" => formula.specified_path.to_s,
"tap" => formula.tap&.name,
"tap_git_head" => formula.tap_git_head,
2024-07-04 11:18:26 -04:00
"spec" => formula.active_spec_sym.to_s,
"versions" => {
2023-05-11 02:25:04 +01:00
"stable" => formula.stable&.version&.to_s,
"head" => formula.head&.version&.to_s,
"version_scheme" => formula.version_scheme,
},
2015-12-06 22:33:41 +08:00
}
end
2015-02-21 12:15:39 -05:00
tab
end
sig { returns(T.attached_class) }
2015-02-21 12:15:39 -05:00
def self.empty
2024-06-22 13:31:50 -04:00
tab = super
tab.used_options = []
tab.unused_options = []
tab.built_as_bottle = false
tab.poured_from_bottle = false
tab.source_modified_time = 0
tab.stdlib = nil
tab.compiler = DevelopmentTools.default_compiler
tab.aliases = []
tab.source["spec"] = "stable"
2024-07-05 10:07:30 -04:00
tab.source["versions"] = empty_source_versions
tab
end
sig { returns(T::Hash[String, T.untyped]) }
2024-07-05 10:07:30 -04:00
def self.empty_source_versions
{
2024-06-22 13:31:50 -04:00
"stable" => nil,
"head" => nil,
"version_scheme" => 0,
}
end
2024-07-05 10:07:30 -04:00
private_class_method :empty_source_versions
def self.runtime_deps_hash(formula, deps)
deps.map do |dep|
formula_to_dep_hash(dep.to_formula, formula.deps.map(&:name))
end
end
sig { returns(T::Boolean) }
def any_args_or_options?
!used_options.empty? || !unused_options.empty?
end
def with?(val)
option_names = val.respond_to?(:option_names) ? val.option_names : [val]
option_names.any? do |name|
include?("with-#{name}") || unused_options.include?("without-#{name}")
end
end
def without?(val)
!with?(val)
2014-07-30 21:04:17 -05:00
end
sig { params(opt: String).returns(T::Boolean) }
def include?(opt)
used_options.include? opt
end
sig { returns(T::Boolean) }
def head?
spec == :head
end
sig { returns(T::Boolean) }
def stable?
spec == :stable
end
# The options used to install the formula.
#
2024-04-22 21:05:48 +02:00
# @api internal
sig { returns(Options) }
def used_options
2023-03-14 22:47:25 -07:00
Options.create(@used_options)
end
sig { returns(Options) }
def unused_options
2023-03-14 22:47:25 -07:00
Options.create(@unused_options)
end
sig { returns(T.any(String, Symbol)) }
def compiler
2023-03-14 22:47:25 -07:00
@compiler || DevelopmentTools.default_compiler
end
def runtime_dependencies
# Homebrew versions prior to 1.1.6 generated incorrect runtime dependency
# lists.
2023-04-18 15:06:50 -07:00
@runtime_dependencies if parsed_homebrew_version >= "1.1.6"
end
sig { returns(CxxStdlib) }
def cxxstdlib
# Older tabs won't have these values, so provide sensible defaults
lib = stdlib.to_sym if stdlib
CxxStdlib.create(lib, compiler.to_sym)
end
sig { returns(T::Boolean) }
def built_bottle?
2014-10-15 00:52:57 -05:00
built_as_bottle && !poured_from_bottle
end
sig { returns(T::Boolean) }
def bottle?
built_as_bottle
end
sig { returns(Symbol) }
2015-07-28 15:33:07 +08:00
def spec
source["spec"].to_sym
end
sig { returns(T::Hash[String, T.untyped]) }
2016-07-04 12:39:08 +03:00
def versions
source["versions"]
end
sig { returns(T.nilable(Version)) }
2016-07-04 12:39:08 +03:00
def stable_version
versions["stable"]&.then { Version.new(_1) }
2016-07-04 12:39:08 +03:00
end
sig { returns(T.nilable(Version)) }
2016-07-04 12:39:08 +03:00
def head_version
versions["head"]&.then { Version.new(_1) }
2016-07-04 12:39:08 +03:00
end
sig { returns(Integer) }
2016-08-11 10:00:39 +02:00
def version_scheme
versions["version_scheme"] || 0
end
2020-10-20 12:03:48 +02:00
sig { returns(Time) }
2016-01-14 18:55:47 +08:00
def source_modified_time
2023-03-14 22:47:25 -07:00
Time.at(@source_modified_time || 0)
2016-01-14 18:55:47 +08:00
end
sig { params(options: T.nilable(T::Hash[String, T.untyped])).returns(String) }
2019-03-18 15:04:37 +00:00
def to_json(options = nil)
attributes = {
2018-11-02 17:18:07 +00:00
"homebrew_version" => homebrew_version,
"used_options" => used_options.as_flags,
"unused_options" => unused_options.as_flags,
"built_as_bottle" => built_as_bottle,
"poured_from_bottle" => poured_from_bottle,
2022-09-10 20:28:21 -04:00
"loaded_from_api" => loaded_from_api,
"installed_as_dependency" => installed_as_dependency,
2018-11-02 17:18:07 +00:00
"installed_on_request" => installed_on_request,
"changed_files" => changed_files&.map(&:to_s),
"time" => time,
"source_modified_time" => source_modified_time.to_i,
2019-11-28 15:10:50 +00:00
"stdlib" => stdlib&.to_s,
"compiler" => compiler.to_s,
2018-11-02 17:18:07 +00:00
"aliases" => aliases,
"runtime_dependencies" => runtime_dependencies,
"source" => source,
"arch" => arch,
2020-04-06 13:04:48 +01:00
"built_on" => built_on,
}
attributes.delete("stdlib") if attributes["stdlib"].blank?
JSON.pretty_generate(attributes, options)
end
# A subset of to_json that we care about for bottles.
sig { returns(T::Hash[String, T.untyped]) }
def to_bottle_hash
attributes = {
"homebrew_version" => homebrew_version,
"changed_files" => changed_files&.map(&:to_s),
"source_modified_time" => source_modified_time.to_i,
"stdlib" => stdlib&.to_s,
"compiler" => compiler.to_s,
"runtime_dependencies" => runtime_dependencies,
"arch" => arch,
"built_on" => built_on,
}
attributes.delete("stdlib") if attributes["stdlib"].blank?
attributes
end
sig { void }
def write
# If this is a new installation, the cache of installed formulae
# will no longer be valid.
Formula.clear_cache unless tabfile.exist?
2024-06-22 13:31:50 -04:00
super
end
2020-10-20 12:03:48 +02:00
sig { returns(String) }
def to_s
s = []
2020-03-13 21:15:06 +00:00
s << if poured_from_bottle
"Poured from bottle"
else
2020-03-13 21:15:06 +00:00
"Built from source"
end
2016-09-23 11:01:40 +02:00
2022-09-10 20:28:21 -04:00
s << "using the formulae.brew.sh API" if loaded_from_api
2016-09-23 11:01:40 +02:00
s << Time.at(time).strftime("on %Y-%m-%d at %H:%M:%S") if time
unless used_options.empty?
s << "with:"
s << used_options.to_a.join(" ")
end
s.join(" ")
end
end