Merge pull request #12018 from FnControlOption/upgrade-tsort

upgrade: use topological sort to upgrade formulae
This commit is contained in:
Mike McQuaid 2021-09-12 19:26:22 +01:00 committed by GitHub
commit 1a904af264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 183 additions and 54 deletions

View File

@ -20,6 +20,5 @@ require "cask/metadata"
require "cask/pkg" require "cask/pkg"
require "cask/quarantine" require "cask/quarantine"
require "cask/staged" require "cask/staged"
require "cask/topological_hash"
require "cask/url" require "cask/url"
require "cask/utils" require "cask/utils"

View File

@ -3,8 +3,8 @@
require "formula_installer" require "formula_installer"
require "unpack_strategy" require "unpack_strategy"
require "utils/topological_hash"
require "cask/topological_hash"
require "cask/config" require "cask/config"
require "cask/download" require "cask/download"
require "cask/staged" require "cask/staged"
@ -294,43 +294,14 @@ module Cask
"but you are running #{@current_arch}." "but you are running #{@current_arch}."
end end
def graph_dependencies(cask_or_formula, acc = TopologicalHash.new)
return acc if acc.key?(cask_or_formula)
if cask_or_formula.is_a?(Cask)
formula_deps = cask_or_formula.depends_on.formula.map { |f| Formula[f] }
cask_deps = cask_or_formula.depends_on.cask.map { |c| CaskLoader.load(c, config: nil) }
else
formula_deps = cask_or_formula.deps.reject(&:build?).map(&:to_formula)
cask_deps = cask_or_formula.requirements.map(&:cask).compact
.map { |c| CaskLoader.load(c, config: nil) }
end
acc[cask_or_formula] ||= []
acc[cask_or_formula] += formula_deps
acc[cask_or_formula] += cask_deps
formula_deps.each do |f|
graph_dependencies(f, acc)
end
cask_deps.each do |c|
graph_dependencies(c, acc)
end
acc
end
def collect_cask_and_formula_dependencies def collect_cask_and_formula_dependencies
return @cask_and_formula_dependencies if @cask_and_formula_dependencies return @cask_and_formula_dependencies if @cask_and_formula_dependencies
graph = graph_dependencies(@cask) graph = ::Utils::TopologicalHash.graph_package_dependencies(@cask)
raise CaskSelfReferencingDependencyError, cask.token if graph[@cask].include?(@cask) raise CaskSelfReferencingDependencyError, cask.token if graph[@cask].include?(@cask)
primary_container.dependencies.each do |dep| ::Utils::TopologicalHash.graph_package_dependencies(primary_container.dependencies, graph)
graph_dependencies(dep, graph)
end
begin begin
@cask_and_formula_dependencies = graph.tsort - [@cask] @cask_and_formula_dependencies = graph.tsort - [@cask]

View File

@ -1,21 +0,0 @@
# typed: true
# frozen_string_literal: true
require "tsort"
module Cask
# Topologically sortable hash map.
class TopologicalHash < Hash
include TSort
private
def tsort_each_node(&block)
each_key(&block)
end
def tsort_each_child(node, &block)
fetch(node).each(&block)
end
end
end

View File

@ -758,3 +758,13 @@ class ShebangDetectionError < RuntimeError
super "Cannot detect #{type} shebang: #{reason}." super "Cannot detect #{type} shebang: #{reason}."
end end
end end
# Raised when one or more formulae have cyclic dependencies.
class CyclicDependencyError < RuntimeError
def initialize(strongly_connected_components)
super <<~EOS
The following packages contain cyclic dependencies:
#{strongly_connected_components.select { |packages| packages.count > 1 }.map(&:to_sentence).join("\n ")}
EOS
end
end

View File

@ -0,0 +1,99 @@
# typed: false
# frozen_string_literal: true
require "utils/topological_hash"
describe Utils::TopologicalHash do
describe "#tsort" do
it "returns a topologically sorted array" do
hash = described_class.new
hash[1] = [2, 3]
hash[2] = [3]
hash[3] = []
hash[4] = []
expect(hash.tsort).to eq [3, 2, 1, 4]
end
end
describe "#strongly_connected_components" do
it "returns an array of arrays" do
hash = described_class.new
hash[1] = [2]
hash[2] = [3, 4]
hash[3] = [2]
hash[4] = []
expect(hash.strongly_connected_components).to eq [[4], [2, 3], [1]]
end
end
describe "::graph_package_dependencies" do
it "returns a topological hash" do
formula1 = formula "homebrew-test-formula1" do
url "foo"
version "0.5"
end
formula2 = formula "homebrew-test-formula2" do
url "foo"
version "0.5"
depends_on "homebrew-test-formula1"
end
formula3 = formula "homebrew-test-formula3" do
url "foo"
version "0.5"
depends_on "homebrew-test-formula4"
end
formula4 = formula "homebrew-test-formula4" do
url "foo"
version "0.5"
depends_on "homebrew-test-formula3"
end
cask1 = Cask::Cask.new("homebrew-test-cask1") do
url "foo"
version "1.2.3"
end
cask2 = Cask::Cask.new("homebrew-test-cask2") do
url "foo"
version "1.2.3"
depends_on cask: "homebrew-test-cask1"
depends_on formula: "homebrew-test-formula1"
end
cask3 = Cask::Cask.new("homebrew-test-cask3") do
url "foo"
version "1.2.3"
depends_on cask: "homebrew-test-cask2"
end
stub_formula_loader formula1
stub_formula_loader formula2
stub_formula_loader formula3
stub_formula_loader formula4
stub_cask_loader cask1
stub_cask_loader cask2
stub_cask_loader cask3
packages = [formula1, formula2, formula3, formula4, cask1, cask2, cask3]
expect(described_class.graph_package_dependencies(packages)).to eq({
formula1 => [],
formula2 => [formula1],
formula3 => [formula4],
formula4 => [formula3],
cask1 => [],
cask2 => [formula1, cask1],
cask3 => [cask2],
})
sorted = [formula1, cask1, cask2, cask3, formula2]
expect(described_class.graph_package_dependencies([cask3, cask2, cask1, formula2, formula1]).tsort).to eq sorted
expect(described_class.graph_package_dependencies([cask3, formula2]).tsort).to eq sorted
expect { described_class.graph_package_dependencies([formula3, formula4]).tsort }.to raise_error TSort::Cyclic
end
end
end

View File

@ -6,6 +6,7 @@ require "formula_installer"
require "development_tools" require "development_tools"
require "messages" require "messages"
require "cleanup" require "cleanup"
require "utils/topological_hash"
module Homebrew module Homebrew
# Helper functions for upgrading formulae. # Helper functions for upgrading formulae.
@ -42,6 +43,13 @@ module Homebrew
end end
end end
dependency_graph = Utils::TopologicalHash.graph_package_dependencies(formulae_to_install)
begin
formulae_to_install = dependency_graph.tsort & formulae_to_install
rescue TSort::Cyclic
raise CyclicDependencyError, dependency_graph.strongly_connected_components if Homebrew::EnvConfig.developer?
end
formula_installers = formulae_to_install.map do |formula| formula_installers = formulae_to_install.map do |formula|
Migrator.migrate_if_needed(formula, force: force, dry_run: dry_run) Migrator.migrate_if_needed(formula, force: force, dry_run: dry_run)
begin begin

View File

@ -0,0 +1,63 @@
# typed: true
# frozen_string_literal: true
require "tsort"
module Utils
# Topologically sortable hash map.
class TopologicalHash < Hash
extend T::Sig
include TSort
sig {
params(
packages: T.any(Cask::Cask, Formula, T::Array[T.any(Cask::Cask, Formula)]),
accumulator: TopologicalHash,
).returns(TopologicalHash)
}
def self.graph_package_dependencies(packages, accumulator = TopologicalHash.new)
packages = Array(packages)
packages.each do |cask_or_formula|
next accumulator if accumulator.key?(cask_or_formula)
if cask_or_formula.is_a?(Cask::Cask)
formula_deps = cask_or_formula.depends_on
.formula
.map { |f| Formula[f] }
cask_deps = cask_or_formula.depends_on
.cask
.map { |c| Cask::CaskLoader.load(c, config: nil) }
else
formula_deps = cask_or_formula.deps
.reject(&:build?)
.map(&:to_formula)
cask_deps = cask_or_formula.requirements
.map(&:cask)
.compact
.map { |c| Cask::CaskLoader.load(c, config: nil) }
end
accumulator[cask_or_formula] ||= []
accumulator[cask_or_formula] += formula_deps
accumulator[cask_or_formula] += cask_deps
graph_package_dependencies(formula_deps, accumulator)
graph_package_dependencies(cask_deps, accumulator)
end
accumulator
end
private
def tsort_each_node(&block)
each_key(&block)
end
def tsort_each_child(node, &block)
fetch(node).each(&block)
end
end
end