Skip to content

Commit 7e2c3cf

Browse files
committed
Order brew bundle formulae via strongly connected components
Sorting installed formulae by dependency built the graph from runtime dependencies recorded in installed keg tabs, which can disagree with the current formulae and form a cycle the live graph does not have. On such a cycle sort! aborted the whole bundle run via odie. Order with each_strongly_connected_component instead: it matches tsort dependency-first order for an acyclic graph but does not raise on a cycle. When a cycle exists, warn naming the full cyclic component and continue, rather than aborting.
1 parent 2e7d998 commit 7e2c3cf

2 files changed

Lines changed: 20 additions & 19 deletions

File tree

Library/Homebrew/bundle/brew.rb

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -426,32 +426,33 @@ def sort!(formulae)
426426
raise "formulae_by_full_name is nil" if @formulae_by_full_name.nil?
427427
raise "formulae_by_name is nil" if @formulae_by_name.nil?
428428

429-
sorted_names = begin
430-
topo.tsort
431-
rescue TSort::Cyclic
432-
# The graph is built from runtime dependencies recorded in installed
433-
# keg tabs, which can disagree with the current formulae and form a
434-
# cycle the live graph does not have (see Homebrew/homebrew-bundle#1513).
435-
# Warn and keep the bundle running with a best-effort order rather than
436-
# aborting. The reinstall remedy below does not reliably clear such cycles.
437-
cyclic = (topo.strongly_connected_components.find { |c| c.size > 1 } || [])
438-
.filter_map { |n| @formulae_by_full_name[n] || @formulae_by_name[n] }
439-
.uniq { |f| f[:full_name] }
440-
.map { |f| f[:full_name] }
429+
# Topo includes TSort. each_strongly_connected_component orders nodes
430+
# dependency-first like tsort but, unlike tsort, does not raise on a cycle.
431+
# Stale keg-tab dependency data can form a cycle the live graph does not
432+
# have (Homebrew/homebrew-bundle#1513), so warn and continue rather than
433+
# aborting the whole bundle.
434+
components = topo.each_strongly_connected_component.to_a
435+
436+
cyclic = components.select { |component| component.size > 1 }
437+
.flatten
438+
.filter_map { |name| @formulae_by_full_name[name] || @formulae_by_name[name] }
439+
.uniq { |f| f[:full_name] }
440+
.map { |f| f[:full_name] }
441+
if cyclic.any?
441442
opoo <<~EOS
442-
Formulae dependency graph sorting failed (likely due to a circular dependency):
443+
Formulae dependency graph sorting found a circular dependency:
443444
#{cyclic.join(", ")}
444-
If this persists, run the following commands and try again:
445+
This is usually caused by stale dependency data in installed keg tabs.
446+
If it persists, run the following commands and try again:
445447
brew update
446448
brew uninstall --ignore-dependencies --force #{cyclic.join(" ")}
447449
brew install #{cyclic.join(" ")}
448450
EOS
449-
topo.each_strongly_connected_component.to_a.flatten
450451
end
451452

452-
@formulae = sorted_names
453-
.map { |name| @formulae_by_full_name[name] || @formulae_by_name[name] }
454-
.uniq { |f| f[:full_name] }
453+
@formulae = components.flatten
454+
.map { |name| @formulae_by_full_name[name] || @formulae_by_name[name] }
455+
.uniq { |f| f[:full_name] }
455456
end
456457
end
457458

Library/Homebrew/test/bundle/brew_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def formula_double_depending_on(name, dependency)
205205
cyclic_bar = formula_double_depending_on("bar", "foo")
206206
expect(Formula).to receive(:installed).and_return([cyclic_foo, cyclic_bar])
207207

208-
expect { dumper.formulae_by_full_name }.to output(/dependency graph sorting failed/).to_stderr
208+
expect { dumper.formulae_by_full_name }.to output(/found a circular dependency/).to_stderr
209209
expect(dumper.formulae.map { |f| f[:full_name] }).to contain_exactly("foo", "bar")
210210
end
211211

0 commit comments

Comments
 (0)