diff --git a/Library/Homebrew/cask/exceptions.rb b/Library/Homebrew/cask/exceptions.rb index acbc0fa338d54..04c3d24bf9d26 100644 --- a/Library/Homebrew/cask/exceptions.rb +++ b/Library/Homebrew/cask/exceptions.rb @@ -88,6 +88,34 @@ def to_s end end + # Error when a cask conflicts with formulae. + # + # @api private + class CaskConflictWithFormulaError < AbstractCaskErrorWithToken + attr_reader :conflicting_formulae + + def initialize(token, conflicting_formulae) + super(token) + @conflicting_formulae = conflicting_formulae + end + + sig { returns(String) } + def to_s + message = [] + message << "Cask '#{token}' conflicts with the following formulae that are installed:" + message.concat conflicting_formulae.map(&:conflict_message) << "" + message << <<~EOS + Please `brew unlink #{conflicting_formulae.map(&:name) * " "}` before continuing. + + Unlinking removes a formula's symlinks from #{HOMEBREW_PREFIX}. You can + link the formula again after the install finishes. You can --force this + install, but the build may fail or cause obscure side effects in the + resulting software. + EOS + message.join("\n") + end + end + # Error when a cask is not available. # # @api private diff --git a/Library/Homebrew/cask/installer.rb b/Library/Homebrew/cask/installer.rb index 494193a252bc2..0143a65c24f6f 100644 --- a/Library/Homebrew/cask/installer.rb +++ b/Library/Homebrew/cask/installer.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "formula_installer" +require "formula_conflict" require "unpack_strategy" require "utils/topological_hash" @@ -138,6 +139,7 @@ def check_deprecate_disable end def check_conflicts + return if force? return unless @cask.conflicts_with @cask.conflicts_with[:cask].each do |conflicting_cask| @@ -151,6 +153,12 @@ def check_conflicts rescue CaskUnavailableError next # Ignore conflicting Casks that do not exist. end + + formula_conflicts = @cask.conflicts_with[:formula].map do |conflicting_formula| + FormulaConflict.new(conflicting_formula, nil) + end + formula_conflicts.select! { |c| c.conflicts?(@cask) } + raise CaskConflictWithFormulaError.new(@cask, formula_conflicts) unless formula_conflicts.empty? end def uninstall_existing_cask diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index 662b9f200a407..24eab097d4476 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -420,18 +420,11 @@ def initialize(formula, conflicts) super message end - def conflict_message(conflict) - message = [] - message << " #{conflict.name}" - message << ": because #{conflict.reason}" if conflict.reason - message.join - end - sig { returns(String) } def message message = [] message << "Cannot install #{formula.full_name} because conflicting formulae are installed." - message.concat conflicts.map { |c| conflict_message(c) } << "" + message.concat conflicts.map(&:conflict_message) << "" message << <<~EOS Please `brew unlink #{conflicts.map(&:name) * " "}` before continuing. diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index ac99e16df6462..740956f16095d 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -4,6 +4,7 @@ require "cache_store" require "did_you_mean" require "formula_support" +require "formula_conflict" require "lock_file" require "formula_pin" require "hardware" diff --git a/Library/Homebrew/formula_conflict.rb b/Library/Homebrew/formula_conflict.rb new file mode 100644 index 0000000000000..f5bdc15a89ac9 --- /dev/null +++ b/Library/Homebrew/formula_conflict.rb @@ -0,0 +1,41 @@ +# typed: true +# frozen_string_literal: true + +# Used to track formulae that cannot be installed at the same time. +FormulaConflict = Struct.new(:name, :reason) do + def conflict_message + message = [] + message << " #{name}" + message << ": because #{reason}" if reason + message.join + end + + def conflicts?(formula_or_cask) + f = Formulary.factory(name) + rescue TapFormulaUnavailableError + # If the formula name is a fully-qualified name let's silently + # ignore it as we don't care about things used in taps that aren't + # currently tapped. + false + rescue FormulaUnavailableError => e + # If the formula name doesn't exist any more then complain but don't + # stop installation from continuing. + official_tap, filename = if formula_or_cask.is_a?(Formula) + ["homebrew-core", formula_or_cask.path.basename] + else + ["homebrew-cask", formula_or_cask.sourcefile_path.basename] + end + opoo <<~EOS + #{formula_or_cask}: #{e.message} + 'conflicts_with "#{name}"' should be removed from #{filename}. + EOS + + raise if Homebrew::EnvConfig.developer? + + $stderr.puts "Please report this issue to the #{formula_or_cask.tap} tap " \ + "(not Homebrew/brew or Homebrew/#{official_tap})!" + false + else + f.linked_keg.exist? && f.opt_prefix.exist? + end +end diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index dd8a0e450ec89..e9b6b4266ee02 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -483,29 +483,7 @@ def install def check_conflicts return if force? - conflicts = formula.conflicts.select do |c| - f = Formulary.factory(c.name) - rescue TapFormulaUnavailableError - # If the formula name is a fully-qualified name let's silently - # ignore it as we don't care about things used in taps that aren't - # currently tapped. - false - rescue FormulaUnavailableError => e - # If the formula name doesn't exist any more then complain but don't - # stop installation from continuing. - opoo <<~EOS - #{formula}: #{e.message} - 'conflicts_with "#{c.name}"' should be removed from #{formula.path.basename}. - EOS - - raise if Homebrew::EnvConfig.developer? - - $stderr.puts "Please report this issue to the #{formula.tap} tap (not Homebrew/brew or Homebrew/homebrew-core)!" - false - else - f.linked_keg.exist? && f.opt_prefix.exist? - end - + conflicts = formula.conflicts.select { |c| c.conflicts?(formula) } raise FormulaConflictError.new(formula, conflicts) unless conflicts.empty? end diff --git a/Library/Homebrew/formula_support.rb b/Library/Homebrew/formula_support.rb index fa570fd296867..edf98c587d410 100644 --- a/Library/Homebrew/formula_support.rb +++ b/Library/Homebrew/formula_support.rb @@ -1,9 +1,6 @@ # typed: true # frozen_string_literal: true -# Used to track formulae that cannot be installed at the same time. -FormulaConflict = Struct.new(:name, :reason) - # Used to annotate formulae that duplicate macOS-provided software # or cause conflicts when linked in. class KegOnlyReason diff --git a/Library/Homebrew/test/exceptions_spec.rb b/Library/Homebrew/test/exceptions_spec.rb index a56fbb914cdd6..f2c6809758523 100644 --- a/Library/Homebrew/test/exceptions_spec.rb +++ b/Library/Homebrew/test/exceptions_spec.rb @@ -166,7 +166,10 @@ class Baz < Formula; end subject { described_class.new(formula, [conflict]) } let(:formula) { instance_double(Formula, full_name: "foo/qux") } - let(:conflict) { instance_double(FormulaConflict, name: "bar", reason: "I decided to") } + let(:conflict) do + instance_double(FormulaConflict, name: "bar", reason: "I decided to", conflicts?: true, + conflict_message: " bar: because I decided to") + end its(:to_s) { is_expected.to match(/Please `brew unlink bar` before continuing\./) } end diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index 49aee219b9c40..586347dfef25e 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -154,7 +154,7 @@ Each cask must declare one or more *artifacts* (i.e. something to install). | name | multiple occurrences allowed? | value | | ------------------------------------------ | :---------------------------: | ----- | | [`uninstall`](#stanza-uninstall) | yes | Procedures to uninstall a cask. Optional unless the `pkg` stanza is used. | -| [`conflicts_with`](#stanza-conflicts_with) | yes | List of conflicts with this cask (*not yet functional*). | +| [`conflicts_with`](#stanza-conflicts_with) | yes | List of conflicts with this cask. | | [`caveats`](#stanza-caveats) | yes | String or Ruby block providing the user with cask-specific information at install time. | | [`deprecate!`](#stanza-deprecate--disable) | no | Date as a String in `YYYY-MM-DD` format and a String or Symbol providing a reason. | | [`disable!`](#stanza-deprecate--disable) | no | Date as a String in `YYYY-MM-DD` format and a String or Symbol providing a reason. | @@ -351,8 +351,6 @@ conflicts_with cask: "macfuse-dev" #### `conflicts_with` *formula* -**Note:** `conflicts_with formula:` is a stub and is not yet functional. - The value should be another formula name. Example: [MacVim](https://github.com/Homebrew/homebrew-cask/blob/aa461148bbb5119af26b82cccf5003e2b4e50d95/Casks/m/macvim.rb#L16), which conflicts with the `macvim` formula.