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/quarantine"
require "cask/staged"
require "cask/tab"
require "cask/url"
require "cask/utils"

View File

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

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,12 @@ module Cask
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
end
def uninstall_flight_blocks?
artifacts.any? do |artifact|
artifact.is_a?(Artifact::AbstractFlightBlock) && artifact.uninstall?
end
end
sig { returns(T.nilable(Time)) }
def install_time
# <caskroom_path>/.metadata/<version>/<timestamp>/Casks/<token>.{rb,json} -> <timestamp>
@ -209,6 +216,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 +476,25 @@ module Cask
hash
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
sig { returns(T.nilable(Homebrew::BundleVersion)) }
@ -482,19 +512,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

@ -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.present? && tabfile.exist?
@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,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.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

@ -636,7 +636,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,41 @@ 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)
attributes = {
def self.generic_attributes(formula_or_cask)
{
"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,
},
},
"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,30 +62,122 @@ 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")
new(attributes)
end
if attributes["source"]["tap"] == "mxcl/master" ||
attributes["source"]["tap"] == "Homebrew/homebrew"
attributes["source"]["tap"] = "homebrew/core"
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,
}
new(attributes)
end
if attributes["source"]["spec"].nil?
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)))
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"] = {
if tab.source["versions"].nil?
tab.source["versions"] = {
"stable" => nil,
"head" => nil,
"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.
["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},
@ -213,37 +268,24 @@ 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" => {
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"
tab.source["versions"] = {
"stable" => nil,
"head" => nil,
"version_scheme" => 0,
},
},
"built_on" => DevelopmentTools.generic_build_system_info,
}
new(attributes)
tab
end
def self.runtime_deps_hash(formula, deps)
@ -259,10 +301,6 @@ class Tab
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 +345,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 +365,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 +437,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

@ -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)