2022-09-14 22:23:01 -04:00

400 lines
11 KiB
Ruby

# typed: false
# frozen_string_literal: true
require "cxxstdlib"
require "ostruct"
require "options"
require "json"
require "development_tools"
require "extend/cachable"
# Inherit from OpenStruct to gain a generic initialization method that takes a
# hash and creates an attribute for each key and value. Rather than calling
# `new` directly, use one of the class methods like {Tab.create}.
class Tab < OpenStruct
extend T::Sig
extend Cachable
FILENAME = "INSTALL_RECEIPT.json"
# Instantiates a Tab for a new installation of a formula.
def self.create(formula, compiler, stdlib)
build = formula.build
runtime_deps = formula.runtime_dependencies(undeclared: false)
attributes = {
"homebrew_version" => HOMEBREW_VERSION,
"used_options" => build.used_options.as_flags,
"unused_options" => build.unused_options.as_flags,
"tabfile" => formula.prefix/FILENAME,
"built_as_bottle" => build.bottle?,
"installed_as_dependency" => false,
"installed_on_request" => false,
"poured_from_bottle" => false,
"loaded_from_api" => false,
"time" => Time.now.to_i,
"source_modified_time" => formula.source_modified_time.to_i,
"compiler" => compiler,
"stdlib" => stdlib,
"aliases" => formula.aliases,
"runtime_dependencies" => Tab.runtime_deps_hash(formula, runtime_deps),
"arch" => Hardware::CPU.arch,
"source" => {
"path" => formula.specified_path.to_s,
"tap" => formula.tap&.name,
"tap_git_head" => nil, # Filled in later if possible
"spec" => formula.active_spec_sym.to_s,
"versions" => {
"stable" => formula.stable&.version.to_s,
"head" => formula.head&.version.to_s,
"version_scheme" => formula.version_scheme,
},
},
"built_on" => DevelopmentTools.build_system_info,
}
# We can only get `tap_git_head` if the tap is installed locally
attributes["source"]["tap_git_head"] = formula.tap.git_head if formula.tap&.installed?
new(attributes)
end
# Returns the {Tab} for an install receipt at `path`.
# Results are cached.
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
end
# Like {from_file}, but bypass the cache.
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
attributes["source_modified_time"] ||= 0
attributes["source"] ||= {}
tapped_from = attributes["tapped_from"]
if !tapped_from.nil? && tapped_from != "path or URL"
attributes["source"]["tap"] = attributes.delete("tapped_from")
end
if attributes["source"]["tap"] == "mxcl/master" ||
attributes["source"]["tap"] == "Homebrew/homebrew"
attributes["source"]["tap"] = "homebrew/core"
end
if attributes["source"]["spec"].nil?
version = PkgVersion.parse path.to_s.split("/").second_to_last
attributes["source"]["spec"] = if version.head?
"head"
else
"stable"
end
end
if attributes["source"]["versions"].nil?
attributes["source"]["versions"] = {
"stable" => nil,
"head" => nil,
"version_scheme" => 0,
}
end
new(attributes)
end
def self.for_keg(keg)
path = keg/FILENAME
tab = if path.exist?
from_file(path)
else
empty
end
tab["tabfile"] = path
tab
end
# Returns a {Tab} for the named formula's installation,
# or a fake one if the formula is not installed.
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
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.
def self.for_formula(f)
paths = []
paths << f.opt_prefix.resolved_path if f.opt_prefix.symlink? && f.opt_prefix.directory?
paths << f.linked_keg.resolved_path if f.linked_keg.symlink? && f.linked_keg.directory?
if (dirs = f.installed_prefixes).length == 1
paths << dirs.first
end
paths << f.latest_installed_prefix
path = paths.map { |pn| pn/FILENAME }.find(&:file?)
if path
tab = from_file(path)
used_options = remap_deprecated_options(f.deprecated_options, tab.used_options)
tab.used_options = used_options.as_flags
else
# Formula is not installed. Return a fake tab.
tab = empty
tab.unused_options = f.options.as_flags
tab.source = {
"path" => f.specified_path.to_s,
"tap" => f.tap&.name,
"spec" => f.active_spec_sym.to_s,
"versions" => {
"stable" => f.stable&.version.to_s,
"head" => f.head&.version.to_s,
"version_scheme" => f.version_scheme,
},
}
end
tab
end
def self.empty
attributes = {
"homebrew_version" => HOMEBREW_VERSION,
"used_options" => [],
"unused_options" => [],
"built_as_bottle" => false,
"installed_as_dependency" => false,
"installed_on_request" => false,
"poured_from_bottle" => false,
"loaded_from_api" => false,
"time" => nil,
"source_modified_time" => 0,
"stdlib" => nil,
"compiler" => DevelopmentTools.default_compiler,
"aliases" => [],
"runtime_dependencies" => nil,
"arch" => nil,
"source" => {
"path" => nil,
"tap" => nil,
"tap_git_head" => nil,
"spec" => "stable",
"versions" => {
"stable" => nil,
"head" => nil,
"version_scheme" => 0,
},
},
"built_on" => DevelopmentTools.generic_build_system_info,
}
new(attributes)
end
def self.runtime_deps_hash(formula, deps)
deps.map do |dep|
f = dep.to_formula
{
"full_name" => f.full_name,
"version" => f.version.to_s,
"declared_directly" => formula.deps.include?(dep),
}
end
end
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)
end
def include?(opt)
used_options.include? opt
end
def head?
spec == :head
end
def stable?
spec == :stable
end
def used_options
Options.create(super)
end
def unused_options
Options.create(super)
end
def compiler
super || DevelopmentTools.default_compiler
end
def parsed_homebrew_version
return Version::NULL if homebrew_version.nil?
Version.new(homebrew_version)
end
def runtime_dependencies
# Homebrew versions prior to 1.1.6 generated incorrect runtime dependency
# lists.
super unless parsed_homebrew_version < "1.1.6"
end
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
def built_bottle?
built_as_bottle && !poured_from_bottle
end
def bottle?
built_as_bottle
end
def tap
tap_name = source["tap"]
Tap.fetch(tap_name) if tap_name
end
def tap=(tap)
tap_name = tap.respond_to?(:name) ? tap.name : tap
source["tap"] = tap_name
end
def spec
source["spec"].to_sym
end
def versions
source["versions"]
end
def stable_version
Version.create(versions["stable"]) if versions["stable"]
end
def head_version
Version.create(versions["head"]) if versions["head"]
end
def version_scheme
versions["version_scheme"] || 0
end
sig { returns(Time) }
def source_modified_time
Time.at(super || 0)
end
def to_json(options = nil)
attributes = {
"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,
"loaded_from_api" => loaded_from_api,
"installed_as_dependency" => installed_as_dependency,
"installed_on_request" => installed_on_request,
"changed_files" => changed_files&.map(&:to_s),
"time" => time,
"source_modified_time" => source_modified_time.to_i,
"stdlib" => stdlib&.to_s,
"compiler" => compiler&.to_s,
"aliases" => aliases,
"runtime_dependencies" => runtime_dependencies,
"source" => source,
"arch" => arch,
"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
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
def write
# If this is a new installation, the cache of installed formulae
# will no longer be valid.
Formula.clear_cache unless tabfile.exist?
self.class.cache[tabfile] = self
tabfile.atomic_write(to_json)
end
sig { returns(String) }
def to_s
s = []
s << if poured_from_bottle
"Poured from bottle"
else
"Built from source"
end
s << "using the formulae.brew.sh API" if loaded_from_api
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