Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cask install receipts #17554

Merged
merged 11 commits into from
Jul 13, 2024
1 change: 1 addition & 0 deletions Library/Homebrew/cask.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@
require "cask/pkg"
require "cask/quarantine"
require "cask/staged"
require "cask/tab"
require "cask/url"
require "cask/utils"
50 changes: 37 additions & 13 deletions Library/Homebrew/cask/cask.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
require "cask/config"
require "cask/dsl"
require "cask/metadata"
require "cask/tab"
require "utils/bottles"
require "extend/api_hashable"

Expand Down Expand Up @@ -158,6 +159,17 @@ def caskfile_only?
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>
Expand Down Expand Up @@ -209,6 +221,10 @@ def bundle_long_version
bundle_version&.version
end

def tab
Tab.for_cask(self)
end

def config_path
metadata_main_container_path/"config.json"
end
Expand Down Expand Up @@ -465,6 +481,27 @@ def to_hash_with_variations(hash_method: :to_h)
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)) }
Expand All @@ -482,19 +519,6 @@ def api_to_local_hash(hash)
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)
Expand Down
11 changes: 8 additions & 3 deletions Library/Homebrew/cask/info.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def self.get_info(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)
Expand All @@ -37,7 +37,7 @@ def self.title_info(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?
Expand All @@ -46,7 +46,12 @@ def self.installation_info(cask)
Formatter.error("does not exist")
end

"Installed\n#{versioned_staged_path} (#{path_details})\n"
tab = Tab.for_cask(cask)
Rylan12 marked this conversation as resolved.
Show resolved Hide resolved

info = ["Installed"]
info << "#{versioned_staged_path} (#{path_details})"
info << " #{tab}" if tab.tabfile&.exist?
info.join("\n")
end

def self.name_info(cask)
Expand Down
21 changes: 18 additions & 3 deletions Library/Homebrew/cask/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "cask/download"
require "cask/migrator"
require "cask/quarantine"
require "cask/tab"

require "cgi"

Expand All @@ -21,8 +22,8 @@ class Installer
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
Expand All @@ -35,13 +36,14 @@ def initialize(cask, command: SystemCommand, force: false, adopt: false,
@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)
Expand Down Expand Up @@ -112,6 +114,11 @@ def install

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)
Expand Down Expand Up @@ -356,6 +363,7 @@ def satisfy_cask_and_formula_dependencies
binaries: binaries?,
verbose: verbose?,
installed_as_dependency: true,
installed_on_request: false,
force: false,
).install
else
Expand Down Expand Up @@ -408,13 +416,20 @@ def uninstall(successor: nil)
oh1 "Uninstalling Cask #{Formatter.identifier(@cask)}"
uninstall_artifacts(clear: true, successor:)
if !reinstall? && !upgrade?
remove_tabfile
remove_download_sha
remove_config_file
end
purge_versioned_files
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
Expand Down
108 changes: 108 additions & 0 deletions Library/Homebrew/cask/tab.rb
Original file line number Diff line number Diff line change
@@ -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"]
Rylan12 marked this conversation as resolved.
Show resolved Hide resolved
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
48 changes: 30 additions & 18 deletions Library/Homebrew/cmd/tab.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
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`.
Expand All @@ -19,13 +19,18 @@
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 }
Expand All @@ -37,38 +42,45 @@
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."

Check warning on line 51 in Library/Homebrew/cmd/tab.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cmd/tab.rb#L51

Added line #L51 was not covered by tests
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
Expand Down
Loading