Skip to content

Implement conflicts_with :formula for casks #16374

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Library/Homebrew/cask/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions Library/Homebrew/cask/installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# frozen_string_literal: true

require "formula_installer"
require "formula_conflict"
require "unpack_strategy"
require "utils/topological_hash"

Expand Down Expand Up @@ -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|
Expand All @@ -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
Expand Down
9 changes: 1 addition & 8 deletions Library/Homebrew/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions Library/Homebrew/formula.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
41 changes: 41 additions & 0 deletions Library/Homebrew/formula_conflict.rb
Original file line number Diff line number Diff line change
@@ -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
24 changes: 1 addition & 23 deletions Library/Homebrew/formula_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 0 additions & 3 deletions Library/Homebrew/formula_support.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion Library/Homebrew/test/exceptions_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions docs/Cask-Cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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.
Expand Down