From acd60181c2ff35eae4b92f2dcb5f3ba919d0e5fa Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Sat, 22 Jun 2024 13:31:50 -0400 Subject: [PATCH 01/11] Add cask install receipts --- Library/Homebrew/cask.rb | 1 + .../cask/artifact/abstract_flight_block.rb | 6 + Library/Homebrew/cask/cask.rb | 43 ++- Library/Homebrew/cask/installer.rb | 21 +- Library/Homebrew/cask/tab.rb | 120 +++++++++ Library/Homebrew/dev-cmd/bottle.rb | 2 +- Library/Homebrew/formula.rb | 2 +- Library/Homebrew/sorbet/rbi/parlour.rbi | 3 + Library/Homebrew/tab.rb | 250 ++++++++++-------- Library/Homebrew/utils/bottles.rb | 2 +- 10 files changed, 316 insertions(+), 134 deletions(-) create mode 100644 Library/Homebrew/cask/tab.rb diff --git a/Library/Homebrew/cask.rb b/Library/Homebrew/cask.rb index d2246dcf9e516..9bb3b7444a1b8 100644 --- a/Library/Homebrew/cask.rb +++ b/Library/Homebrew/cask.rb @@ -20,5 +20,6 @@ require "cask/pkg" require "cask/quarantine" require "cask/staged" +require "cask/tab" require "cask/url" require "cask/utils" diff --git a/Library/Homebrew/cask/artifact/abstract_flight_block.rb b/Library/Homebrew/cask/artifact/abstract_flight_block.rb index 5c8c4daec7b34..1a4cedc8df8ab 100644 --- a/Library/Homebrew/cask/artifact/abstract_flight_block.rb +++ b/Library/Homebrew/cask/artifact/abstract_flight_block.rb @@ -34,6 +34,12 @@ def summarize 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) diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index aa1a62516aaea..00ff239de7a9e 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -7,6 +7,7 @@ require "cask/config" require "cask/dsl" require "cask/metadata" +require "cask/tab" require "utils/bottles" require "extend/api_hashable" @@ -158,6 +159,12 @@ def caskfile_only? 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 # /.metadata///Casks/.{rb,json} -> @@ -209,6 +216,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 @@ -465,6 +476,25 @@ 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 + 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 @@ 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) diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index 517070f5ba919..b16ac8709c562 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -10,6 +10,7 @@ require "cask/download" require "cask/migrator" require "cask/quarantine" +require "cask/tab" require "cgi" @@ -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 @@ -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) @@ -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) @@ -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 @@ -408,6 +416,7 @@ 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 @@ -415,6 +424,12 @@ def uninstall(successor: nil) 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 diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb new file mode 100644 index 0000000000000..837947a5dad61 --- /dev/null +++ b/Library/Homebrew/cask/tab.rb @@ -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 diff --git a/Library/Homebrew/dev-cmd/bottle.rb b/Library/Homebrew/dev-cmd/bottle.rb index 32c04a66edfa5..338a6646b9b52 100644 --- a/Library/Homebrew/dev-cmd/bottle.rb +++ b/Library/Homebrew/dev-cmd/bottle.rb @@ -501,7 +501,7 @@ def bottle_formula(formula) 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 diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 6bf5a1203f683..4eb98cfa9e91e 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -636,7 +636,7 @@ def latest_version_installed? # 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}. diff --git a/Library/Homebrew/sorbet/rbi/parlour.rbi b/Library/Homebrew/sorbet/rbi/parlour.rbi index e0df736599522..ec4254527fc5e 100644 --- a/Library/Homebrew/sorbet/rbi/parlour.rbi +++ b/Library/Homebrew/sorbet/rbi/parlour.rbi @@ -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 diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 163e9182ea6ff..7092a3bf6fd07 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -8,78 +8,41 @@ 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. - # - # @api internal - attr_accessor :poured_from_bottle - - 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 + attr_accessor :homebrew_version, :tabfile, :loaded_from_api, :time, :arch, :source, :built_on - # Returns the formula's runtime dependencies. + # Returns the formula or cask runtime dependencies. # # @api internal - attr_writer :runtime_dependencies + attr_accessor :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 @@ def self.from_file_content(content, path) 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, + } + + 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 - if attributes["source"]["spec"].nil? + # 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 @@ def self.from_file_content(content, path) # 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 @@ def self.for_formula(formula) 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 + + 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, } - new(attributes) + tab end def self.runtime_deps_hash(formula, deps) @@ -259,10 +301,6 @@ def self.runtime_deps_hash(formula, deps) 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 @@ def compiler @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 @@ def bottle? 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 @@ def write # 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) } diff --git a/Library/Homebrew/utils/bottles.rb b/Library/Homebrew/utils/bottles.rb index 34ce838f85ace..aaaa57fb1939d 100644 --- a/Library/Homebrew/utils/bottles.rb +++ b/Library/Homebrew/utils/bottles.rb @@ -106,7 +106,7 @@ def path_resolved_basename(root_url, name, checksum, filename) 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) From 673b171b100a8cd8ed03ab6b8c5d22a0324d5bc3 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 25 Jun 2024 00:19:22 -0400 Subject: [PATCH 02/11] Update commands to use cask install receipts --- Library/Homebrew/cask/info.rb | 11 +++++--- Library/Homebrew/cmd/tab.rb | 48 ++++++++++++++++++++++------------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/Library/Homebrew/cask/info.rb b/Library/Homebrew/cask/info.rb index 18ccf66894881..ea38b24222df0 100644 --- a/Library/Homebrew/cask/info.rb +++ b/Library/Homebrew/cask/info.rb @@ -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) @@ -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? @@ -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) + + info = ["Installed"] + info << "#{versioned_staged_path} (#{path_details})" + info << " #{tab}" if tab.tabfile&.exist? + info.join("\n") end def self.name_info(cask) diff --git a/Library/Homebrew/cmd/tab.rb b/Library/Homebrew/cmd/tab.rb index 70b2aa261c8e1..48c891c74ac5e 100644 --- a/Library/Homebrew/cmd/tab.rb +++ b/Library/Homebrew/cmd/tab.rb @@ -10,7 +10,7 @@ module Cmd 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 @@ class TabCmd < AbstractCommand EOS switch "--installed-on-request", - description: "Mark as installed on request." + description: "Mark or as installed on request." switch "--no-installed-on-request", - description: "Mark as not installed on request." + description: "Mark or 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 @@ def run 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 From 46cb7f28472114c99a7207c890b14ec696d3e8d5 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 25 Jun 2024 00:15:41 -0400 Subject: [PATCH 03/11] Update tests --- Library/Homebrew/test/cask/cask_spec.rb | 76 +++++ Library/Homebrew/test/cask/tab_spec.rb | 275 ++++++++++++++++++ Library/Homebrew/test/cmd/deps_spec.rb | 2 +- Library/Homebrew/test/cmd/untap_spec.rb | 2 +- Library/Homebrew/test/cmd/uses_spec.rb | 2 +- Library/Homebrew/test/formula_spec.rb | 12 +- .../test/installed_dependents_spec.rb | 2 +- .../fixtures/cask/Casks/many-artifacts.rb | 32 ++ .../cask/Casks/with-depends-on-everything.rb | 15 + .../test/support/fixtures/cask_receipt.json | 53 ++++ .../test/support/fixtures/receipt.json | 17 +- .../spec/shared_context/integration_test.rb | 2 +- Library/Homebrew/test/tab_spec.rb | 73 +++-- Library/Homebrew/test/uninstall_spec.rb | 2 +- 14 files changed, 527 insertions(+), 38 deletions(-) create mode 100644 Library/Homebrew/test/cask/tab_spec.rb create mode 100644 Library/Homebrew/test/support/fixtures/cask/Casks/many-artifacts.rb create mode 100644 Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb create mode 100644 Library/Homebrew/test/support/fixtures/cask_receipt.json diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index 701a58a2041cd..17e6b5c9beda5 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -212,6 +212,82 @@ 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 "#to_h" do let(:expected_json) { (TEST_FIXTURE_DIR/"cask/everything.json").read.strip } diff --git a/Library/Homebrew/test/cask/tab_spec.rb b/Library/Homebrew/test/cask/tab_spec.rb new file mode 100644 index 0000000000000..e6eeabef94225 --- /dev/null +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -0,0 +1,275 @@ +# 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 + + specify "::runtime_deps_hash" 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") + expect(Formulary).to receive(:factory).with("unar", { warn: false }).and_return(unar) + + expected_hash = { + arch: [{ type: :intel, bits: 64 }, { type: :arm, bits: 64 }], + cask: [ + { "full_name"=>"local-caffeine", "version"=>"1.2.3", "declared_directly"=>true }, + { "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>true }, + ], + formula: [ + { "full_name"=>"unar", "version"=>"1.2", "revision"=>0, "pkg_version"=>"1.2", "declared_directly"=>true }, + ], + macos: MacOSRequirement.new([:el_capitan], comparator: ">="), + } + + runtime_deps_hash = described_class.runtime_deps_hash(cask, cask.depends_on) + tab = described_class.new + tab.runtime_dependencies = runtime_deps_hash + expect(tab.runtime_dependencies).to eql(expected_hash) + 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") + + 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([{ app: ["Caffeine.app"] }]) + 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 +end diff --git a/Library/Homebrew/test/cmd/deps_spec.rb b/Library/Homebrew/test/cmd/deps_spec.rb index a4ccf274de7b9..80edb9433737b 100644 --- a/Library/Homebrew/test/cmd/deps_spec.rb +++ b/Library/Homebrew/test/cmd/deps_spec.rb @@ -32,7 +32,7 @@ # 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 diff --git a/Library/Homebrew/test/cmd/untap_spec.rb b/Library/Homebrew/test/cmd/untap_spec.rb index bc5d286724d67..fb1d3a77b0e5c 100644 --- a/Library/Homebrew/test/cmd/untap_spec.rb +++ b/Library/Homebrew/test/cmd/untap_spec.rb @@ -33,7 +33,7 @@ def load_formula(name:, with_formula_file: false, mock_install: false) 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": { diff --git a/Library/Homebrew/test/cmd/uses_spec.rb b/Library/Homebrew/test/cmd/uses_spec.rb index 83199c30fcfca..92d27df77812b 100644 --- a/Library/Homebrew/test/cmd/uses_spec.rb +++ b/Library/Homebrew/test/cmd/uses_spec.rb @@ -36,7 +36,7 @@ %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" } diff --git a/Library/Homebrew/test/formula_spec.rb b/Library/Homebrew/test/formula_spec.rb index 6fc08f61d1215..e85d970cc3a03 100644 --- a/Library/Homebrew/test/formula_spec.rb +++ b/Library/Homebrew/test/formula_spec.rb @@ -264,7 +264,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ class FooVariations < Formula 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 @@ def pour_bottle? 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] diff --git a/Library/Homebrew/test/installed_dependents_spec.rb b/Library/Homebrew/test/installed_dependents_spec.rb index 4bcbd383c44f4..35823954eea38 100644 --- a/Library/Homebrew/test/installed_dependents_spec.rb +++ b/Library/Homebrew/test/installed_dependents_spec.rb @@ -61,7 +61,7 @@ def alter_tab(keg) 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 diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/many-artifacts.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/many-artifacts.rb new file mode 100644 index 0000000000000..a488fd35f1d96 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/many-artifacts.rb @@ -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 diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb new file mode 100644 index 0000000000000..85570a7b002c8 --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb @@ -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: "local-transmission" + depends_on formula: "unar" + depends_on macos: ">= :el_capitan" + + app "Caffeine.app" +end diff --git a/Library/Homebrew/test/support/fixtures/cask_receipt.json b/Library/Homebrew/test/support/fixtures/cask_receipt.json new file mode 100644 index 0000000000000..e6ca53b9bbbfe --- /dev/null +++ b/Library/Homebrew/test/support/fixtures/cask_receipt.json @@ -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" + } +} diff --git a/Library/Homebrew/test/support/fixtures/receipt.json b/Library/Homebrew/test/support/fixtures/receipt.json index 6907ae040c5a2..c51ec4d08f182 100644 --- a/Library/Homebrew/test/support/fixtures/receipt.json +++ b/Library/Homebrew/test/support/fixtures/receipt.json @@ -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" + } } } diff --git a/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb b/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb index c11c4e4d4df9d..0132ceebb7951 100644 --- a/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb +++ b/Library/Homebrew/test/support/helper/spec/shared_context/integration_test.rb @@ -191,7 +191,7 @@ class #{Formulary.class_s(name)} < Formula 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 diff --git a/Library/Homebrew/test/tab_spec.rb b/Library/Homebrew/test/tab_spec.rb index 961f9c0e5cded..6f968305f10db 100644 --- a/Library/Homebrew/test/tab_spec.rb +++ b/Library/Homebrew/test/tab_spec.rb @@ -18,20 +18,40 @@ 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 @@ "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 @@ 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 @@ -156,10 +179,13 @@ 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 +197,9 @@ 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 +215,7 @@ 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 +227,9 @@ 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 +243,7 @@ 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 +251,9 @@ 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 +273,7 @@ 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 +312,7 @@ 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 diff --git a/Library/Homebrew/test/uninstall_spec.rb b/Library/Homebrew/test/uninstall_spec.rb index e4cc7d1f9191d..530a687310cb0 100644 --- a/Library/Homebrew/test/uninstall_spec.rb +++ b/Library/Homebrew/test/uninstall_spec.rb @@ -33,7 +33,7 @@ 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" }, ] From 3a732460e9cc073136810f41cd1fd58035d1f38a Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Thu, 4 Jul 2024 11:18:26 -0400 Subject: [PATCH 04/11] Refactor Tab create method --- Library/Homebrew/cask/tab.rb | 29 ++++++---------- Library/Homebrew/tab.rb | 65 ++++++++++++++++++------------------ 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index 837947a5dad61..f31dbabe232c8 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -9,23 +9,15 @@ class Tab < ::AbstractTab # 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) + 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, cask.depends_on) + tab.source["version"] = cask.version.to_s + tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true) + + tab end # Returns a {Tab} for an already installed cask, @@ -39,11 +31,10 @@ def self.for_cask(cask) tab.source = { "path" => cask.sourcefile_path.to_s, "tap" => cask.tap&.name, - "tap_git_head" => nil, + "tap_git_head" => (cask.tap&.installed? ? cask.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 diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 7092a3bf6fd07..71d3ec12b7128 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -30,16 +30,24 @@ class AbstractTab # @api internal attr_accessor :runtime_dependencies - def self.generic_attributes(formula_or_cask) - { + # Instantiates a {Tab} for a new installation of a formula or cask. + def self.create(formula_or_cask) + attributes = { "homebrew_version" => HOMEBREW_VERSION, "installed_as_dependency" => false, "installed_on_request" => false, "loaded_from_api" => formula_or_cask.loaded_from_api?, "time" => Time.now.to_i, "arch" => Hardware::CPU.arch, + "source" => { + "path" => formula_or_cask.sourcefile_path.to_s, + "tap" => formula_or_cask.tap&.name, + "tap_git_head" => (formula_or_cask.tap&.installed? ? formula_or_cask.tap.git_head : nil), + }, "built_on" => DevelopmentTools.build_system_info, } + + new(attributes) end # Returns the {Tab} for a formula or cask install receipt at `path`. @@ -124,36 +132,28 @@ class Tab < AbstractTab # 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) - 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? + 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["versions"] = { + "stable" => formula.stable&.version&.to_s, + "head" => formula.head&.version&.to_s, + "version_scheme" => formula.version_scheme, + } - new(attributes) + tab end # Like {from_file}, but bypass the cache. @@ -253,10 +253,11 @@ def self.for_formula(formula) 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&.installed? ? formula.tap.git_head : nil), + "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, From e176159b236a1791ade7eec7d066648498f91030 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Thu, 4 Jul 2024 11:18:30 -0400 Subject: [PATCH 05/11] Fix test --- Library/Homebrew/cask/tab.rb | 1 + Library/Homebrew/tab.rb | 2 +- Library/Homebrew/test/cask/tab_spec.rb | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index f31dbabe232c8..9702e9b2acae5 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -15,6 +15,7 @@ def self.create(cask) tab.uninstall_flight_blocks = cask.uninstall_flight_blocks? tab.runtime_dependencies = Tab.runtime_deps_hash(cask, cask.depends_on) 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 diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 71d3ec12b7128..e58de185bf015 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -40,7 +40,6 @@ def self.create(formula_or_cask) "time" => Time.now.to_i, "arch" => Hardware::CPU.arch, "source" => { - "path" => formula_or_cask.sourcefile_path.to_s, "tap" => formula_or_cask.tap&.name, "tap_git_head" => (formula_or_cask.tap&.installed? ? formula_or_cask.tap.git_head : nil), }, @@ -147,6 +146,7 @@ def self.create(formula, compiler, 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, diff --git a/Library/Homebrew/test/cask/tab_spec.rb b/Library/Homebrew/test/cask/tab_spec.rb index e6eeabef94225..7a84a93da0d1d 100644 --- a/Library/Homebrew/test/cask/tab_spec.rb +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -212,6 +212,10 @@ 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 @@ -225,7 +229,7 @@ "version" => "1.2.3", }) expect(tab.runtime_dependencies).to eq({}) - expect(tab.uninstall_artifacts).to eq([{ app: ["Caffeine.app"] }]) + expect(tab.uninstall_artifacts).to eq(expected_artifacts) end end From 79fd5ee2b75a683e5d3fa24766bb7e4c570589ec Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Fri, 5 Jul 2024 10:07:30 -0400 Subject: [PATCH 06/11] Refactor tap and versions --- Library/Homebrew/cask/tab.rb | 2 +- Library/Homebrew/tab.rb | 29 +++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index 9702e9b2acae5..68846ab3fa00f 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -32,7 +32,7 @@ def self.for_cask(cask) tab.source = { "path" => cask.sourcefile_path.to_s, "tap" => cask.tap&.name, - "tap_git_head" => (cask.tap&.installed? ? cask.tap.git_head : nil), + "tap_git_head" => tap_git_head(cask), "version" => cask.version.to_s, } tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true) diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index e58de185bf015..1d600f857ee80 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -41,7 +41,7 @@ def self.create(formula_or_cask) "arch" => Hardware::CPU.arch, "source" => { "tap" => formula_or_cask.tap&.name, - "tap_git_head" => (formula_or_cask.tap&.installed? ? formula_or_cask.tap.git_head : nil), + "tap_git_head" => tap_git_head(formula_or_cask), }, "built_on" => DevelopmentTools.build_system_info, } @@ -93,6 +93,12 @@ def self.empty new(attributes) end + def self.tap_git_head(formula_or_cask) + return unless formula_or_cask.tap&.installed? + + formula_or_cask.tap.git_head + end + def initialize(attributes = {}) attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } end @@ -176,13 +182,7 @@ def self.from_file_content(content, path) end end - if tab.source["versions"].nil? - tab.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| @@ -255,7 +255,7 @@ def self.for_formula(formula) tab.source = { "path" => formula.specified_path.to_s, "tap" => formula.tap&.name, - "tap_git_head" => (formula.tap&.installed? ? formula.tap.git_head : nil), + "tap_git_head" => tap_git_head(formula), "spec" => formula.active_spec_sym.to_s, "versions" => { "stable" => formula.stable&.version&.to_s, @@ -280,14 +280,19 @@ def self.empty tab.compiler = DevelopmentTools.default_compiler tab.aliases = [] tab.source["spec"] = "stable" - tab.source["versions"] = { + tab.source["versions"] = empty_source_versions + + tab + end + + def self.empty_source_versions + { "stable" => nil, "head" => nil, "version_scheme" => 0, } - - tab end + private_class_method :empty_source_versions def self.runtime_deps_hash(formula, deps) deps.map do |dep| From dd510a5606639d57c50be2eb258dd08ea557434f Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Fri, 5 Jul 2024 10:47:05 -0400 Subject: [PATCH 07/11] Improve test coverage --- Library/Homebrew/test/cask/info_spec.rb | 26 +++++++++ Library/Homebrew/test/cask/tab_spec.rb | 40 +++++++++++++ Library/Homebrew/test/tab_spec.rb | 75 +++++++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/Library/Homebrew/test/cask/info_spec.rb b/Library/Homebrew/test/cask/info_spec.rb index 8710a8ba70e21..b53c4ef3c0774 100644 --- a/Library/Homebrew/test/cask/info_spec.rb +++ b/Library/Homebrew/test/cask/info_spec.rb @@ -121,4 +121,30 @@ 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 diff --git a/Library/Homebrew/test/cask/tab_spec.rb b/Library/Homebrew/test/cask/tab_spec.rb index 7a84a93da0d1d..7df32cef23540 100644 --- a/Library/Homebrew/test/cask/tab_spec.rb +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -276,4 +276,44 @@ 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 diff --git a/Library/Homebrew/test/tab_spec.rb b/Library/Homebrew/test/tab_spec.rb index 6f968305f10db..6eed8cf7d6929 100644 --- a/Library/Homebrew/test/tab_spec.rb +++ b/Library/Homebrew/test/tab_spec.rb @@ -397,6 +397,25 @@ end end + describe "::tap_git_head" do + it "returns nil if the tap is nil" do + formula = instance_double(Formula, tap: nil) + expect(described_class.tap_git_head(formula)).to be_nil + end + + it "returns nil if the tap is not installed" do + tap = instance_double(Tap, installed?: false) + formula = instance_double(Formula, tap:) + expect(described_class.tap_git_head(formula)).to be_nil + end + + it "returns the tap git head if the tap is installed" do + tap = instance_double(Tap, installed?: true, git_head: "0453e16c8e3fac73104da50927a86221ca0740c2") + formula = instance_double(Formula, tap:) + expect(described_class.tap_git_head(formula)).to eq("0453e16c8e3fac73104da50927a86221ca0740c2") + 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) @@ -430,6 +449,62 @@ 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) From 119e02ceb08293dbc5dd1546dbc16cfc6155d9b0 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 9 Jul 2024 13:16:07 -0400 Subject: [PATCH 08/11] Cleanup `tap_git_head` and `uninstall_flight_blocks?` Co-authored-by: Kevin --- .../cask/artifact/abstract_flight_block.rb | 6 --- Library/Homebrew/cask/cask.rb | 13 ++++-- Library/Homebrew/cask/installer.rb | 2 +- Library/Homebrew/cask/tab.rb | 2 +- Library/Homebrew/tab.rb | 14 ++---- Library/Homebrew/test/cask/cask_spec.rb | 45 ++++++++++++++++--- Library/Homebrew/test/tab_spec.rb | 19 -------- 7 files changed, 55 insertions(+), 46 deletions(-) diff --git a/Library/Homebrew/cask/artifact/abstract_flight_block.rb b/Library/Homebrew/cask/artifact/abstract_flight_block.rb index 1a4cedc8df8ab..5c8c4daec7b34 100644 --- a/Library/Homebrew/cask/artifact/abstract_flight_block.rb +++ b/Library/Homebrew/cask/artifact/abstract_flight_block.rb @@ -34,12 +34,6 @@ def summarize 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) diff --git a/Library/Homebrew/cask/cask.rb b/Library/Homebrew/cask/cask.rb index 00ff239de7a9e..c2a53ade24e52 100644 --- a/Library/Homebrew/cask/cask.rb +++ b/Library/Homebrew/cask/cask.rb @@ -161,7 +161,12 @@ def caskfile_only? def uninstall_flight_blocks? artifacts.any? do |artifact| - artifact.is_a?(Artifact::AbstractFlightBlock) && artifact.uninstall? + case artifact + when Artifact::PreflightBlock + artifact.directives.key?(:uninstall_preflight) + when Artifact::PostflightBlock + artifact.directives.key?(:uninstall_postflight) + end end end @@ -480,11 +485,13 @@ def artifacts_list(compact: false, uninstall_only: false) artifacts.filter_map do |artifact| case artifact when Artifact::AbstractFlightBlock - next if uninstall_only && !artifact.uninstall? + 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 => nil } unless compact + { 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) diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index b16ac8709c562..f2b1b23b19982 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -426,7 +426,7 @@ def uninstall(successor: nil) def remove_tabfile tabfile = @cask.tab.tabfile - FileUtils.rm_f tabfile if tabfile.present? && tabfile.exist? + FileUtils.rm_f tabfile if tabfile @cask.config_path.parent.rmdir_if_possible end diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index 68846ab3fa00f..23c48d65c7922 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -32,7 +32,7 @@ def self.for_cask(cask) tab.source = { "path" => cask.sourcefile_path.to_s, "tap" => cask.tap&.name, - "tap_git_head" => tap_git_head(cask), + "tap_git_head" => cask.tap_git_head, "version" => cask.version.to_s, } tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true) diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 1d600f857ee80..267cd6bfc871d 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -41,7 +41,7 @@ def self.create(formula_or_cask) "arch" => Hardware::CPU.arch, "source" => { "tap" => formula_or_cask.tap&.name, - "tap_git_head" => tap_git_head(formula_or_cask), + "tap_git_head" => formula_or_cask.tap_git_head, }, "built_on" => DevelopmentTools.build_system_info, } @@ -93,12 +93,6 @@ def self.empty new(attributes) end - def self.tap_git_head(formula_or_cask) - return unless formula_or_cask.tap&.installed? - - formula_or_cask.tap.git_head - end - def initialize(attributes = {}) attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } end @@ -134,6 +128,7 @@ class Tab < AbstractTab 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) @@ -169,8 +164,7 @@ def self.from_file_content(content, path) 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 = 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? @@ -255,7 +249,7 @@ def self.for_formula(formula) tab.source = { "path" => formula.specified_path.to_s, "tap" => formula.tap&.name, - "tap_git_head" => tap_git_head(formula), + "tap_git_head" => formula.tap_git_head, "spec" => formula.active_spec_sym.to_s, "versions" => { "stable" => formula.stable&.version&.to_s, diff --git a/Library/Homebrew/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index 17e6b5c9beda5..038316a682715 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -217,16 +217,16 @@ it "returns all artifacts when no options are given" do expected_artifacts = [ - { "uninstall_preflight" => nil }, - { "preflight" => nil }, + { 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 }, + { uninstall_postflight: nil }, + { postflight: nil }, { zap: [{ rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"], trash: "~/Library/Logs/ManyArtifacts.log", @@ -255,13 +255,13 @@ it "returns only uninstall artifacts when uninstall_only is true" do expected_artifacts = [ - { "uninstall_preflight" => nil }, + { uninstall_preflight: nil }, { uninstall: [{ rmdir: "#{TEST_TMPDIR}/empty_directory_path", trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"], }] }, { app: ["ManyArtifacts/ManyArtifacts.app"] }, - { "uninstall_postflight" => nil }, + { uninstall_postflight: nil }, { zap: [{ rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"], trash: "~/Library/Logs/ManyArtifacts.log", @@ -288,6 +288,39 @@ 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 } diff --git a/Library/Homebrew/test/tab_spec.rb b/Library/Homebrew/test/tab_spec.rb index 6eed8cf7d6929..6c8414ae44e8b 100644 --- a/Library/Homebrew/test/tab_spec.rb +++ b/Library/Homebrew/test/tab_spec.rb @@ -397,25 +397,6 @@ end end - describe "::tap_git_head" do - it "returns nil if the tap is nil" do - formula = instance_double(Formula, tap: nil) - expect(described_class.tap_git_head(formula)).to be_nil - end - - it "returns nil if the tap is not installed" do - tap = instance_double(Tap, installed?: false) - formula = instance_double(Formula, tap:) - expect(described_class.tap_git_head(formula)).to be_nil - end - - it "returns the tap git head if the tap is installed" do - tap = instance_double(Tap, installed?: true, git_head: "0453e16c8e3fac73104da50927a86221ca0740c2") - formula = instance_double(Formula, tap:) - expect(described_class.tap_git_head(formula)).to eq("0453e16c8e3fac73104da50927a86221ca0740c2") - 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) From d17da89382d700450098d42be972c9bdc0b474d0 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 9 Jul 2024 15:25:57 -0400 Subject: [PATCH 09/11] Include recursive dependencies in cask tabs --- Library/Homebrew/cask/tab.rb | 49 +++++----- Library/Homebrew/tab.rb | 20 ++-- Library/Homebrew/test/cask/tab_spec.rb | 93 ++++++++++++++----- .../cask/Casks/with-depends-on-everything.rb | 2 +- Library/Homebrew/test/tab_spec.rb | 75 ++++++++++++--- 5 files changed, 172 insertions(+), 67 deletions(-) diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index 23c48d65c7922..08ceb477b0f02 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -50,34 +50,33 @@ def self.empty 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 + 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 - [type, deps] + 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[:macos] = depends_on.macos if depends_on.macos + runtime_deps[:arch] = depends_on.arch if depends_on.arch + + runtime_deps end def version diff --git a/Library/Homebrew/tab.rb b/Library/Homebrew/tab.rb index 267cd6bfc871d..85afd06e63b3c 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -93,6 +93,17 @@ def self.empty 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 @@ -290,14 +301,7 @@ def self.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 diff --git a/Library/Homebrew/test/cask/tab_spec.rb b/Library/Homebrew/test/cask/tab_spec.rb index 7df32cef23540..9d8818f28baef 100644 --- a/Library/Homebrew/test/cask/tab_spec.rb +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -85,28 +85,79 @@ expect(tab.runtime_dependencies).not_to be_nil end - specify "::runtime_deps_hash" 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") - expect(Formulary).to receive(:factory).with("unar", { warn: false }).and_return(unar) - - expected_hash = { - arch: [{ type: :intel, bits: 64 }, { type: :arm, bits: 64 }], - cask: [ - { "full_name"=>"local-caffeine", "version"=>"1.2.3", "declared_directly"=>true }, - { "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>true }, - ], - formula: [ - { "full_name"=>"unar", "version"=>"1.2", "revision"=>0, "pkg_version"=>"1.2", "declared_directly"=>true }, - ], - macos: MacOSRequirement.new([:el_capitan], comparator: ">="), - } + describe "::runtime_deps_hash" do + specify "with no dependencies" do + cask = Cask::CaskLoader.load("local-transmission") - runtime_deps_hash = described_class.runtime_deps_hash(cask, cask.depends_on) - tab = described_class.new - tab.runtime_dependencies = runtime_deps_hash - expect(tab.runtime_dependencies).to eql(expected_hash) + expect(described_class.runtime_deps_hash(cask, cask.depends_on)).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, cask.depends_on)).to eq(expected_hash) + end + + specify "with macos symbol dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-macos-symbol") + + expected_hash = { + macos: MacOSRequirement.new([MacOS.version.to_sym], comparator: "=="), + } + expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + end + + specify "with macos array dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-macos-array") + + expected_hash = { + macos: MacOSRequirement.new([[:catalina, MacOS.version.to_sym]], comparator: "=="), + } + expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + end + + specify "with arch dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-arch") + + expected_hash = { + arch: [ + { type: :intel, bits: 64 }, + { type: :arm, bits: 64 }, + ], + } + expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + 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 = { + arch: [{ type: :intel, bits: 64 }, { type: :arm, bits: 64 }], + 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 }, + ], + macos: MacOSRequirement.new([:el_capitan], comparator: ">="), + } + + runtime_deps_hash = described_class.runtime_deps_hash(cask, cask.depends_on) + tab = described_class.new + tab.runtime_dependencies = runtime_deps_hash + expect(tab.runtime_dependencies).to eql(expected_hash) + end end specify "other attributes" do diff --git a/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb b/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb index 85570a7b002c8..fef3374ef79d7 100644 --- a/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb +++ b/Library/Homebrew/test/support/fixtures/cask/Casks/with-depends-on-everything.rb @@ -7,7 +7,7 @@ depends_on arch: [:intel, :arm64] depends_on cask: "local-caffeine" - depends_on cask: "local-transmission" + depends_on cask: "with-depends-on-cask" depends_on formula: "unar" depends_on macos: ">= :el_capitan" diff --git a/Library/Homebrew/test/tab_spec.rb b/Library/Homebrew/test/tab_spec.rb index 6c8414ae44e8b..d3041239c3c61 100644 --- a/Library/Homebrew/test/tab_spec.rb +++ b/Library/Homebrew/test/tab_spec.rb @@ -155,18 +155,69 @@ 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" } + stub_formula_loader foo + bar = formula("bar") { url "bar-2.0" } + 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 From fbe422b768c1ce8bbc6287f409f4a0c0e3b3bd80 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Tue, 9 Jul 2024 20:01:43 -0400 Subject: [PATCH 10/11] Fix test --- Library/Homebrew/test/tab_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Library/Homebrew/test/tab_spec.rb b/Library/Homebrew/test/tab_spec.rb index d3041239c3c61..594e9c7747295 100644 --- a/Library/Homebrew/test/tab_spec.rb +++ b/Library/Homebrew/test/tab_spec.rb @@ -191,8 +191,8 @@ it "includes recursive dependencies" do foo = formula("foo") { url "foo-1.0" } - stub_formula_loader foo bar = formula("bar") { url "bar-2.0" } + stub_formula_loader foo stub_formula_loader bar # Simulating dependencies formula => foo => bar From fec84544a9ef84cbb5c8ff8eaa65f3dd78eab508 Mon Sep 17 00:00:00 2001 From: Rylan Polster Date: Wed, 10 Jul 2024 11:25:05 -0400 Subject: [PATCH 11/11] Remove `arch` and `macos` dependencies from cask tab --- Library/Homebrew/cask/tab.rb | 7 ++---- Library/Homebrew/test/cask/tab_spec.rb | 32 ++++++++------------------ 2 files changed, 11 insertions(+), 28 deletions(-) diff --git a/Library/Homebrew/cask/tab.rb b/Library/Homebrew/cask/tab.rb index 08ceb477b0f02..4cf359b33ada3 100644 --- a/Library/Homebrew/cask/tab.rb +++ b/Library/Homebrew/cask/tab.rb @@ -13,7 +13,7 @@ def self.create(cask) tab.tabfile = cask.metadata_main_container_path/FILENAME tab.uninstall_flight_blocks = cask.uninstall_flight_blocks? - tab.runtime_dependencies = Tab.runtime_deps_hash(cask, cask.depends_on) + 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) @@ -49,7 +49,7 @@ def self.empty tab end - def self.runtime_deps_hash(cask, depends_on) + 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) @@ -73,9 +73,6 @@ def self.runtime_deps_hash(cask, depends_on) end end - runtime_deps[:macos] = depends_on.macos if depends_on.macos - runtime_deps[:arch] = depends_on.arch if depends_on.arch - runtime_deps end diff --git a/Library/Homebrew/test/cask/tab_spec.rb b/Library/Homebrew/test/cask/tab_spec.rb index 9d8818f28baef..9b346e2601389 100644 --- a/Library/Homebrew/test/cask/tab_spec.rb +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -89,7 +89,7 @@ specify "with no dependencies" do cask = Cask::CaskLoader.load("local-transmission") - expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq({}) + expect(described_class.runtime_deps_hash(cask)).to eq({}) end specify "with cask dependencies" do @@ -100,37 +100,25 @@ { "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>true }, ], } - expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + expect(described_class.runtime_deps_hash(cask)).to eq(expected_hash) end - specify "with macos symbol dependencies" do + it "ignores macos symbol dependencies" do cask = Cask::CaskLoader.load("with-depends-on-macos-symbol") - expected_hash = { - macos: MacOSRequirement.new([MacOS.version.to_sym], comparator: "=="), - } - expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + expect(described_class.runtime_deps_hash(cask)).to eq({}) end - specify "with macos array dependencies" do + it "ignores macos array dependencies" do cask = Cask::CaskLoader.load("with-depends-on-macos-array") - expected_hash = { - macos: MacOSRequirement.new([[:catalina, MacOS.version.to_sym]], comparator: "=="), - } - expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + expect(described_class.runtime_deps_hash(cask)).to eq({}) end - specify "with arch dependencies" do + it "ignores arch dependencies" do cask = Cask::CaskLoader.load("with-depends-on-arch") - expected_hash = { - arch: [ - { type: :intel, bits: 64 }, - { type: :arm, bits: 64 }, - ], - } - expect(described_class.runtime_deps_hash(cask, cask.depends_on)).to eq(expected_hash) + expect(described_class.runtime_deps_hash(cask)).to eq({}) end specify "with all types of dependencies" do @@ -141,7 +129,6 @@ expect(Formulary).to receive(:factory).with("unar").and_return(unar) expected_hash = { - arch: [{ type: :intel, bits: 64 }, { type: :arm, bits: 64 }], 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 }, @@ -150,10 +137,9 @@ formula: [ { "full_name"=>"unar", "version"=>"1.2", "revision"=>0, "pkg_version"=>"1.2", "declared_directly"=>true }, ], - macos: MacOSRequirement.new([:el_capitan], comparator: ">="), } - runtime_deps_hash = described_class.runtime_deps_hash(cask, cask.depends_on) + 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)