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/cask.rb b/Library/Homebrew/cask/cask.rb index aa1a62516aaea..c2a53ade24e52 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,17 @@ def caskfile_only? languages.any? || artifacts.any?(Artifact::AbstractFlightBlock) end + def uninstall_flight_blocks? + artifacts.any? do |artifact| + case artifact + when Artifact::PreflightBlock + artifact.directives.key?(:uninstall_preflight) + when Artifact::PostflightBlock + artifact.directives.key?(:uninstall_postflight) + end + end + end + sig { returns(T.nilable(Time)) } def install_time # /.metadata///Casks/.{rb,json} -> @@ -209,6 +221,10 @@ def bundle_long_version bundle_version&.version end + def tab + Tab.for_cask(self) + end + def config_path metadata_main_container_path/"config.json" end @@ -465,6 +481,27 @@ def to_hash_with_variations(hash_method: :to_h) hash end + def artifacts_list(compact: false, uninstall_only: false) + artifacts.filter_map do |artifact| + case artifact + when Artifact::AbstractFlightBlock + uninstall_flight_block = artifact.directives.key?(:uninstall_preflight) || + artifact.directives.key?(:uninstall_postflight) + next if uninstall_only && !uninstall_flight_block + + # Only indicate whether this block is used as we don't load it from the API + # We can skip this entirely once we move to internal JSON v3. + { artifact.summarize.to_sym => nil } unless compact + else + zap_artifact = artifact.is_a?(Artifact::Zap) + uninstall_artifact = artifact.respond_to?(:uninstall_phase) || artifact.respond_to?(:post_uninstall_phase) + next if uninstall_only && !zap_artifact && !uninstall_artifact + + { artifact.class.dsl_key => artifact.to_args } + end + end + end + private sig { returns(T.nilable(Homebrew::BundleVersion)) } @@ -482,19 +519,6 @@ def api_to_local_hash(hash) hash end - def artifacts_list(compact: false) - artifacts.filter_map do |artifact| - case artifact - when Artifact::AbstractFlightBlock - # Only indicate whether this block is used as we don't load it from the API - # We can skip this entirely once we move to internal JSON v3. - { artifact.summarize => nil } unless compact - else - { artifact.class.dsl_key => artifact.to_args } - end - end - end - def url_specs url&.specs.dup.tap do |url_specs| case url_specs&.dig(:user_agent) 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/cask/installer.rb b/Library/Homebrew/cask/installer.rb index 517070f5ba919..f2b1b23b19982 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 + @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..4cf359b33ada3 --- /dev/null +++ b/Library/Homebrew/cask/tab.rb @@ -0,0 +1,108 @@ +# typed: true +# frozen_string_literal: true + +require "tab" + +module Cask + class Tab < ::AbstractTab + attr_accessor :uninstall_flight_blocks, :uninstall_artifacts + + # Instantiates a {Tab} for a new installation of a cask. + def self.create(cask) + tab = super + + tab.tabfile = cask.metadata_main_container_path/FILENAME + tab.uninstall_flight_blocks = cask.uninstall_flight_blocks? + tab.runtime_dependencies = Tab.runtime_deps_hash(cask) + tab.source["version"] = cask.version.to_s + tab.source["path"] = cask.sourcefile_path.to_s + tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true) + + tab + end + + # Returns a {Tab} for an already installed cask, + # or a fake one if the cask is not installed. + def self.for_cask(cask) + path = cask.metadata_main_container_path/FILENAME + + return from_file(path) if path.exist? + + tab = empty + tab.source = { + "path" => cask.sourcefile_path.to_s, + "tap" => cask.tap&.name, + "tap_git_head" => cask.tap_git_head, + "version" => cask.version.to_s, + } + tab.uninstall_artifacts = cask.artifacts_list(uninstall_only: true) + + tab + end + + def self.empty + tab = super + tab.uninstall_flight_blocks = false + tab.uninstall_artifacts = [] + tab.source["version"] = nil + + tab + end + + def self.runtime_deps_hash(cask) + cask_and_formula_dep_graph = ::Utils::TopologicalHash.graph_package_dependencies(cask) + cask_deps, formula_deps = cask_and_formula_dep_graph.values.flatten.uniq.partition do |dep| + dep.is_a?(Cask) + end + + runtime_deps = {} + + if cask_deps.any? + runtime_deps[:cask] = cask_deps.map do |dep| + { + "full_name" => dep.full_name, + "version" => dep.version.to_s, + "declared_directly" => cask.depends_on.cask.include?(dep.full_name), + } + end + end + + if formula_deps.any? + runtime_deps[:formula] = formula_deps.map do |dep| + formula_to_dep_hash(dep, cask.depends_on.formula) + end + end + + runtime_deps + end + + def version + source["version"] + end + + def to_json(*_args) + attributes = { + "homebrew_version" => homebrew_version, + "loaded_from_api" => loaded_from_api, + "uninstall_flight_blocks" => uninstall_flight_blocks, + "installed_as_dependency" => installed_as_dependency, + "installed_on_request" => installed_on_request, + "time" => time, + "runtime_dependencies" => runtime_dependencies, + "source" => source, + "arch" => arch, + "uninstall_artifacts" => uninstall_artifacts, + "built_on" => built_on, + } + + JSON.pretty_generate(attributes) + end + + def to_s + s = ["Installed"] + s << "using the formulae.brew.sh API" if loaded_from_api + s << Time.at(time).strftime("on %Y-%m-%d at %H:%M:%S") if time + s.join(" ") + end + end +end 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 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..85afd06e63b3c 100644 --- a/Library/Homebrew/tab.rb +++ b/Library/Homebrew/tab.rb @@ -8,78 +8,48 @@ 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) + # Instantiates a {Tab} for a new installation of a formula or cask. + def self.create(formula_or_cask) attributes = { "homebrew_version" => HOMEBREW_VERSION, - "used_options" => build.used_options.as_flags, - "unused_options" => build.unused_options.as_flags, - "tabfile" => formula.prefix/FILENAME, - "built_as_bottle" => build.bottle?, "installed_as_dependency" => false, "installed_on_request" => false, - "poured_from_bottle" => false, - "loaded_from_api" => false, + "loaded_from_api" => formula_or_cask.loaded_from_api?, "time" => Time.now.to_i, - "source_modified_time" => formula.source_modified_time.to_i, - "compiler" => compiler, - "stdlib" => stdlib, - "aliases" => formula.aliases, - "runtime_dependencies" => Tab.runtime_deps_hash(formula, runtime_deps), "arch" => Hardware::CPU.arch, "source" => { - "path" => formula.specified_path.to_s, - "tap" => formula.tap&.name, - "tap_git_head" => nil, # Filled in later if possible - "spec" => formula.active_spec_sym.to_s, - "versions" => { - "stable" => formula.stable&.version&.to_s, - "head" => formula.head&.version&.to_s, - "version_scheme" => formula.version_scheme, - }, + "tap" => formula_or_cask.tap&.name, + "tap_git_head" => formula_or_cask.tap_git_head, }, "built_on" => DevelopmentTools.build_system_info, } - # We can only get `tap_git_head` if the tap is installed locally - attributes["source"]["tap_git_head"] = formula.tap.git_head if formula.tap&.installed? - new(attributes) end - # Returns the {Tab} for an install receipt at `path`. + # Returns the {Tab} for a formula or cask install receipt at `path`. # # NOTE: Results are cached. def self.from_file(path) @@ -99,42 +69,132 @@ 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 - if attributes["source"]["spec"].nil? + def self.formula_to_dep_hash(formula, declared_deps) + { + "full_name" => formula.full_name, + "version" => formula.version.to_s, + "revision" => formula.revision, + "pkg_version" => formula.pkg_version.to_s, + "declared_directly" => declared_deps.include?(formula.full_name), + } + end + private_class_method :formula_to_dep_hash + + def initialize(attributes = {}) + attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } + end + + def parsed_homebrew_version + return Version::NULL if homebrew_version.nil? + + Version.new(homebrew_version) + end + + sig { returns(T.nilable(Tap)) } + def tap + tap_name = source["tap"] + Tap.fetch(tap_name) if tap_name + end + + def tap=(tap) + tap_name = tap.respond_to?(:name) ? tap.name : tap + source["tap"] = tap_name + end + + def write + self.class.cache[tabfile] = self + tabfile.atomic_write(to_json) + end +end + +class Tab < AbstractTab + # Check whether the formula was poured from a bottle. + # + # @api internal + attr_accessor :poured_from_bottle + + attr_accessor :built_as_bottle, :changed_files, :stdlib, :aliases + attr_writer :used_options, :unused_options, :compiler, :source_modified_time + attr_reader :tapped_from + + # Instantiates a {Tab} for a new installation of a formula. + def self.create(formula, compiler, stdlib) + tab = super(formula) + build = formula.build + runtime_deps = formula.runtime_dependencies(undeclared: false) + + tab.used_options = build.used_options.as_flags + tab.unused_options = build.unused_options.as_flags + tab.tabfile = formula.prefix/FILENAME + tab.built_as_bottle = build.bottle? + tab.poured_from_bottle = false + tab.source_modified_time = formula.source_modified_time.to_i + tab.compiler = compiler + tab.stdlib = stdlib + tab.aliases = formula.aliases + tab.runtime_dependencies = Tab.runtime_deps_hash(formula, runtime_deps) + tab.source["spec"] = formula.active_spec_sym.to_s + tab.source["path"] = formula.specified_path.to_s + tab.source["versions"] = { + "stable" => formula.stable&.version&.to_s, + "head" => formula.head&.version&.to_s, + "version_scheme" => formula.version_scheme, + } + + tab + end + + # Like {from_file}, but bypass the cache. + def self.from_file_content(content, path) + tab = super + + tab.source_modified_time ||= 0 + tab.source ||= {} + + tab.tap = tab.tapped_from if !tab.tapped_from.nil? && tab.tapped_from != "path or URL" + tab.tap = "homebrew/core" if tab.tap == "mxcl/master" || tab.tap == "Homebrew/homebrew" + + if tab.source["spec"].nil? version = PkgVersion.parse(File.basename(File.dirname(path))) - attributes["source"]["spec"] = if version.head? + tab.source["spec"] = if version.head? "head" else "stable" end end - if attributes["source"]["versions"].nil? - attributes["source"]["versions"] = { - "stable" => nil, - "head" => nil, - "version_scheme" => 0, - } - end + tab.source["versions"] ||= empty_source_versions # Tabs created with Homebrew 1.5.13 through 4.0.17 inclusive created empty string versions in some cases. ["stable", "head"].each do |spec| - attributes["source"]["versions"][spec] = attributes["source"]["versions"][spec].presence + tab.source["versions"][spec] = tab.source["versions"][spec].presence end - new(attributes) + tab end # Get the {Tab} for the given {Keg}, @@ -198,10 +258,11 @@ 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_git_head, + "spec" => formula.active_spec_sym.to_s, + "versions" => { "stable" => formula.stable&.version&.to_s, "head" => formula.head&.version&.to_s, "version_scheme" => formula.version_scheme, @@ -213,56 +274,37 @@ 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"] = empty_source_versions - new(attributes) + tab end + def self.empty_source_versions + { + "stable" => nil, + "head" => nil, + "version_scheme" => 0, + } + end + private_class_method :empty_source_versions + def self.runtime_deps_hash(formula, deps) deps.map do |dep| - f = dep.to_formula - { - "full_name" => f.full_name, - "version" => f.version.to_s, - "revision" => f.revision, - "pkg_version" => f.pkg_version.to_s, - "declared_directly" => formula.deps.include?(dep), - } + formula_to_dep_hash(dep.to_formula, formula.deps.map(&:name)) end end - def initialize(attributes = {}) - attributes.each { |key, value| instance_variable_set(:"@#{key}", value) } - end - def any_args_or_options? !used_options.empty? || !unused_options.empty? end @@ -307,12 +349,6 @@ 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 +369,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 +441,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/test/cask/cask_spec.rb b/Library/Homebrew/test/cask/cask_spec.rb index 701a58a2041cd..038316a682715 100644 --- a/Library/Homebrew/test/cask/cask_spec.rb +++ b/Library/Homebrew/test/cask/cask_spec.rb @@ -212,6 +212,115 @@ end end + describe "#artifacts_list" do + subject(:cask) { Cask::CaskLoader.load("many-artifacts") } + + it "returns all artifacts when no options are given" do + expected_artifacts = [ + { uninstall_preflight: nil }, + { preflight: nil }, + { uninstall: [{ + rmdir: "#{TEST_TMPDIR}/empty_directory_path", + trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"], + }] }, + { pkg: ["ManyArtifacts/ManyArtifacts.pkg"] }, + { app: ["ManyArtifacts/ManyArtifacts.app"] }, + { uninstall_postflight: nil }, + { postflight: nil }, + { zap: [{ + rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"], + trash: "~/Library/Logs/ManyArtifacts.log", + }] }, + ] + + expect(cask.artifacts_list).to eq(expected_artifacts) + end + + it "skips flight blocks when compact is true" do + expected_artifacts = [ + { uninstall: [{ + rmdir: "#{TEST_TMPDIR}/empty_directory_path", + trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"], + }] }, + { pkg: ["ManyArtifacts/ManyArtifacts.pkg"] }, + { app: ["ManyArtifacts/ManyArtifacts.app"] }, + { zap: [{ + rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"], + trash: "~/Library/Logs/ManyArtifacts.log", + }] }, + ] + + expect(cask.artifacts_list(compact: true)).to eq(expected_artifacts) + end + + it "returns only uninstall artifacts when uninstall_only is true" do + expected_artifacts = [ + { uninstall_preflight: nil }, + { uninstall: [{ + rmdir: "#{TEST_TMPDIR}/empty_directory_path", + trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"], + }] }, + { app: ["ManyArtifacts/ManyArtifacts.app"] }, + { uninstall_postflight: nil }, + { zap: [{ + rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"], + trash: "~/Library/Logs/ManyArtifacts.log", + }] }, + ] + + expect(cask.artifacts_list(uninstall_only: true)).to eq(expected_artifacts) + end + + it "skips flight blocks and returns only uninstall artifacts when compact and uninstall_only are true" do + expected_artifacts = [ + { uninstall: [{ + rmdir: "#{TEST_TMPDIR}/empty_directory_path", + trash: ["#{TEST_TMPDIR}/foo", "#{TEST_TMPDIR}/bar"], + }] }, + { app: ["ManyArtifacts/ManyArtifacts.app"] }, + { zap: [{ + rmdir: ["~/Library/Caches/ManyArtifacts", "~/Library/Application Support/ManyArtifacts"], + trash: "~/Library/Logs/ManyArtifacts.log", + }] }, + ] + + expect(cask.artifacts_list(compact: true, uninstall_only: true)).to eq(expected_artifacts) + end + end + + describe "#uninstall_flight_blocks?" do + matcher :have_uninstall_flight_blocks do + match do |actual| + actual.uninstall_flight_blocks? == true + end + end + + it "returns true when there are uninstall_preflight blocks" do + cask = Cask::CaskLoader.load("with-uninstall-preflight") + expect(cask).to have_uninstall_flight_blocks + end + + it "returns true when there are uninstall_postflight blocks" do + cask = Cask::CaskLoader.load("with-uninstall-postflight") + expect(cask).to have_uninstall_flight_blocks + end + + it "returns false when there are only preflight blocks" do + cask = Cask::CaskLoader.load("with-preflight") + expect(cask).not_to have_uninstall_flight_blocks + end + + it "returns false when there are only postflight blocks" do + cask = Cask::CaskLoader.load("with-postflight") + expect(cask).not_to have_uninstall_flight_blocks + end + + it "returns false when there are no flight blocks" do + cask = Cask::CaskLoader.load("local-caffeine") + expect(cask).not_to have_uninstall_flight_blocks + end + end + describe "#to_h" do let(:expected_json) { (TEST_FIXTURE_DIR/"cask/everything.json").read.strip } 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 new file mode 100644 index 0000000000000..9b346e2601389 --- /dev/null +++ b/Library/Homebrew/test/cask/tab_spec.rb @@ -0,0 +1,356 @@ +# frozen_string_literal: true + +require "cask" + +RSpec.describe Cask::Tab, :cask do + matcher :be_installed_as_dependency do + match do |actual| + actual.installed_as_dependency == true + end + end + + matcher :be_installed_on_request do + match do |actual| + actual.installed_on_request == true + end + end + + matcher :be_loaded_from_api do + match do |actual| + actual.loaded_from_api == true + end + end + + matcher :have_uninstall_flight_blocks do + match do |actual| + actual.uninstall_flight_blocks == true + end + end + + subject(:tab) do + described_class.new( + "homebrew_version" => HOMEBREW_VERSION, + "loaded_from_api" => false, + "uninstall_flight_blocks" => true, + "installed_as_dependency" => false, + "installed_on_request" => true, + "time" => time, + "runtime_dependencies" => { + "cask" => [{ "full_name" => "bar", "version" => "2.0", "declared_directly" => false }], + }, + "source" => { + "path" => CoreCaskTap.instance.path.to_s, + "tap" => CoreCaskTap.instance.to_s, + "tap_git_head" => "8b79aa759500f0ffdf65a23e12950cbe3bf8fe17", + "version" => "1.2.3", + }, + "arch" => Hardware::CPU.arch, + "uninstall_artifacts" => [{ "app" => ["Foo.app"] }], + "built_on" => DevelopmentTools.build_system_info, + ) + end + + let(:time) { Time.now.to_i } + + let(:f) { formula { url "foo-1.0" } } + let(:f_tab_path) { f.prefix/"INSTALL_RECEIPT.json" } + let(:f_tab_content) { (TEST_FIXTURE_DIR/"receipt.json").read } + + specify "defaults" do + stub_const("HOMEBREW_VERSION", "4.3.7") + + tab = described_class.empty + + expect(tab.homebrew_version).to eq(HOMEBREW_VERSION) + expect(tab).not_to be_installed_as_dependency + expect(tab).not_to be_installed_on_request + expect(tab).not_to be_loaded_from_api + expect(tab).not_to have_uninstall_flight_blocks + expect(tab.tap).to be_nil + expect(tab.time).to be_nil + expect(tab.runtime_dependencies).to be_nil + expect(tab.source["path"]).to be_nil + end + + specify "#runtime_dependencies" do + tab = described_class.new + expect(tab.runtime_dependencies).to be_nil + + tab.runtime_dependencies = {} + expect(tab.runtime_dependencies).not_to be_nil + + tab.runtime_dependencies = { + "cask" => [{ "full_name" => "bar", "version" => "2.0", "declared_directly" => false }], + } + expect(tab.runtime_dependencies).not_to be_nil + end + + describe "::runtime_deps_hash" do + specify "with no dependencies" do + cask = Cask::CaskLoader.load("local-transmission") + + expect(described_class.runtime_deps_hash(cask)).to eq({}) + end + + specify "with cask dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-cask") + + expected_hash = { + cask: [ + { "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>true }, + ], + } + expect(described_class.runtime_deps_hash(cask)).to eq(expected_hash) + end + + it "ignores macos symbol dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-macos-symbol") + + expect(described_class.runtime_deps_hash(cask)).to eq({}) + end + + it "ignores macos array dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-macos-array") + + expect(described_class.runtime_deps_hash(cask)).to eq({}) + end + + it "ignores arch dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-arch") + + expect(described_class.runtime_deps_hash(cask)).to eq({}) + end + + specify "with all types of dependencies" do + cask = Cask::CaskLoader.load("with-depends-on-everything") + + unar = instance_double(Formula, full_name: "unar", version: "1.2", revision: 0, pkg_version: "1.2", + deps: [], requirements: []) + expect(Formulary).to receive(:factory).with("unar").and_return(unar) + + expected_hash = { + cask: [ + { "full_name"=>"local-caffeine", "version"=>"1.2.3", "declared_directly"=>true }, + { "full_name"=>"with-depends-on-cask", "version"=>"1.2.3", "declared_directly"=>true }, + { "full_name"=>"local-transmission", "version"=>"2.61", "declared_directly"=>false }, + ], + formula: [ + { "full_name"=>"unar", "version"=>"1.2", "revision"=>0, "pkg_version"=>"1.2", "declared_directly"=>true }, + ], + } + + runtime_deps_hash = described_class.runtime_deps_hash(cask) + tab = described_class.new + tab.runtime_dependencies = runtime_deps_hash + expect(tab.runtime_dependencies).to eql(expected_hash) + end + end + + specify "other attributes" do + expect(tab.tap.name).to eq("homebrew/cask") + expect(tab.time).to eq(time) + expect(tab).not_to be_loaded_from_api + expect(tab).to have_uninstall_flight_blocks + expect(tab).not_to be_installed_as_dependency + expect(tab).to be_installed_on_request + expect(tab).not_to be_loaded_from_api + end + + describe "::from_file" do + it "parses a cask Tab from a file" do + path = Pathname.new("#{TEST_FIXTURE_DIR}/cask_receipt.json") + tab = described_class.from_file(path) + source_path = "/opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/f/foo.rb" + runtime_dependencies = { + "cask" => [ + { + "full_name" => "bar", + "version" => "2.0", + "declared_directly" => true, + }, + ], + "formula" => [ + { + "full_name" => "baz", + "version" => "3.0", + "revision" => 0, + "pkg_version" => "3.0", + "declared_directly" => true, + }, + ], + "macos" => { + ">=" => [ + "12", + ], + }, + } + + expect(tab).not_to be_loaded_from_api + expect(tab).to have_uninstall_flight_blocks + expect(tab).not_to be_installed_as_dependency + expect(tab).to be_installed_on_request + expect(tab.time).to eq(Time.at(1_719_289_256).to_i) + expect(tab.runtime_dependencies).to eq(runtime_dependencies) + expect(tab.source["path"]).to eq(source_path) + expect(tab.version).to eq("1.2.3") + expect(tab.tap.name).to eq("homebrew/cask") + end + end + + describe "::from_file_content" do + it "parses a cask Tab from a file" do + path = Pathname.new("#{TEST_FIXTURE_DIR}/cask_receipt.json") + tab = described_class.from_file_content(path.read, path) + source_path = "/opt/homebrew/Library/Taps/homebrew/homebrew-cask/Casks/f/foo.rb" + runtime_dependencies = { + "cask" => [ + { + "full_name" => "bar", + "version" => "2.0", + "declared_directly" => true, + }, + ], + "formula" => [ + { + "full_name" => "baz", + "version" => "3.0", + "revision" => 0, + "pkg_version" => "3.0", + "declared_directly" => true, + }, + ], + "macos" => { + ">=" => [ + "12", + ], + }, + } + + expect(tab).not_to be_loaded_from_api + expect(tab).to have_uninstall_flight_blocks + expect(tab).not_to be_installed_as_dependency + expect(tab).to be_installed_on_request + expect(tab.tabfile).to eq(path) + expect(tab.time).to eq(Time.at(1_719_289_256).to_i) + expect(tab.runtime_dependencies).to eq(runtime_dependencies) + expect(tab.source["path"]).to eq(source_path) + expect(tab.version).to eq("1.2.3") + expect(tab.tap.name).to eq("homebrew/cask") + end + + it "raises a parse exception message including the Tab filename" do + expect { described_class.from_file_content("''", "cask_receipt.json") }.to raise_error( + JSON::ParserError, + /receipt.json:/, + ) + end + end + + describe "::create" do + it "creates a cask Tab" do + cask = Cask::CaskLoader.load("local-caffeine") + expected_artifacts = [ + { app: ["Caffeine.app"] }, + { zap: [{ trash: "#{TEST_FIXTURE_DIR}/cask/caffeine/org.example.caffeine.plist" }] }, + ] + + tab = described_class.create(cask) + expect(tab).not_to be_loaded_from_api + expect(tab).not_to have_uninstall_flight_blocks + expect(tab).not_to be_installed_as_dependency + expect(tab).not_to be_installed_on_request + expect(tab.source).to eq({ + "path" => "#{CoreCaskTap.instance.path}/Casks/local-caffeine.rb", + "tap" => CoreCaskTap.instance.name, + "tap_git_head" => nil, + "version" => "1.2.3", + }) + expect(tab.runtime_dependencies).to eq({}) + expect(tab.uninstall_artifacts).to eq(expected_artifacts) + end + end + + describe "::for_cask" do + let(:cask) { Cask::CaskLoader.load("local-transmission") } + let(:cask_tab_path) { cask.metadata_main_container_path/AbstractTab::FILENAME } + let(:cask_tab_content) { (TEST_FIXTURE_DIR/"cask_receipt.json").read } + + it "creates a Tab for a given cask" do + tab = described_class.for_cask(cask) + expect(tab.source["path"]).to eq(cask.sourcefile_path.to_s) + end + + it "creates a Tab for a given cask with existing Tab" do + cask_tab_path.dirname.mkpath + cask_tab_path.write cask_tab_content + + tab = described_class.for_cask(cask) + expect(tab.tabfile).to eq(cask_tab_path) + end + + it "can create a Tab for a non-existent cask" do + cask_tab_path.dirname.mkpath + + tab = described_class.for_cask(cask) + expect(tab.tabfile).to be_nil + end + end + + specify "#to_json" do + json_tab = described_class.new(JSON.parse(tab.to_json)) + expect(json_tab.homebrew_version).to eq(tab.homebrew_version) + expect(json_tab.loaded_from_api).to eq(tab.loaded_from_api) + expect(json_tab.uninstall_flight_blocks).to eq(tab.uninstall_flight_blocks) + expect(json_tab.installed_as_dependency).to eq(tab.installed_as_dependency) + expect(json_tab.installed_on_request).to eq(tab.installed_on_request) + expect(json_tab.time).to eq(tab.time) + expect(json_tab.runtime_dependencies).to eq(tab.runtime_dependencies) + expect(json_tab.source["path"]).to eq(tab.source["path"]) + expect(json_tab.tap).to eq(tab.tap) + expect(json_tab.source["tap_git_head"]).to eq(tab.source["tap_git_head"]) + expect(json_tab.version).to eq(tab.version) + expect(json_tab.arch).to eq(tab.arch.to_s) + expect(json_tab.uninstall_artifacts).to eq(tab.uninstall_artifacts) + expect(json_tab.built_on["os"]).to eq(tab.built_on["os"]) + end + + describe "#to_s" do + let(:time_string) { Time.at(1_720_189_863).strftime("%Y-%m-%d at %H:%M:%S") } + + it "returns install information for a Tab with a time that was loaded from the API" do + tab = described_class.new( + loaded_from_api: true, + time: 1_720_189_863, + ) + output = "Installed using the formulae.brew.sh API on #{time_string}" + expect(tab.to_s).to eq(output) + end + + it "returns install information for a Tab with a time that was not loaded from the API" do + tab = described_class.new( + loaded_from_api: false, + time: 1_720_189_863, + ) + output = "Installed on #{time_string}" + expect(tab.to_s).to eq(output) + end + + it "returns install information for a Tab without a time that was loaded from the API" do + tab = described_class.new( + loaded_from_api: true, + time: nil, + ) + output = "Installed using the formulae.brew.sh API" + expect(tab.to_s).to eq(output) + end + + it "returns install information for a Tab without a time that was not loaded from the API" do + tab = described_class.new( + loaded_from_api: false, + time: nil, + ) + output = "Installed" + expect(tab.to_s).to eq(output) + end + end +end 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..fef3374ef79d7 --- /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: "with-depends-on-cask" + 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..594e9c7747295 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 @@ -132,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" } + bar = formula("bar") { url "bar-2.0" } + stub_formula_loader foo + stub_formula_loader bar + + # Simulating dependencies formula => foo => bar + formula_declared_deps = [Dependency.new("foo")] + formula_recursive_deps = [Dependency.new("foo"), Dependency.new("bar")] + formula = instance_double(Formula, deps: formula_declared_deps) + + expected_output = [ + { + "full_name" => "foo", + "version" => "1.0", + "revision" => 0, + "pkg_version" => "1.0", + "declared_directly" => true, + }, + { + "full_name" => "bar", + "version" => "2.0", + "revision" => 0, + "pkg_version" => "2.0", + "declared_directly" => false, + }, + ] + expect(described_class.runtime_deps_hash(formula, formula_recursive_deps)).to eq(expected_output) + end end specify "#cxxstdlib" do @@ -156,10 +230,13 @@ expect(tab.time).to eq(time) expect(tab).not_to be_built_as_bottle expect(tab).to be_poured_from_bottle + expect(tab).not_to be_installed_as_dependency + expect(tab).to be_installed_on_request + expect(tab).not_to be_loaded_from_api end describe "::from_file" do - it "parses a Tab from a file" do + it "parses a formula Tab from a file" do path = Pathname.new("#{TEST_FIXTURE_DIR}/receipt.json") tab = described_class.from_file(path) source_path = "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb" @@ -171,6 +248,9 @@ expect(tab.changed_files).to eq(changed_files) expect(tab).not_to be_built_as_bottle expect(tab).to be_poured_from_bottle + expect(tab).not_to be_installed_as_dependency + expect(tab).to be_installed_on_request + expect(tab).not_to be_loaded_from_api expect(tab).to be_stable expect(tab).not_to be_head expect(tab.tap.name).to eq("homebrew/core") @@ -186,7 +266,7 @@ end describe "::from_file_content" do - it "parses a Tab from a file" do + it "parses a formula Tab from a file" do path = Pathname.new("#{TEST_FIXTURE_DIR}/receipt.json") tab = described_class.from_file_content(path.read, path) source_path = "/usr/local/Library/Taps/homebrew/homebrew-core/Formula/foo.rb" @@ -198,6 +278,9 @@ expect(tab.changed_files).to eq(changed_files) expect(tab).not_to be_built_as_bottle expect(tab).to be_poured_from_bottle + expect(tab).not_to be_installed_as_dependency + expect(tab).to be_installed_on_request + expect(tab).not_to be_loaded_from_api expect(tab).to be_stable expect(tab).not_to be_head expect(tab.tap.name).to eq("homebrew/core") @@ -211,7 +294,7 @@ expect(tab.source["path"]).to eq(source_path) end - it "can parse an old Tab file" do + it "can parse an old formula Tab file" do path = Pathname.new("#{TEST_FIXTURE_DIR}/receipt_old.json") tab = described_class.from_file_content(path.read, path) @@ -219,6 +302,9 @@ expect(tab.unused_options.sort).to eq(unused_options.sort) expect(tab).not_to be_built_as_bottle expect(tab).to be_poured_from_bottle + expect(tab).not_to be_installed_as_dependency + expect(tab).not_to be_installed_on_request + expect(tab).not_to be_loaded_from_api expect(tab).to be_stable expect(tab).not_to be_head expect(tab.tap.name).to eq("homebrew/core") @@ -238,7 +324,7 @@ end describe "::create" do - it "creates a Tab" do + it "creates a formula Tab" do # < 1.1.7 runtime dependencies were wrong so are ignored stub_const("HOMEBREW_VERSION", "1.1.7") @@ -277,7 +363,7 @@ expect(tab.source["path"]).to eq(f.path.to_s) end - it "can create a Tab from an alias" do + it "can create a formula Tab from an alias" do alias_path = CoreTap.instance.alias_dir/"bar" f = formula(alias_path:) { url "foo-1.0" } compiler = DevelopmentTools.default_compiler @@ -395,6 +481,62 @@ 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) 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" }, ] 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)