Merge pull request #17554 from Homebrew/cask-install-receipt

This commit is contained in:
Mike McQuaid 2024-07-13 10:55:06 -04:00 committed by GitHub
commit f39b5c1426
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1147 additions and 219 deletions

View File

@ -20,5 +20,6 @@ require "cask/migrator"
require "cask/pkg"
require "cask/quarantine"
require "cask/staged"
require "cask/tab"
require "cask/url"
require "cask/utils"

View File

@ -7,6 +7,7 @@ require "cask/cask_loader"
require "cask/config"
require "cask/dsl"
require "cask/metadata"
require "cask/tab"
require "utils/bottles"
require "extend/api_hashable"
@ -158,6 +159,17 @@ module Cask
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
end
def uninstall_flight_blocks?
artifacts.any? do |artifact|
case artifact
when Artifact::PreflightBlock
artifact.directives.key?(:uninstall_preflight)
when Artifact::PostflightBlock
artifact.directives.key?(:uninstall_postflight)
end
end
end
sig { returns(T.nilable(Time)) }
def install_time
# <caskroom_path>/.metadata/<version>/<timestamp>/Casks/<token>.{rb,json} -> <timestamp>
@ -209,6 +221,10 @@ module Cask
bundle_version&.version
end
def tab
Tab.for_cask(self)
end
def config_path
metadata_main_container_path/"config.json"
end
@ -465,6 +481,27 @@ module Cask
hash
end
def artifacts_list(compact: false, uninstall_only: false)
artifacts.filter_map do |artifact|
case artifact
when Artifact::AbstractFlightBlock
uninstall_flight_block = artifact.directives.key?(:uninstall_preflight) ||
artifact.directives.key?(:uninstall_postflight)
next if uninstall_only && !uninstall_flight_block
# Only indicate whether this block is used as we don't load it from the API
# We can skip this entirely once we move to internal JSON v3.
{ artifact.summarize.to_sym => nil } unless compact
else
zap_artifact = artifact.is_a?(Artifact::Zap)
uninstall_artifact = artifact.respond_to?(:uninstall_phase) || artifact.respond_to?(:post_uninstall_phase)
next if uninstall_only && !zap_artifact && !uninstall_artifact
{ artifact.class.dsl_key => artifact.to_args }
end
end
end
private
sig { returns(T.nilable(Homebrew::BundleVersion)) }
@ -482,19 +519,6 @@ module Cask
hash
end
def artifacts_list(compact: false)
artifacts.filter_map do |artifact|
case artifact
when Artifact::AbstractFlightBlock
# Only indicate whether this block is used as we don't load it from the API
# We can skip this entirely once we move to internal JSON v3.
{ artifact.summarize => nil } unless compact
else
{ artifact.class.dsl_key => artifact.to_args }
end
end
end
def url_specs
url&.specs.dup.tap do |url_specs|
case url_specs&.dig(:user_agent)

View File

@ -12,7 +12,7 @@ module Cask
output << "#{Formatter.url(cask.homepage)}\n" if cask.homepage
deprecate_disable = DeprecateDisable.message(cask)
output << "#{deprecate_disable.capitalize}\n" if deprecate_disable
output << installation_info(cask)
output << "#{installation_info(cask)}\n"
repo = repo_info(cask)
output << "#{repo}\n" if repo
output << name_info(cask)
@ -37,7 +37,7 @@ module Cask
end
def self.installation_info(cask)
return "Not installed\n" unless cask.installed?
return "Not installed" unless cask.installed?
versioned_staged_path = cask.caskroom_path.join(cask.installed_version)
path_details = if versioned_staged_path.exist?
@ -46,7 +46,12 @@ module Cask
Formatter.error("does not exist")
end
"Installed\n#{versioned_staged_path} (#{path_details})\n"
tab = Tab.for_cask(cask)
info = ["Installed"]
info << "#{versioned_staged_path} (#{path_details})"
info << " #{tab}" if tab.tabfile&.exist?
info.join("\n")
end
def self.name_info(cask)

View File

@ -10,6 +10,7 @@ require "cask/config"
require "cask/download"
require "cask/migrator"
require "cask/quarantine"
require "cask/tab"
require "cgi"
@ -21,8 +22,8 @@ module Cask
def initialize(cask, command: SystemCommand, force: false, adopt: false,
skip_cask_deps: false, binaries: true, verbose: false,
zap: false, require_sha: false, upgrade: false, reinstall: false,
installed_as_dependency: false, quarantine: true,
verify_download_integrity: true, quiet: false)
installed_as_dependency: false, installed_on_request: true,
quarantine: true, verify_download_integrity: true, quiet: false)
@cask = cask
@command = command
@force = force
@ -35,13 +36,14 @@ module Cask
@reinstall = reinstall
@upgrade = upgrade
@installed_as_dependency = installed_as_dependency
@installed_on_request = installed_on_request
@quarantine = quarantine
@verify_download_integrity = verify_download_integrity
@quiet = quiet
end
attr_predicate :binaries?, :force?, :adopt?, :skip_cask_deps?, :require_sha?,
:reinstall?, :upgrade?, :verbose?, :zap?, :installed_as_dependency?,
:reinstall?, :upgrade?, :verbose?, :zap?, :installed_as_dependency?, :installed_on_request?,
:quarantine?, :quiet?
def self.caveats(cask)
@ -112,6 +114,11 @@ module Cask
install_artifacts(predecessor:)
tab = Tab.create(@cask)
tab.installed_as_dependency = installed_as_dependency?
tab.installed_on_request = installed_on_request?
tab.write
if (tap = @cask.tap) && tap.should_report_analytics?
::Utils::Analytics.report_package_event(:cask_install, package_name: @cask.token, tap_name: tap.name,
on_request: true)
@ -356,6 +363,7 @@ on_request: true)
binaries: binaries?,
verbose: verbose?,
installed_as_dependency: true,
installed_on_request: false,
force: false,
).install
else
@ -408,6 +416,7 @@ on_request: true)
oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}"
uninstall_artifacts(clear: true, successor:)
if !reinstall? && !upgrade?
remove_tabfile
remove_download_sha
remove_config_file
end
@ -415,6 +424,12 @@ on_request: true)
purge_caskroom_path if force?
end
def remove_tabfile
tabfile = @cask.tab.tabfile
FileUtils.rm_f tabfile if tabfile
@cask.config_path.parent.rmdir_if_possible
end
def remove_config_file
FileUtils.rm_f @cask.config_path
@cask.config_path.parent.rmdir_if_possible

View File

@ -0,0 +1,108 @@
# typed: true
# frozen_string_literal: true
require "tab"
module Cask
class Tab < ::AbstractTab
attr_accessor :uninstall_flight_blocks, :uninstall_artifacts
# Instantiates a {Tab} for a new installation of a cask.
def self.create(cask)
tab = super
tab.tabfile = cask.metadata_main_container_path/FILENAME
tab.uninstall_flight_blocks = cask.uninstall_flight_blocks?
tab.runtime_dependencies = Tab.runtime_deps_hash(cask)
tab.source["version"] = cask.version.to_s
tab.source["path"] = cask.sourcefile_path.to_s
tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true)
tab
end
# Returns a {Tab} for an already installed cask,
# or a fake one if the cask is not installed.
def self.for_cask(cask)
path = cask.metadata_main_container_path/FILENAME
return from_file(path) if path.exist?
tab = empty
tab.source = {
"path" => cask.sourcefile_path.to_s,
"tap" => cask.tap&.name,
"tap_git_head" => cask.tap_git_head,
"version" => cask.version.to_s,
}
tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true)
tab
end
def self.empty
tab = super
tab.uninstall_flight_blocks = false
tab.uninstall_artifacts = []
tab.source["version"] = nil
tab
end
def self.runtime_deps_hash(cask)
cask_and_formula_dep_graph = ::Utils::TopologicalHash.graph_package_dependencies(cask)
cask_deps, formula_deps = cask_and_formula_dep_graph.values.flatten.uniq.partition do |dep|
dep.is_a?(Cask)
end
runtime_deps = {}
if cask_deps.any?
runtime_deps[:cask] = cask_deps.map do |dep|
{
"full_name" => dep.full_name,
"version" => dep.version.to_s,
"declared_directly" => cask.depends_on.cask.include?(dep.full_name),
}
end
end
if formula_deps.any?
runtime_deps[:formula] = formula_deps.map do |dep|
formula_to_dep_hash(dep, cask.depends_on.formula)
end
end
runtime_deps
end
def version
source["version"]
end
def to_json(*_args)
attributes = {
"homebrew_version" => homebrew_version,
"loaded_from_api" => loaded_from_api,
"uninstall_flight_blocks" => uninstall_flight_blocks,
"installed_as_dependency" => installed_as_dependency,
"installed_on_request" => installed_on_request,
"time" => time,
"runtime_dependencies" => runtime_dependencies,
"source" => source,
"arch" => arch,
"uninstall_artifacts" => uninstall_artifacts,
"built_on" => built_on,
}
JSON.pretty_generate(attributes)
end
def to_s
s = ["Installed"]
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
s.join(" ")
end
end
end

View File

@ -10,7 +10,7 @@ module Homebrew
class TabCmd < AbstractCommand
cmd_args do
description <<~EOS
Edit tab information for installed formulae.
Edit tab information for installed formulae or casks.
This can be useful when you want to control whether an installed
formula should be removed by `brew autoremove`.
@ -19,13 +19,18 @@ module Homebrew
EOS
switch "--installed-on-request",
description: "Mark <formula> as installed on request."
description: "Mark <installed_formula> or <installed_cask> as installed on request."
switch "--no-installed-on-request",
description: "Mark <formula> as not installed on request."
description: "Mark <installed_formula> or <installed_cask> as not installed on request."
switch "--formula", "--formulae",
description: "Only mark formulae."
switch "--cask", "--casks",
description: "Only mark casks."
conflicts "--formula", "--cask"
conflicts "--installed-on-request", "--no-installed-on-request"
named_args :formula, min: 1
named_args [:installed_formula, :installed_cask], min: 1
end
sig { override.void }
@ -37,38 +42,45 @@ module Homebrew
end
raise UsageError, "No marking option specified." if installed_on_request.nil?
formulae = args.named.to_formulae
if (formulae_not_installed = formulae.reject(&:any_version_installed?)).any?
formula_names = formulae_not_installed.map(&:name)
is_or_are = (formula_names.length == 1) ? "is" : "are"
odie "#{formula_names.to_sentence} #{is_or_are} not installed."
formulae, casks = args.named.to_formulae_to_casks
formulae_not_installed = formulae.reject(&:any_version_installed?)
casks_not_installed = casks.reject(&:installed?)
if formulae_not_installed.any? || casks_not_installed.any?
names = formulae_not_installed.map(&:name) + casks_not_installed.map(&:token)
is_or_are = (names.length == 1) ? "is" : "are"
odie "#{names.to_sentence} #{is_or_are} not installed."
end
formulae.each do |formula|
update_tab formula, installed_on_request:
[*formulae, *casks].each do |formula_or_cask|
update_tab formula_or_cask, installed_on_request:
end
end
private
sig { params(formula: Formula, installed_on_request: T::Boolean).void }
def update_tab(formula, installed_on_request:)
tab = Tab.for_formula(formula)
unless tab.tabfile.exist?
sig { params(formula_or_cask: T.any(Formula, Cask::Cask), installed_on_request: T::Boolean).void }
def update_tab(formula_or_cask, installed_on_request:)
name, tab = if formula_or_cask.is_a?(Formula)
[formula_or_cask.name, Tab.for_formula(formula_or_cask)]
else
[formula_or_cask.token, formula_or_cask.tab]
end
if tab.tabfile.blank? || !tab.tabfile.exist?
raise ArgumentError,
"Tab file for #{formula.name} does not exist."
"Tab file for #{name} does not exist."
end
installed_on_request_str = "#{"not " unless installed_on_request}installed on request"
if (tab.installed_on_request && installed_on_request) ||
(!tab.installed_on_request && !installed_on_request)
ohai "#{formula.name} is already marked as #{installed_on_request_str}."
ohai "#{name} is already marked as #{installed_on_request_str}."
return
end
tab.installed_on_request = installed_on_request
tab.write
ohai "#{formula.name} is now marked as #{installed_on_request_str}."
ohai "#{name} is now marked as #{installed_on_request_str}."
end
end
end

View File

@ -524,7 +524,7 @@ module Homebrew
tab.time = nil
tab.changed_files = changed_files.dup
if args.only_json_tab?
tab.changed_files.delete(Pathname.new(Tab::FILENAME))
tab.changed_files.delete(Pathname.new(AbstractTab::FILENAME))
tab.tabfile.unlink
else
tab.write

View File

@ -637,7 +637,7 @@ class Formula
# If at least one version of {Formula} is installed.
sig { returns(T::Boolean) }
def any_version_installed?
installed_prefixes.any? { |keg| (keg/Tab::FILENAME).file? }
installed_prefixes.any? { |keg| (keg/AbstractTab::FILENAME).file? }
end
# The link status symlink directory for this {Formula}.

View File

@ -344,6 +344,9 @@ module Cask
sig { returns(T::Boolean) }
def installed_as_dependency?; end
sig { returns(T::Boolean) }
def installed_on_request?; end
sig { returns(T::Boolean) }
def quarantine?; end

View File

@ -8,78 +8,48 @@ require "development_tools"
require "extend/cachable"
# Rather than calling `new` directly, use one of the class methods like {Tab.create}.
class Tab
class AbstractTab
extend Cachable
FILENAME = "INSTALL_RECEIPT.json"
# Check whether the formula was installed as a dependency.
# Check whether the formula or cask was installed as a dependency.
#
# @api internal
attr_accessor :installed_as_dependency
# Check whether the formula was installed on request.
# Check whether the formula or cask was installed on request.
#
# @api internal
attr_accessor :installed_on_request
# Check whether the formula was poured from a bottle.
attr_accessor :homebrew_version, :tabfile, :loaded_from_api, :time, :arch, :source, :built_on
# Returns the formula or cask runtime dependencies.
#
# @api internal
attr_accessor :poured_from_bottle
attr_accessor :runtime_dependencies
attr_accessor :homebrew_version, :tabfile, :built_as_bottle,
:changed_files, :loaded_from_api, :time, :stdlib, :aliases, :arch, :source,
:built_on
attr_writer :used_options, :unused_options, :compiler, :source_modified_time
# Returns the formula's runtime dependencies.
#
# @api internal
attr_writer :runtime_dependencies
# 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)
# Instantiates a {Tab} for a new installation of a formula or cask.
def self.create(formula_or_cask)
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,
"loaded_from_api" => formula_or_cask.loaded_from_api?,
"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,
},
"tap" => formula_or_cask.tap&.name,
"tap_git_head" => formula_or_cask.tap_git_head,
},
"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`.
# Returns the {Tab} for a formula or cask install receipt at `path`.
#
# NOTE: Results are cached.
def self.from_file(path)
@ -99,42 +69,132 @@ class Tab
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
new(attributes)
end
if attributes["source"]["tap"] == "mxcl/master" ||
attributes["source"]["tap"] == "Homebrew/homebrew"
attributes["source"]["tap"] = "homebrew/core"
end
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.generic_build_system_info,
}
if attributes["source"]["spec"].nil?
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,
"pkg_version" => formula.pkg_version.to_s,
"declared_directly" => declared_deps.include?(formula.full_name),
}
end
private_class_method :formula_to_dep_hash
def initialize(attributes = {})
attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
end
def parsed_homebrew_version
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
def tap=(tap)
tap_name = tap.respond_to?(:name) ? tap.name : tap
source["tap"] = tap_name
end
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
# Instantiates a {Tab} for a new installation of a formula.
def self.create(formula, compiler, stdlib)
tab = super(formula)
build = formula.build
runtime_deps = formula.runtime_dependencies(undeclared: false)
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
tab.source["path"] = formula.specified_path.to_s
tab.source["versions"] = {
"stable" => formula.stable&.version&.to_s,
"head" => formula.head&.version&.to_s,
"version_scheme" => formula.version_scheme,
}
tab
end
# Like {from_file}, but bypass the cache.
def self.from_file_content(content, path)
tab = super
tab.source_modified_time ||= 0
tab.source ||= {}
tab.tap = tab.tapped_from if !tab.tapped_from.nil? && tab.tapped_from != "path or URL"
tab.tap = "homebrew/core" if tab.tap == "mxcl/master" || tab.tap == "Homebrew/homebrew"
if tab.source["spec"].nil?
version = PkgVersion.parse(File.basename(File.dirname(path)))
attributes["source"]["spec"] = if version.head?
tab.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
tab.source["versions"] ||= empty_source_versions
# Tabs created with Homebrew 1.5.13 through 4.0.17 inclusive created empty string versions in some cases.
["stable", "head"].each do |spec|
attributes["source"]["versions"][spec] = attributes["source"]["versions"][spec].presence
tab.source["versions"][spec] = tab.source["versions"][spec].presence
end
new(attributes)
tab
end
# Get the {Tab} for the given {Keg},
@ -198,10 +258,11 @@ class Tab
tab = empty
tab.unused_options = formula.options.as_flags
tab.source = {
"path" => formula.specified_path.to_s,
"tap" => formula.tap&.name,
"spec" => formula.active_spec_sym.to_s,
"versions" => {
"path" => formula.specified_path.to_s,
"tap" => formula.tap&.name,
"tap_git_head" => formula.tap_git_head,
"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,
@ -213,56 +274,37 @@ class 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,
}
tab = super
new(attributes)
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"
tab.source["versions"] = empty_source_versions
tab
end
def self.empty_source_versions
{
"stable" => nil,
"head" => nil,
"version_scheme" => 0,
}
end
private_class_method :empty_source_versions
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,
"revision" => f.revision,
"pkg_version" => f.pkg_version.to_s,
"declared_directly" => formula.deps.include?(dep),
}
formula_to_dep_hash(dep.to_formula, formula.deps.map(&:name))
end
end
def initialize(attributes = {})
attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
end
def any_args_or_options?
!used_options.empty? || !unused_options.empty?
end
@ -307,12 +349,6 @@ class Tab
@compiler || 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.
@ -333,17 +369,6 @@ class Tab
built_as_bottle
end
sig { returns(T.nilable(Tap)) }
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
@ -416,8 +441,7 @@ class Tab
# will no longer be valid.
Formula.clear_cache unless tabfile.exist?
self.class.cache[tabfile] = self
tabfile.atomic_write(to_json)
super
end
sig { returns(String) }

View File

@ -212,6 +212,115 @@ RSpec.describe Cask::Cask, :cask do
end
end
describe "#artifacts_list" do
subject(:cask) { Cask::CaskLoader.load("many-artifacts") }
it "returns all artifacts when no options are given" do
expected_artifacts = [
{ uninstall_preflight: nil },
{ preflight: nil },
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ pkg: ["ManyArtifacts/ManyArtifacts.pkg"] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ uninstall_postflight: nil },
{ postflight: nil },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list).to eq(expected_artifacts)
end
it "skips flight blocks when compact is true" do
expected_artifacts = [
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ pkg: ["ManyArtifacts/ManyArtifacts.pkg"] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list(compact: true)).to eq(expected_artifacts)
end
it "returns only uninstall artifacts when uninstall_only is true" do
expected_artifacts = [
{ uninstall_preflight: nil },
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ uninstall_postflight: nil },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list(uninstall_only: true)).to eq(expected_artifacts)
end
it "skips flight blocks and returns only uninstall artifacts when compact and uninstall_only are true" do
expected_artifacts = [
{ uninstall: [{
rmdir: "#{TEST_TMPDIR}/empty_directory_path",
trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
}] },
{ app: ["ManyArtifacts/ManyArtifacts.app"] },
{ zap: [{
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"],
trash: "~/Library/Logs/ManyArtifacts.log",
}] },
]
expect(cask.artifacts_list(compact: true, uninstall_only: true)).to eq(expected_artifacts)
end
end
describe "#uninstall_flight_blocks?" do
matcher :have_uninstall_flight_blocks do
match do |actual|
actual.uninstall_flight_blocks? == true
end
end
it "returns true when there are uninstall_preflight blocks" do
cask = Cask::CaskLoader.load("with-uninstall-preflight")
expect(cask).to have_uninstall_flight_blocks
end
it "returns true when there are uninstall_postflight blocks" do
cask = Cask::CaskLoader.load("with-uninstall-postflight")
expect(cask).to have_uninstall_flight_blocks
end
it "returns false when there are only preflight blocks" do
cask = Cask::CaskLoader.load("with-preflight")
expect(cask).not_to have_uninstall_flight_blocks
end
it "returns false when there are only postflight blocks" do
cask = Cask::CaskLoader.load("with-postflight")
expect(cask).not_to have_uninstall_flight_blocks
end
it "returns false when there are no flight blocks" do
cask = Cask::CaskLoader.load("local-caffeine")
expect(cask).not_to have_uninstall_flight_blocks
end
end
describe "#to_h" do
let(:expected_json) { (TEST_FIXTURE_DIR/"cask/everything.json").read.strip }

View File

@ -121,4 +121,30 @@ RSpec.describe Cask::Info, :cask do
Caffeine.app (App)
EOS
end
it "prints install information for an installed Cask" do
cask = Cask::CaskLoader.load("local-transmission")
time = 1_720_189_863
tab = Cask::Tab.new(loaded_from_api: true, tabfile: TEST_FIXTURE_DIR/"cask_receipt.json", time:)
expect(cask).to receive(:installed?).and_return(true)
expect(cask).to receive(:installed_version).and_return("2.61")
expect(Cask::Tab).to receive(:for_cask).with(cask).and_return(tab)
expect do
described_class.info(cask)
end.to output(<<~EOS).to_stdout
==> local-transmission: 2.61
https://transmissionbt.com/
Installed
#{HOMEBREW_PREFIX}/Caskroom/local-transmission/2.61 (does not exist)
Installed using the formulae.brew.sh API on #{Time.at(time).strftime("%Y-%m-%d at %H:%M:%S")}
From: https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/l/local-transmission.rb
==> Name
Transmission
==> Description
BitTorrent client
==> Artifacts
Transmission.app (App)
EOS
end
end

View File

@ -0,0 +1,356 @@
# frozen_string_literal: true
require "cask"
RSpec.describe Cask::Tab, :cask do
matcher :be_installed_as_dependency do
match do |actual|
actual.installed_as_dependency == true
end
end
matcher :be_installed_on_request do
match do |actual|
actual.installed_on_request == true
end
end
matcher :be_loaded_from_api do
match do |actual|
actual.loaded_from_api == true
end
end
matcher :have_uninstall_flight_blocks do
match do |actual|
actual.uninstall_flight_blocks == true
end
end
subject(:tab) do
described_class.new(
"homebrew_version" => HOMEBREW_VERSION,
"loaded_from_api" => false,
"uninstall_flight_blocks" => true,
"installed_as_dependency" => false,
"installed_on_request" => true,
"time" => time,
"runtime_dependencies" => {
"cask" => [{ "full_name" => "bar", "version" => "2.0", "declared_directly" => false }],
},
"source" => {
"path" => CoreCaskTap.instance.path.to_s,
"tap" => CoreCaskTap.instance.to_s,
"tap_git_head" => "8b79aa759500f0ffdf65a23e12950cbe3bf8fe17",
"version" => "1.2.3",
},
"arch" => Hardware::CPU.arch,
"uninstall_artifacts" => [{ "app" => ["Foo.app"] }],
"built_on" => DevelopmentTools.build_system_info,
)
end
let(:time) { Time.now.to_i }
let(:f) { formula { url "foo-1.0" } }
let(:f_tab_path) { f.prefix/"INSTALL_RECEIPT.json" }
let(:f_tab_content) { (TEST_FIXTURE_DIR/"receipt.json").read }
specify "defaults" do
stub_const("HOMEBREW_VERSION", "4.3.7")
tab = described_class.empty
expect(tab.homebrew_version).to eq(HOMEBREW_VERSION)
expect(tab).not_to be_installed_as_dependency
expect(tab).not_to be_installed_on_request
expect(tab).not_to be_loaded_from_api
expect(tab).not_to have_uninstall_flight_blocks
expect(tab.tap).to be_nil
expect(tab.time).to be_nil
expect(tab.runtime_dependencies).to be_nil
expect(tab.source["path"]).to be_nil
end
specify "#runtime_dependencies" do
tab = described_class.new
expect(tab.runtime_dependencies).to be_nil
tab.runtime_dependencies = {}
expect(tab.runtime_dependencies).not_to be_nil
tab.runtime_dependencies = {
"cask" => [{ "full_name" => "bar", "version" => "2.0", "declared_directly" => false }],
}
expect(tab.runtime_dependencies).not_to be_nil
end
describe "::runtime_deps_hash" do
specify "with no dependencies" do
cask = Cask::CaskLoader.load("local-transmission")
expect(described_class.runtime_deps_hash(cask)).to eq({})
end
specify "with cask dependencies" do
cask = Cask::CaskLoader.load("with-depends-on-cask")
expected_hash = {
cask: [
{ "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>true },
],
}
expect(described_class.runtime_deps_hash(cask)).to eq(expected_hash)
end
it "ignores macos symbol dependencies" do
cask = Cask::CaskLoader.load("with-depends-on-macos-symbol")
expect(described_class.runtime_deps_hash(cask)).to eq({})
end
it "ignores macos array dependencies" do
cask = Cask::CaskLoader.load("with-depends-on-macos-array")
expect(described_class.runtime_deps_hash(cask)).to eq({})
end
it "ignores arch dependencies" do
cask = Cask::CaskLoader.load("with-depends-on-arch")
expect(described_class.runtime_deps_hash(cask)).to eq({})
end
specify "with all types of dependencies" do
cask = Cask::CaskLoader.load("with-depends-on-everything")
unar = instance_double(Formula, full_name: "unar", version: "1.2", revision: 0, pkg_version: "1.2",
deps: [], requirements: [])
expect(Formulary).to receive(:factory).with("unar").and_return(unar)
expected_hash = {
cask: [
{ "full_name"=>"local-caffeine", "version"=>"1.2.3", "declared_directly"=>true },
{ "full_name"=>"with-depends-on-cask", "version"=>"1.2.3", "declared_directly"=>true },
{ "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>false },
],
formula: [
{ "full_name"=>"unar", "version"=>"1.2", "revision"=>0, "pkg_version"=>"1.2", "declared_directly"=>true },
],
}
runtime_deps_hash = described_class.runtime_deps_hash(cask)
tab = described_class.new
tab.runtime_dependencies = runtime_deps_hash
expect(tab.runtime_dependencies).to eql(expected_hash)
end
end
specify "other attributes" do
expect(tab.tap.name).to eq("homebrew/cask")
expect(tab.time).to eq(time)
expect(tab).not_to be_loaded_from_api
expect(tab).to have_uninstall_flight_blocks
expect(tab).not_to be_installed_as_dependency
expect(tab).to be_installed_on_request
expect(tab).not_to be_loaded_from_api
end
describe "::from_file" do
it "parses a cask Tab from a file" do
path = Pathname.new("#{TEST_FIXTURE_DIR}/cask_receipt.json")
tab = described_class.from_file(path)
source_path = "/opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/f/foo.rb"
runtime_dependencies = {
"cask" => [
{
"full_name" => "bar",
"version" => "2.0",
"declared_directly" => true,
},
],
"formula" => [
{
"full_name" => "baz",
"version" => "3.0",
"revision" => 0,
"pkg_version" => "3.0",
"declared_directly" => true,
},
],
"macos" => {
">=" => [
"12",
],
},
}
expect(tab).not_to be_loaded_from_api
expect(tab).to have_uninstall_flight_blocks
expect(tab).not_to be_installed_as_dependency
expect(tab).to be_installed_on_request
expect(tab.time).to eq(Time.at(1_719_289_256).to_i)
expect(tab.runtime_dependencies).to eq(runtime_dependencies)
expect(tab.source["path"]).to eq(source_path)
expect(tab.version).to eq("1.2.3")
expect(tab.tap.name).to eq("homebrew/cask")
end
end
describe "::from_file_content" do
it "parses a cask Tab from a file" do
path = Pathname.new("#{TEST_FIXTURE_DIR}/cask_receipt.json")
tab = described_class.from_file_content(path.read, path)
source_path = "/opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/f/foo.rb"
runtime_dependencies = {
"cask" => [
{
"full_name" => "bar",
"version" => "2.0",
"declared_directly" => true,
},
],
"formula" => [
{
"full_name" => "baz",
"version" => "3.0",
"revision" => 0,
"pkg_version" => "3.0",
"declared_directly" => true,
},
],
"macos" => {
">=" => [
"12",
],
},
}
expect(tab).not_to be_loaded_from_api
expect(tab).to have_uninstall_flight_blocks
expect(tab).not_to be_installed_as_dependency
expect(tab).to be_installed_on_request
expect(tab.tabfile).to eq(path)
expect(tab.time).to eq(Time.at(1_719_289_256).to_i)
expect(tab.runtime_dependencies).to eq(runtime_dependencies)
expect(tab.source["path"]).to eq(source_path)
expect(tab.version).to eq("1.2.3")
expect(tab.tap.name).to eq("homebrew/cask")
end
it "raises a parse exception message including the Tab filename" do
expect { described_class.from_file_content("''", "cask_receipt.json") }.to raise_error(
JSON::ParserError,
/receipt.json:/,
)
end
end
describe "::create" do
it "creates a cask Tab" do
cask = Cask::CaskLoader.load("local-caffeine")
expected_artifacts = [
{ app: ["Caffeine.app"] },
{ zap: [{ trash: "#{TEST_FIXTURE_DIR}/cask/caffeine/org.example.caffeine.plist" }] },
]
tab = described_class.create(cask)
expect(tab).not_to be_loaded_from_api
expect(tab).not_to have_uninstall_flight_blocks
expect(tab).not_to be_installed_as_dependency
expect(tab).not_to be_installed_on_request
expect(tab.source).to eq({
"path" => "#{CoreCaskTap.instance.path}/Casks/local-caffeine.rb",
"tap" => CoreCaskTap.instance.name,
"tap_git_head" => nil,
"version" => "1.2.3",
})
expect(tab.runtime_dependencies).to eq({})
expect(tab.uninstall_artifacts).to eq(expected_artifacts)
end
end
describe "::for_cask" do
let(:cask) { Cask::CaskLoader.load("local-transmission") }
let(:cask_tab_path) { cask.metadata_main_container_path/AbstractTab::FILENAME }
let(:cask_tab_content) { (TEST_FIXTURE_DIR/"cask_receipt.json").read }
it "creates a Tab for a given cask" do
tab = described_class.for_cask(cask)
expect(tab.source["path"]).to eq(cask.sourcefile_path.to_s)
end
it "creates a Tab for a given cask with existing Tab" do
cask_tab_path.dirname.mkpath
cask_tab_path.write cask_tab_content
tab = described_class.for_cask(cask)
expect(tab.tabfile).to eq(cask_tab_path)
end
it "can create a Tab for a non-existent cask" do
cask_tab_path.dirname.mkpath
tab = described_class.for_cask(cask)
expect(tab.tabfile).to be_nil
end
end
specify "#to_json" do
json_tab = described_class.new(JSON.parse(tab.to_json))
expect(json_tab.homebrew_version).to eq(tab.homebrew_version)
expect(json_tab.loaded_from_api).to eq(tab.loaded_from_api)
expect(json_tab.uninstall_flight_blocks).to eq(tab.uninstall_flight_blocks)
expect(json_tab.installed_as_dependency).to eq(tab.installed_as_dependency)
expect(json_tab.installed_on_request).to eq(tab.installed_on_request)
expect(json_tab.time).to eq(tab.time)
expect(json_tab.runtime_dependencies).to eq(tab.runtime_dependencies)
expect(json_tab.source["path"]).to eq(tab.source["path"])
expect(json_tab.tap).to eq(tab.tap)
expect(json_tab.source["tap_git_head"]).to eq(tab.source["tap_git_head"])
expect(json_tab.version).to eq(tab.version)
expect(json_tab.arch).to eq(tab.arch.to_s)
expect(json_tab.uninstall_artifacts).to eq(tab.uninstall_artifacts)
expect(json_tab.built_on["os"]).to eq(tab.built_on["os"])
end
describe "#to_s" do
let(:time_string) { Time.at(1_720_189_863).strftime("%Y-%m-%d at %H:%M:%S") }
it "returns install information for a Tab with a time that was loaded from the API" do
tab = described_class.new(
loaded_from_api: true,
time: 1_720_189_863,
)
output = "Installed using the formulae.brew.sh API on #{time_string}"
expect(tab.to_s).to eq(output)
end
it "returns install information for a Tab with a time that was not loaded from the API" do
tab = described_class.new(
loaded_from_api: false,
time: 1_720_189_863,
)
output = "Installed on #{time_string}"
expect(tab.to_s).to eq(output)
end
it "returns install information for a Tab without a time that was loaded from the API" do
tab = described_class.new(
loaded_from_api: true,
time: nil,
)
output = "Installed using the formulae.brew.sh API"
expect(tab.to_s).to eq(output)
end
it "returns install information for a Tab without a time that was not loaded from the API" do
tab = described_class.new(
loaded_from_api: false,
time: nil,
)
output = "Installed"
expect(tab.to_s).to eq(output)
end
end
end

View File

@ -32,7 +32,7 @@ RSpec.describe Homebrew::Cmd::Deps do
# Mock `Formula#any_version_installed?` by creating the tab in a plausible keg directory
keg_dir = HOMEBREW_CELLAR/"installed"/"1.0"
keg_dir.mkpath
touch keg_dir/Tab::FILENAME
touch keg_dir/AbstractTab::FILENAME
expect { brew "deps", "baz", "--include-test", "--missing", "--skip-recommended" }
.to be_a_success

View File

@ -33,7 +33,7 @@ RSpec.describe Homebrew::Cmd::Untap do
keg_path = HOMEBREW_CELLAR/name/"1.2.3"
keg_path.mkpath
tab_path = keg_path/Tab::FILENAME
tab_path = keg_path/AbstractTab::FILENAME
tab_path.write <<~JSON
{
"source": {

View File

@ -36,7 +36,7 @@ RSpec.describe Homebrew::Cmd::Uses do
%w[foo installed].each do |formula_name|
keg_dir = HOMEBREW_CELLAR/formula_name/"1.0"
keg_dir.mkpath
touch keg_dir/Tab::FILENAME
touch keg_dir/AbstractTab::FILENAME
end
expect { brew "uses", "foo", "--eval-all", "--include-optional", "--missing", "--recursive" }

View File

@ -264,7 +264,7 @@ RSpec.describe Formula do
prefix = HOMEBREW_CELLAR/f.name/"0.1"
prefix.mkpath
FileUtils.touch prefix/Tab::FILENAME
FileUtils.touch prefix/AbstractTab::FILENAME
expect(f).to have_any_version_installed
end
@ -279,7 +279,7 @@ RSpec.describe Formula do
oldname_prefix.mkpath
oldname_tab = Tab.empty
oldname_tab.tabfile = oldname_prefix/Tab::FILENAME
oldname_tab.tabfile = oldname_prefix/AbstractTab::FILENAME
oldname_tab.write
expect(f).not_to need_migration
@ -346,7 +346,7 @@ RSpec.describe Formula do
head_prefix.mkpath
tab = Tab.empty
tab.tabfile = head_prefix/Tab::FILENAME
tab.tabfile = head_prefix/AbstractTab::FILENAME
tab.source["versions"] = { "stable" => "1.0" }
tab.write
@ -378,7 +378,7 @@ RSpec.describe Formula do
prefix.mkpath
tab = Tab.empty
tab.tabfile = prefix/Tab::FILENAME
tab.tabfile = prefix/AbstractTab::FILENAME
tab.source_modified_time = stamp
tab.write
end
@ -1106,7 +1106,7 @@ RSpec.describe Formula do
prefix = f.prefix(version)
prefix.mkpath
tab = Tab.empty
tab.tabfile = prefix/Tab::FILENAME
tab.tabfile = prefix/AbstractTab::FILENAME
tab.source_modified_time = 1
tab.write
end
@ -1340,7 +1340,7 @@ RSpec.describe Formula do
def setup_tab_for_prefix(prefix, options = {})
prefix.mkpath
tab = Tab.empty
tab.tabfile = prefix/Tab::FILENAME
tab.tabfile = prefix/AbstractTab::FILENAME
tab.source["path"] = options[:path].to_s if options[:path]
tab.source["tap"] = options[:tap] if options[:tap]
tab.source["versions"] = options[:versions] if options[:versions]

View File

@ -61,7 +61,7 @@ RSpec.describe InstalledDependents do
def tab_dependencies(keg, deps, homebrew_version: "1.1.6")
alter_tab(keg) do |tab|
tab.homebrew_version = homebrew_version
tab.tabfile = keg/Tab::FILENAME
tab.tabfile = keg/AbstractTab::FILENAME
tab.runtime_dependencies = deps
end
end

View File

@ -0,0 +1,32 @@
cask "many-artifacts" do
version "1.2.3"
sha256 "8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b"
url "file://#{TEST_FIXTURE_DIR}/cask/ManyArtifacts.zip"
homepage "https://brew.sh/many-artifacts"
app "ManyArtifacts/ManyArtifacts.app"
pkg "ManyArtifacts/ManyArtifacts.pkg"
preflight do
# do nothing
end
postflight do
# do nothing
end
uninstall_preflight do
# do nothing
end
uninstall_postflight do
# do nothing
end
uninstall trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"],
rmdir: "#{TEST_TMPDIR}/empty_directory_path"
zap trash: "~/Library/Logs/ManyArtifacts.log",
rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"]
end

View File

@ -0,0 +1,15 @@
cask "with-depends-on-everything" do
version "1.2.3"
sha256 "67cdb8a02803ef37fdbf7e0be205863172e41a561ca446cd84f0d7ab35a99d94"
url "file://#{TEST_FIXTURE_DIR}/cask/caffeine.zip"
homepage "https://brew.sh/with-depends-on-everything"
depends_on arch: [:intel, :arm64]
depends_on cask: "local-caffeine"
depends_on cask: "with-depends-on-cask"
depends_on formula: "unar"
depends_on macos: ">= :el_capitan"
app "Caffeine.app"
end

View File

@ -0,0 +1,53 @@
{
"homebrew_version": "4.3.7",
"loaded_from_api": false,
"uninstall_flight_blocks": true,
"installed_as_dependency": false,
"installed_on_request": true,
"time": 1719289256,
"runtime_dependencies": {
"cask": [
{
"full_name": "bar",
"version": "2.0",
"declared_directly": true
}
],
"formula": [
{
"full_name": "baz",
"version": "3.0",
"revision": 0,
"pkg_version": "3.0",
"declared_directly": true
}
],
"macos": {
">=": [
"12"
]
}
},
"source": {
"path": "/opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/f/foo.rb",
"tap": "homebrew/cask",
"tap_git_head": "8b79aa759500f0ffdf65a23e12950cbe3bf8fe17",
"version": "1.2.3"
},
"arch": "arm64",
"uninstall_artifacts": [
{
"app": [
"Foo.app"
]
}
],
"built_on": {
"os": "Macintosh",
"os_version": "macOS 14",
"cpu_family": "arm_firestorm_icestorm",
"xcode": "15.4",
"clt": "15.3.0.0.1.1708646388",
"preferred_perl": "5.34"
}
}

View File

@ -10,6 +10,9 @@
],
"built_as_bottle": false,
"poured_from_bottle": true,
"loaded_from_api": false,
"installed_as_dependency": false,
"installed_on_request": true,
"changed_files": [
"INSTALL_RECEIPT.json",
"bin/foo"
@ -27,12 +30,12 @@
}
],
"source": {
"path": "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb",
"tap": "homebrew/core",
"spec": "stable",
"versions": {
"stable": "2.14",
"head": "HEAD-0000000"
}
"path": "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb",
"tap": "homebrew/core",
"spec": "stable",
"versions": {
"stable": "2.14",
"head": "HEAD-0000000"
}
}
}

View File

@ -191,7 +191,7 @@ RSpec.shared_context "integration test" do # rubocop:disable RSpec/ContextWordin
keg.mkpath
tab = Tab.for_name(name)
tab.tabfile ||= keg/Tab::FILENAME
tab.tabfile ||= keg/AbstractTab::FILENAME
tab_attributes.each do |key, value|
tab.instance_variable_set(:"@#{key}", value)
end

View File

@ -18,20 +18,40 @@ RSpec.describe Tab do
end
end
matcher :be_installed_as_dependency do
match do |actual|
actual.installed_as_dependency == true
end
end
matcher :be_installed_on_request do
match do |actual|
actual.installed_on_request == true
end
end
matcher :be_loaded_from_api do
match do |actual|
actual.loaded_from_api == true
end
end
subject(:tab) do
described_class.new(
"homebrew_version" => HOMEBREW_VERSION,
"used_options" => used_options.as_flags,
"unused_options" => unused_options.as_flags,
"built_as_bottle" => false,
"poured_from_bottle" => true,
"changed_files" => [],
"time" => time,
"source_modified_time" => 0,
"compiler" => "clang",
"stdlib" => "libcxx",
"runtime_dependencies" => [],
"source" => {
"homebrew_version" => HOMEBREW_VERSION,
"used_options" => used_options.as_flags,
"unused_options" => unused_options.as_flags,
"built_as_bottle" => false,
"poured_from_bottle" => true,
"installed_as_dependency" => false,
"installed_on_request" => true,
"changed_files" => [],
"time" => time,
"source_modified_time" => 0,
"compiler" => "clang",
"stdlib" => "libcxx",
"runtime_dependencies" => [],
"source" => {
"tap" => CoreTap.instance.to_s,
"path" => CoreTap.instance.path.to_s,
"spec" => "stable",
@ -40,8 +60,8 @@ RSpec.describe Tab do
"head" => "HEAD-1111111",
},
},
"arch" => Hardware::CPU.arch,
"built_on" => DevelopmentTools.build_system_info,
"arch" => Hardware::CPU.arch,
"built_on" => DevelopmentTools.build_system_info,
)
end
@ -65,6 +85,9 @@ RSpec.describe Tab do
expect(tab.changed_files).to be_nil
expect(tab).not_to be_built_as_bottle
expect(tab).not_to be_poured_from_bottle
expect(tab).not_to be_installed_as_dependency
expect(tab).not_to be_installed_on_request
expect(tab).not_to be_loaded_from_api
expect(tab).to be_stable
expect(tab).not_to be_head
expect(tab.tap).to be_nil
@ -132,18 +155,69 @@ RSpec.describe Tab do
expect(tab.runtime_dependencies).not_to be_nil
end
specify "::runtime_deps_hash" do
runtime_deps = [Dependency.new("foo")]
foo = formula("foo") { url "foo-1.0" }
stub_formula_loader foo
runtime_deps_hash = described_class.runtime_deps_hash(foo, runtime_deps)
tab = described_class.new
tab.homebrew_version = "1.1.6"
tab.runtime_dependencies = runtime_deps_hash
expect(tab.runtime_dependencies).to eql(
[{ "full_name" => "foo", "version" => "1.0", "revision" => 0, "pkg_version" => "1.0",
"declared_directly" => false }],
)
describe "::runtime_deps_hash" do
it "handles older Homebrew versions correctly" do
runtime_deps = [Dependency.new("foo")]
foo = formula("foo") { url "foo-1.0" }
stub_formula_loader foo
runtime_deps_hash = described_class.runtime_deps_hash(foo, runtime_deps)
tab = described_class.new
tab.homebrew_version = "1.1.6"
tab.runtime_dependencies = runtime_deps_hash
expect(tab.runtime_dependencies).to eql(
[{ "full_name" => "foo", "version" => "1.0", "revision" => 0, "pkg_version" => "1.0",
"declared_directly" => false }],
)
end
it "include declared dependencies" do
foo = formula("foo") { url "foo-1.0" }
stub_formula_loader foo
runtime_deps = [Dependency.new("foo")]
formula = instance_double(Formula, deps: runtime_deps)
expected_output = [
{
"full_name" => "foo",
"version" => "1.0",
"revision" => 0,
"pkg_version" => "1.0",
"declared_directly" => true,
},
]
expect(described_class.runtime_deps_hash(formula, runtime_deps)).to eq(expected_output)
end
it "includes recursive dependencies" do
foo = formula("foo") { url "foo-1.0" }
bar = formula("bar") { url "bar-2.0" }
stub_formula_loader foo
stub_formula_loader bar
# Simulating dependencies formula => foo => bar
formula_declared_deps = [Dependency.new("foo")]
formula_recursive_deps = [Dependency.new("foo"), Dependency.new("bar")]
formula = instance_double(Formula, deps: formula_declared_deps)
expected_output = [
{
"full_name" => "foo",
"version" => "1.0",
"revision" => 0,
"pkg_version" => "1.0",
"declared_directly" => true,
},
{
"full_name" => "bar",
"version" => "2.0",
"revision" => 0,
"pkg_version" => "2.0",
"declared_directly" => false,
},
]
expect(described_class.runtime_deps_hash(formula, formula_recursive_deps)).to eq(expected_output)
end
end
specify "#cxxstdlib" do
@ -156,10 +230,13 @@ RSpec.describe Tab do
expect(tab.time).to eq(time)
expect(tab).not_to be_built_as_bottle
expect(tab).to be_poured_from_bottle
expect(tab).not_to be_installed_as_dependency
expect(tab).to be_installed_on_request
expect(tab).not_to be_loaded_from_api
end
describe "::from_file" do
it "parses a Tab from a file" do
it "parses a formula Tab from a file" do
path = Pathname.new("#{TEST_FIXTURE_DIR}/receipt.json")
tab = described_class.from_file(path)
source_path = "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb"
@ -171,6 +248,9 @@ RSpec.describe Tab do
expect(tab.changed_files).to eq(changed_files)
expect(tab).not_to be_built_as_bottle
expect(tab).to be_poured_from_bottle
expect(tab).not_to be_installed_as_dependency
expect(tab).to be_installed_on_request
expect(tab).not_to be_loaded_from_api
expect(tab).to be_stable
expect(tab).not_to be_head
expect(tab.tap.name).to eq("homebrew/core")
@ -186,7 +266,7 @@ RSpec.describe Tab do
end
describe "::from_file_content" do
it "parses a Tab from a file" do
it "parses a formula Tab from a file" do
path = Pathname.new("#{TEST_FIXTURE_DIR}/receipt.json")
tab = described_class.from_file_content(path.read, path)
source_path = "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb"
@ -198,6 +278,9 @@ RSpec.describe Tab do
expect(tab.changed_files).to eq(changed_files)
expect(tab).not_to be_built_as_bottle
expect(tab).to be_poured_from_bottle
expect(tab).not_to be_installed_as_dependency
expect(tab).to be_installed_on_request
expect(tab).not_to be_loaded_from_api
expect(tab).to be_stable
expect(tab).not_to be_head
expect(tab.tap.name).to eq("homebrew/core")
@ -211,7 +294,7 @@ RSpec.describe Tab do
expect(tab.source["path"]).to eq(source_path)
end
it "can parse an old Tab file" do
it "can parse an old formula Tab file" do
path = Pathname.new("#{TEST_FIXTURE_DIR}/receipt_old.json")
tab = described_class.from_file_content(path.read, path)
@ -219,6 +302,9 @@ RSpec.describe Tab do
expect(tab.unused_options.sort).to eq(unused_options.sort)
expect(tab).not_to be_built_as_bottle
expect(tab).to be_poured_from_bottle
expect(tab).not_to be_installed_as_dependency
expect(tab).not_to be_installed_on_request
expect(tab).not_to be_loaded_from_api
expect(tab).to be_stable
expect(tab).not_to be_head
expect(tab.tap.name).to eq("homebrew/core")
@ -238,7 +324,7 @@ RSpec.describe Tab do
end
describe "::create" do
it "creates a Tab" do
it "creates a formula Tab" do
# < 1.1.7 runtime dependencies were wrong so are ignored
stub_const("HOMEBREW_VERSION", "1.1.7")
@ -277,7 +363,7 @@ RSpec.describe Tab do
expect(tab.source["path"]).to eq(f.path.to_s)
end
it "can create a Tab from an alias" do
it "can create a formula Tab from an alias" do
alias_path = CoreTap.instance.alias_dir/"bar"
f = formula(alias_path:) { url "foo-1.0" }
compiler = DevelopmentTools.default_compiler
@ -395,6 +481,62 @@ RSpec.describe Tab do
expect(json_tab.built_on["os"]).to eq(tab.built_on["os"])
end
describe "#to_s" do
let(:time_string) { Time.at(1_720_189_863).strftime("%Y-%m-%d at %H:%M:%S") }
it "returns install information for the Tab" do
tab = described_class.new(
poured_from_bottle: true,
loaded_from_api: true,
time: 1_720_189_863,
used_options: %w[--with-foo --without-bar],
)
output = "Poured from bottle using the formulae.brew.sh API on #{time_string} " \
"with: --with-foo --without-bar"
expect(tab.to_s).to eq(output)
end
it "includes 'Poured from bottle' if the formula was installed from a bottle" do
tab = described_class.new(poured_from_bottle: true)
expect(tab.to_s).to include("Poured from bottle")
end
it "includes 'Built from source' if the formula was not installed from a bottle" do
tab = described_class.new(poured_from_bottle: false)
expect(tab.to_s).to include("Built from source")
end
it "includes 'using the formulae.brew.sh API' if the formula was installed from the API" do
tab = described_class.new(loaded_from_api: true)
expect(tab.to_s).to include("using the formulae.brew.sh API")
end
it "does not include 'using the formulae.brew.sh API' if the formula was not installed from the API" do
tab = described_class.new(loaded_from_api: false)
expect(tab.to_s).not_to include("using the formulae.brew.sh API")
end
it "includes the time value if specified" do
tab = described_class.new(time: 1_720_189_863)
expect(tab.to_s).to include("on #{time_string}")
end
it "does not include the time value if not specified" do
tab = described_class.new(time: nil)
expect(tab.to_s).not_to match(/on %d+-%d+-%d+ at %d+:%d+:%d+/)
end
it "includes options if specified" do
tab = described_class.new(used_options: %w[--with-foo --without-bar])
expect(tab.to_s).to include("with: --with-foo --without-bar")
end
it "not to include options if not specified" do
tab = described_class.new(used_options: [])
expect(tab.to_s).not_to include("with: ")
end
end
specify "::remap_deprecated_options" do
deprecated_options = [DeprecatedOption.new("with-foo", "with-foo-new")]
remapped_options = described_class.remap_deprecated_options(deprecated_options, tab.used_options)

View File

@ -33,7 +33,7 @@ RSpec.describe Homebrew::Uninstall do
tab = Tab.empty
tab.homebrew_version = "1.1.6"
tab.tabfile = dependent_formula.latest_installed_prefix/Tab::FILENAME
tab.tabfile = dependent_formula.latest_installed_prefix/AbstractTab::FILENAME
tab.runtime_dependencies = [
{ "full_name" => "dependency", "version" => "1" },
]

View File

@ -106,7 +106,7 @@ module Utils
def load_tab(formula)
keg = Keg.new(formula.prefix)
tabfile = keg/Tab::FILENAME
tabfile = keg/AbstractTab::FILENAME
bottle_json_path = formula.local_bottle_path&.sub(/\.(\d+\.)?tar\.gz$/, ".json")
if (tab_attributes = formula.bottle_tab_attributes.presence)