Skip to content

Commit

Permalink
Implement conflicts_with :formula for casks
Browse files Browse the repository at this point in the history
  • Loading branch information
Rylan12 committed Dec 21, 2023
1 parent f61ef4b commit 6e878b6
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 38 deletions.
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

Check warning on line 99 in Library/Homebrew/cask/exceptions.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cask/exceptions.rb#L98-L99

Added lines #L98 - L99 were not covered by tests
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

Check warning on line 107 in Library/Homebrew/cask/exceptions.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cask/exceptions.rb#L104-L107

Added lines #L104 - L107 were not covered by tests
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")

Check warning on line 115 in Library/Homebrew/cask/exceptions.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cask/exceptions.rb#L115

Added line #L115 was not covered by tests
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)

Check warning on line 158 in Library/Homebrew/cask/installer.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cask/installer.rb#L157-L158

Added lines #L157 - L158 were not covered by tests
end
formula_conflicts.select! { |c| c.conflicts?(@cask) }

Check warning on line 160 in Library/Homebrew/cask/installer.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cask/installer.rb#L160

Added line #L160 was not covered by tests
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}"

Check warning on line 8 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L7-L8

Added lines #L7 - L8 were not covered by tests
message << ": because #{reason}" if reason
message.join

Check warning on line 10 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L10

Added line #L10 was not covered by tests
end

def conflicts?(formula_or_cask)
f = Formulary.factory(name)

Check warning on line 14 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L14

Added line #L14 was not covered by tests
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

Check warning on line 19 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L19

Added line #L19 was not covered by tests
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)

Check warning on line 23 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L23

Added line #L23 was not covered by tests
["homebrew-core", formula_or_cask.path.basename]
else
["homebrew-cask", formula_or_cask.sourcefile_path.basename]
end
opoo <<~EOS

Check warning on line 28 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L28

Added line #L28 was not covered by tests
#{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 " \

Check warning on line 35 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L35

Added line #L35 was not covered by tests
"(not Homebrew/brew or Homebrew/#{official_tap})!"
false

Check warning on line 37 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L37

Added line #L37 was not covered by tests
else
f.linked_keg.exist? && f.opt_prefix.exist?

Check warning on line 39 in Library/Homebrew/formula_conflict.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/formula_conflict.rb#L39

Added line #L39 was not covered by tests
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

0 comments on commit 6e878b6

Please sign in to comment.