Add cask install receipts

This commit is contained in:
Rylan Polster 2024-06-22 13:31:50 -04:00
parent 5e7765ca60
commit acd60181c2
No known key found for this signature in database
GPG Key ID: 46A744940CFF4D64
10 changed files with 317 additions and 135 deletions

View File

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

View File

@ -34,6 +34,12 @@ module Cask
directives.keys.map(&:to_s).join(", ") directives.keys.map(&:to_s).join(", ")
end end
def uninstall?
directives.keys.any? do |key|
key.to_s.start_with?("uninstall_")
end
end
private private
def class_for_dsl_key(dsl_key) def class_for_dsl_key(dsl_key)

View File

@ -7,6 +7,7 @@ require "cask/cask_loader"
require "cask/config" require "cask/config"
require "cask/dsl" require "cask/dsl"
require "cask/metadata" require "cask/metadata"
require "cask/tab"
require "utils/bottles" require "utils/bottles"
require "extend/api_hashable" require "extend/api_hashable"
@ -158,6 +159,12 @@ module Cask
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock) languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
end end
def uninstall_flight_blocks?
artifacts.any? do |artifact|
artifact.is_a?(Artifact::AbstractFlightBlock) && artifact.uninstall?
end
end
sig { returns(T.nilable(Time)) } sig { returns(T.nilable(Time)) }
def install_time def install_time
# <caskroom_path>/.metadata/<version>/<timestamp>/Casks/<token>.{rb,json} -> <timestamp> # <caskroom_path>/.metadata/<version>/<timestamp>/Casks/<token>.{rb,json} -> <timestamp>
@ -209,6 +216,10 @@ module Cask
bundle_version&.version bundle_version&.version
end end
def tab
Tab.for_cask(self)
end
def config_path def config_path
metadata_main_container_path/"config.json" metadata_main_container_path/"config.json"
end end
@ -465,6 +476,25 @@ module Cask
hash hash
end end
def artifacts_list(compact: false, uninstall_only: false)
artifacts.filter_map do |artifact|
case artifact
when Artifact::AbstractFlightBlock
next if uninstall_only && !artifact.uninstall?
# 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
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 private
sig { returns(T.nilable(Homebrew::BundleVersion)) } sig { returns(T.nilable(Homebrew::BundleVersion)) }
@ -482,19 +512,6 @@ module Cask
hash hash
end 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 def url_specs
url&.specs.dup.tap do |url_specs| url&.specs.dup.tap do |url_specs|
case url_specs&.dig(:user_agent) case url_specs&.dig(:user_agent)

View File

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

View File

@ -0,0 +1,120 @@
# 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)
attributes = generic_attributes(cask).merge({
"tabfile" => cask.metadata_main_container_path/FILENAME,
"uninstall_flight_blocks" => cask.uninstall_flight_blocks?,
"runtime_dependencies" => Tab.runtime_deps_hash(cask, cask.depends_on),
"source" => {
"path" => cask.sourcefile_path.to_s,
"tap" => cask.tap&.name,
"tap_git_head" => nil, # Filled in later if possible
"version" => cask.version.to_s,
},
"uninstall_artifacts" => cask.artifacts_list(uninstall_only: true),
})
# We can only get `tap_git_head` if the tap is installed locally
attributes["source"]["tap_git_head"] = cask.tap.git_head if cask.tap&.installed?
new(attributes)
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" => nil,
"version" => cask.version.to_s,
}
tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true)
tab.source["tap_git_head"] = cask.tap.git_head if cask.tap&.installed?
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, depends_on)
mappable_types = [:cask, :formula]
depends_on.to_h do |type, deps|
next [type, deps] unless mappable_types.include? type
deps = deps.map do |dep|
if type == :cask
c = CaskLoader.load(dep)
{
"full_name" => c.full_name,
"version" => c.version.to_s,
"declared_directly" => cask.depends_on.cask.include?(dep),
}
elsif type == :formula
f = Formulary.factory(dep, warn: false)
{
"full_name" => f.full_name,
"version" => f.version.to_s,
"revision" => f.revision,
"pkg_version" => f.pkg_version.to_s,
"declared_directly" => cask.depends_on.formula.include?(dep),
}
else
dep
end
end
[type, deps]
end
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

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

View File

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

View File

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

View File

@ -8,78 +8,41 @@ require "development_tools"
require "extend/cachable" require "extend/cachable"
# Rather than calling `new` directly, use one of the class methods like {Tab.create}. # Rather than calling `new` directly, use one of the class methods like {Tab.create}.
class Tab class AbstractTab
extend Cachable extend Cachable
FILENAME = "INSTALL_RECEIPT.json" 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 # @api internal
attr_accessor :installed_as_dependency 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 # @api internal
attr_accessor :installed_on_request 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 # @api internal
attr_accessor :poured_from_bottle attr_accessor :runtime_dependencies
attr_accessor :homebrew_version, :tabfile, :built_as_bottle, def self.generic_attributes(formula_or_cask)
: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)
attributes = {
"homebrew_version" => HOMEBREW_VERSION, "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_as_dependency" => false,
"installed_on_request" => false, "installed_on_request" => false,
"poured_from_bottle" => false, "loaded_from_api" => formula_or_cask.loaded_from_api?,
"loaded_from_api" => false,
"time" => Time.now.to_i, "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, "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, "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 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. # NOTE: Results are cached.
def self.from_file(path) def self.from_file(path)
@ -99,30 +62,122 @@ class Tab
raise e, "Cannot parse #{path}: #{e}", e.backtrace raise e, "Cannot parse #{path}: #{e}", e.backtrace
end end
attributes["tabfile"] = path attributes["tabfile"] = path
attributes["source_modified_time"] ||= 0
attributes["source"] ||= {}
tapped_from = attributes["tapped_from"] new(attributes)
if !tapped_from.nil? && tapped_from != "path or URL" end
attributes["source"]["tap"] = attributes.delete("tapped_from")
end
if attributes["source"]["tap"] == "mxcl/master" || def self.empty
attributes["source"]["tap"] == "Homebrew/homebrew" attributes = {
attributes["source"]["tap"] = "homebrew/core" "homebrew_version" => HOMEBREW_VERSION,
end "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 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
# 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 = generic_attributes(formula).merge({
"used_options" => build.used_options.as_flags,
"unused_options" => build.unused_options.as_flags,
"tabfile" => formula.prefix/FILENAME,
"built_as_bottle" => build.bottle?,
"poured_from_bottle" => false,
"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),
"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,
},
},
})
# 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
# Like {from_file}, but bypass the cache.
def self.from_file_content(content, path)
tab = super
tab.source_modified_time ||= 0
tab.source ||= {}
tapped_from = tab.instance_variable_get(:@tapped_from)
tab.tap = tapped_from if !tapped_from.nil? && 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))) version = PkgVersion.parse(File.basename(File.dirname(path)))
attributes["source"]["spec"] = if version.head? tab.source["spec"] = if version.head?
"head" "head"
else else
"stable" "stable"
end end
end end
if attributes["source"]["versions"].nil? if tab.source["versions"].nil?
attributes["source"]["versions"] = { tab.source["versions"] = {
"stable" => nil, "stable" => nil,
"head" => nil, "head" => nil,
"version_scheme" => 0, "version_scheme" => 0,
@ -131,10 +186,10 @@ class Tab
# Tabs created with Homebrew 1.5.13 through 4.0.17 inclusive created empty string versions in some cases. # Tabs created with Homebrew 1.5.13 through 4.0.17 inclusive created empty string versions in some cases.
["stable", "head"].each do |spec| ["stable", "head"].each do |spec|
attributes["source"]["versions"][spec] = attributes["source"]["versions"][spec].presence tab.source["versions"][spec] = tab.source["versions"][spec].presence
end end
new(attributes) tab
end end
# Get the {Tab} for the given {Keg}, # Get the {Tab} for the given {Keg},
@ -213,37 +268,24 @@ class Tab
end end
def self.empty def self.empty
attributes = { tab = super
"homebrew_version" => HOMEBREW_VERSION,
"used_options" => [], tab.used_options = []
"unused_options" => [], tab.unused_options = []
"built_as_bottle" => false, tab.built_as_bottle = false
"installed_as_dependency" => false, tab.poured_from_bottle = false
"installed_on_request" => false, tab.source_modified_time = 0
"poured_from_bottle" => false, tab.stdlib = nil
"loaded_from_api" => false, tab.compiler = DevelopmentTools.default_compiler
"time" => nil, tab.aliases = []
"source_modified_time" => 0, tab.source["spec"] = "stable"
"stdlib" => nil, tab.source["versions"] = {
"compiler" => DevelopmentTools.default_compiler, "stable" => nil,
"aliases" => [], "head" => nil,
"runtime_dependencies" => nil, "version_scheme" => 0,
"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) tab
end end
def self.runtime_deps_hash(formula, deps) def self.runtime_deps_hash(formula, deps)
@ -259,10 +301,6 @@ class Tab
end end
end end
def initialize(attributes = {})
attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
end
def any_args_or_options? def any_args_or_options?
!used_options.empty? || !unused_options.empty? !used_options.empty? || !unused_options.empty?
end end
@ -307,12 +345,6 @@ class Tab
@compiler || DevelopmentTools.default_compiler @compiler || DevelopmentTools.default_compiler
end end
def parsed_homebrew_version
return Version::NULL if homebrew_version.nil?
Version.new(homebrew_version)
end
def runtime_dependencies def runtime_dependencies
# Homebrew versions prior to 1.1.6 generated incorrect runtime dependency # Homebrew versions prior to 1.1.6 generated incorrect runtime dependency
# lists. # lists.
@ -333,17 +365,6 @@ class Tab
built_as_bottle built_as_bottle
end 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 def spec
source["spec"].to_sym source["spec"].to_sym
end end
@ -416,8 +437,7 @@ class Tab
# will no longer be valid. # will no longer be valid.
Formula.clear_cache unless tabfile.exist? Formula.clear_cache unless tabfile.exist?
self.class.cache[tabfile] = self super
tabfile.atomic_write(to_json)
end end
sig { returns(String) } sig { returns(String) }

View File

@ -106,7 +106,7 @@ module Utils
def load_tab(formula) def load_tab(formula)
keg = Keg.new(formula.prefix) 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") bottle_json_path = formula.local_bottle_path&.sub(/\.(\d+\.)?tar\.gz$/, ".json")
if (tab_attributes = formula.bottle_tab_attributes.presence) if (tab_attributes = formula.bottle_tab_attributes.presence)