pax_global_header00006660000000000000000000000064140145613330014512gustar00rootroot0000000000000052 comment=6246872e187bd88cafda44743cbec263e59e5841 crystal-molinillo-0.2.0/000077500000000000000000000000001401456133300151665ustar00rootroot00000000000000crystal-molinillo-0.2.0/.editorconfig000066400000000000000000000002261401456133300176430ustar00rootroot00000000000000root = true [*.cr] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true crystal-molinillo-0.2.0/.github/000077500000000000000000000000001401456133300165265ustar00rootroot00000000000000crystal-molinillo-0.2.0/.github/workflows/000077500000000000000000000000001401456133300205635ustar00rootroot00000000000000crystal-molinillo-0.2.0/.github/workflows/crystal.yml000066400000000000000000000004171401456133300227710ustar00rootroot00000000000000name: Crystal CI on: [push] jobs: build: runs-on: ubuntu-latest container: image: crystallang/crystal steps: - uses: actions/checkout@v2 - name: Install dependencies run: shards install - name: Run tests run: crystal spec crystal-molinillo-0.2.0/.gitignore000066400000000000000000000002251401456133300171550ustar00rootroot00000000000000/docs/ /lib/ /bin/ /.shards/ *.dwarf # Libraries don't need dependency lock # Dependencies will be locked in applications that use them /shard.lock crystal-molinillo-0.2.0/.gitmodules000066400000000000000000000001571401456133300173460ustar00rootroot00000000000000[submodule "spec/fixture"] path = spec/fixture url = https://github.com/CocoaPods/Resolver-Integration-Specs crystal-molinillo-0.2.0/.travis.yml000066400000000000000000000002501401456133300172740ustar00rootroot00000000000000language: crystal # Uncomment the following if you'd like Travis to run specs and check code formatting # script: # - crystal spec # - crystal tool format --check crystal-molinillo-0.2.0/LICENSE000066400000000000000000000022331401456133300161730ustar00rootroot00000000000000This project is licensed under the MIT license. Copyright (c) 2020 Manas Technology Solutions Copyright (c) 2014 Samuel E. Giddins segiddins@segiddins.me Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. crystal-molinillo-0.2.0/README.md000066400000000000000000000023031401456133300164430ustar00rootroot00000000000000# crystal-molinillo A port of [Molinillo](https://github.com/CocoaPods/Molinillo/) (generic dependency resolution algorithm) to [Crystal](https://crystal-lang.org) ## Installation 1. Add the dependency to your `shard.yml`: ```yaml dependencies: molinillo: github: crystal-lang/crystal-molinillo ``` 2. Run `shards install` ## Usage ```crystal require "molinillo" ``` This was built to be used by [Shards](https://github.com/crystal-lang/shards). Check [`MolinilloSolver`](https://github.com/crystal-lang/shards/blob/master/src/molinillo_solver.cr) for an example of integration. ## Development This code uses a subrepository with test fixtures. Make sure you clone the repository with `--recursive` before running tests: ``` git clone --recursive https://github.com/crystal-lang/crystal-molinillo ``` ## Contributing 1. Fork it () 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request ## Contributors - [Juan Wajnerman](https://github.com/waj) - creator and maintainer crystal-molinillo-0.2.0/shard.yml000066400000000000000000000001721401456133300170120ustar00rootroot00000000000000name: molinillo version: 0.2.0 authors: - Juan Wajnerman crystal: ">= 0.35.0, < 2.0.0" license: MIT crystal-molinillo-0.2.0/spec/000077500000000000000000000000001401456133300161205ustar00rootroot00000000000000crystal-molinillo-0.2.0/spec/dependency_graph/000077500000000000000000000000001401456133300214175ustar00rootroot00000000000000crystal-molinillo-0.2.0/spec/dependency_graph/log_spec.cr000066400000000000000000000016351401456133300235450ustar00rootroot00000000000000# frozen_string_literal: true require "../spec_helper" alias DG = Molinillo::DependencyGraph(Int32, Int32) def shared_examples_for_replay(prepare) it "replays the log" do copy = DG.new graph = DG.new.tap { |g| prepare.call(g) } graph.log.each &.up(copy) copy.should eq(graph) end it "can undo to an empty graph" do graph = DG.new tag = Reference.new graph.tag(tag) prepare.call(graph) graph.rewind_to(tag) graph.should eq(DG.new) end end describe Molinillo::DependencyGraph::Log do describe "with empty log" do shared_examples_for_replay ->(g : DG) {} end describe "with some graph" do shared_examples_for_replay ->(g : DG) do g.add_child_vertex("Foo", 1, [nil] of String?, 4) g.add_child_vertex("Bar", 2, ["Foo", nil], 3) g.add_child_vertex("Baz", 3, %w(Foo Bar), 2) g.add_child_vertex("Foo", 4, [] of String?, 1) end end end crystal-molinillo-0.2.0/spec/dependency_graph_spec.cr000066400000000000000000000054741401456133300227710ustar00rootroot00000000000000require "./spec_helper" private def test_dependency_graph graph = Molinillo::DependencyGraph(String, String).new root = graph.add_vertex("Root", "Root", true) root2 = graph.add_vertex("Root2", "Root2", true) child = graph.add_child_vertex("Child", "Child", %w(Root), "Child") {graph: graph, root: root, root2: root2, child: child} end describe Molinillo::DependencyGraph do describe "in general" do it "returns root vertices by name" do data = test_dependency_graph data[:graph].root_vertex_named("Root").should eq(data[:root]) end it "returns vertices by name" do data = test_dependency_graph data[:graph].vertex_named("Root").should eq(data[:root]) data[:graph].vertex_named("Child").should eq(data[:child]) end it "returns nil for non-existent root vertices" do data = test_dependency_graph data[:graph].root_vertex_named("missing").should be_nil end it "returns nil for non-existent vertices" do data = test_dependency_graph data[:graph].vertex_named("missing").should be_nil end end describe "detaching a vertex" do it "detaches a root vertex without successors" do graph = Molinillo::DependencyGraph(String, String).new root = graph.add_vertex("root", "root", true) graph.detach_vertex_named(root.name) graph.vertex_named(root.name).should be_nil graph.vertices.should be_empty end it "detaches a root vertex with successors" do graph = Molinillo::DependencyGraph(String, String).new root = graph.add_vertex("root", "root", true) child = graph.add_child_vertex("child", "child", %w(root), "child") graph.detach_vertex_named(root.name) graph.vertex_named(root.name).should be_nil graph.vertex_named(child.name).should be_nil graph.vertices.should be_empty end it "detaches a root vertex with successors with other parents" do graph = Molinillo::DependencyGraph(String, String).new root = graph.add_vertex("root", "root", true) root2 = graph.add_vertex("root2", "root2", true) child = graph.add_child_vertex("child", "child", %w(root root2), "child") graph.detach_vertex_named(root.name) graph.vertex_named(root.name).should be_nil graph.vertex_named(child.name).should eq(child) child.predecessors.should eq([root2]) graph.vertices.size.should eq(2) end it "detaches a vertex with predecessors" do graph = Molinillo::DependencyGraph(String, String).new parent = graph.add_vertex("parent", "parent", true) child = graph.add_child_vertex("child", "child", %w(parent), "child") graph.detach_vertex_named(child.name) graph.vertex_named(child.name).should be_nil graph.vertices.should eq({parent.name => parent}) parent.outgoing_edges.should be_empty end end end crystal-molinillo-0.2.0/spec/fixture/000077500000000000000000000000001401456133300176065ustar00rootroot00000000000000crystal-molinillo-0.2.0/spec/resolver_spec.cr000066400000000000000000000070271401456133300213270ustar00rootroot00000000000000require "./spec_helper" module Molinillo FIXTURE_CASE_DIR = FIXTURE_DIR / "case" class TestCase getter fixture : Fixture getter name : String @index : SpecificationProvider(Gem::Dependency | TestSpecification, TestSpecification)? @requested : Array(Gem::Dependency | TestSpecification)? @result : DependencyGraph(TestSpecification?, TestSpecification?)? @conflicts : Set(String)? @@all : Array(TestCase)? def self.from_fixture(fixture_path) fixture = File.open(fixture_path) { |f| Fixture.from_json(f) } new(fixture) end def initialize(@fixture) @name = fixture.name end def index @index ||= TestIndex.from_fixture(@fixture.index || "awesome") end def requested @requested ||= @fixture.requested.map do |(name, reqs)| Gem::Dependency.new(name.delete("\x01"), reqs.split(',').map(&.chomp)).as(Gem::Dependency | TestSpecification) end end def add_dependencies_to_graph(graph : DependencyGraph(P, P), parent, hash, all_parents = Set(DependencyGraph::Vertex(P, P)).new) forall P name = hash.name version = hash.version # Gem::Version.new(hash['version']) dependency = index.specs[name].find { |s| Shards::Versions.compare(s.version, version) == 0 }.not_nil! vertex = if parent graph.add_vertex(name, dependency).tap do |v| graph.add_edge(parent, v, dependency) end else graph.add_vertex(name, dependency, true) end return unless all_parents.add?(vertex) hash.dependencies.each do |dep| add_dependencies_to_graph(graph, vertex, dep, all_parents) end end def result @result ||= @fixture.resolved.reduce(DependencyGraph(TestSpecification?, TestSpecification?).new) do |graph, r| graph.tap do |g| add_dependencies_to_graph(g, nil, r) end end end def base @fixture.base.reduce(DependencyGraph(Gem::Dependency | TestSpecification, Gem::Dependency | TestSpecification).new) do |graph, r| graph.tap do |g| add_dependencies_to_graph(g, nil, r) end end end def conflicts @conflicts ||= @fixture.conflicts.to_set end def self.all @@all ||= Dir.glob(FIXTURE_CASE_DIR.to_s + "**/*.json").map { |fixture| TestCase.from_fixture(fixture) } end def resolve(index_class) index = index_class.new(self.index.specs) resolver = Resolver(Gem::Dependency | TestSpecification, TestSpecification).new(index, TestUI.new) resolver.resolve(requested, base) end def run(index_class) it name do # skip 'does not yet reliably pass' if test_case.ignore?(index_class) if fixture.conflicts.any? error = expect_raises(ResolverError) { resolve(index_class) } names = case error when CircularDependencyError error.vertices.map &.name when VersionConflict error.conflicts.keys else fail "Unexpected error type: #{error}" end.to_set names.should eq(self.conflicts) else result = resolve(index_class) result.should eq(self.result) end end end end describe Resolver do describe "dependency resolution" do describe "with the TestIndex index" do TestCase.all.each &.run(TestIndex) end end end end # it "list all cases" do # pp Molinillo::TestCase.all # end crystal-molinillo-0.2.0/spec/spec_helper.cr000066400000000000000000000001541401456133300207370ustar00rootroot00000000000000require "spec" require "../src/molinillo" require "./spec_helper/*" FIXTURE_DIR = Path.new("spec/fixture") crystal-molinillo-0.2.0/spec/spec_helper/000077500000000000000000000000001401456133300204115ustar00rootroot00000000000000crystal-molinillo-0.2.0/spec/spec_helper/fixture.cr000066400000000000000000000007051401456133300224270ustar00rootroot00000000000000require "json" class Molinillo::Fixture include JSON::Serializable property name : String property index : String? property requested : Hash(String, String) property base : Array(Dependency) property resolved : Array(Dependency) property conflicts : Array(String) end class Molinillo::Fixture::Dependency include JSON::Serializable property name : String property version : String property dependencies : Array(Dependency) end crystal-molinillo-0.2.0/spec/spec_helper/gem.cr000066400000000000000000000013751401456133300215150ustar00rootroot00000000000000module Gem class Dependency property name : String property requirement : Requirement def initialize(@name, requirements : Array(String)) @requirement = Requirement.new(requirements) end def prerelease? requirement.prerelease? end def to_s(io) io << name end end class Requirement property requirements : Array(String) def initialize(@requirements) end def satisfied_by?(version : String) requirements.all? do |req| Shards::Versions.matches?(version, req) end end def prerelease? requirements.any? { |r| Shards::Versions.prerelease?(r) } end def inspect(io) io << '"' io << requirements.join ", " io << '"' end end end crystal-molinillo-0.2.0/spec/spec_helper/index.cr000066400000000000000000000036261401456133300220550ustar00rootroot00000000000000require "./specification" module Molinillo FIXTURE_INDEX_DIR = FIXTURE_DIR / "index" class TestIndex getter specs : Hash(String, Array(TestSpecification)) include SpecificationProvider(Gem::Dependency | TestSpecification, TestSpecification) def self.from_fixture(fixture_name) new(TestIndex.specs_from_fixture(fixture_name)) end @@specs_from_fixture = {} of String => Hash(String, Array(TestSpecification)) def self.specs_from_fixture(fixture_name) @@specs_from_fixture[fixture_name] ||= begin lines = File.read_lines(FIXTURE_INDEX_DIR / (fixture_name + ".json")) lines = lines.map { |line| line.partition("//")[0] } Hash(String, Array(TestSpecification)).from_json(lines.join '\n').tap do |all_specs| all_specs.each do |name, specs| specs.sort! { |a, b| Shards::Versions.compare(b.version, a.version) } end end end end def initialize(@specs) end def requirement_satisfied_by?(requirement, activated, spec) if Shards::Versions.prerelease?(spec.version) && !requirement.prerelease? vertex = activated.vertex_named!(spec.name) return false if vertex.requirements.none?(&.prerelease?) end case requirement when TestSpecification requirement.version == spec.version when Gem::Dependency requirement.requirement.satisfied_by?(spec.version) end end def search_for(dependency : R) case dependency when Gem::Dependency specs.fetch(dependency.name) { Array(TestSpecification).new }.select do |spec| dependency.requirement.satisfied_by?(spec.version) end else raise "BUG: Unexpected dependency type: #{dependency}" end end def name_for(dependency) dependency.name end def dependencies_for(specification : S) specification.dependencies end end end crystal-molinillo-0.2.0/spec/spec_helper/specification.cr000066400000000000000000000021121401456133300235530ustar00rootroot00000000000000require "json" module Molinillo class TestSpecification include JSON::Serializable property name : String property version : String @[JSON::Field(converter: Molinillo::DepConverter)] property dependencies : Array(Gem::Dependency | TestSpecification) def to_s(io) io << "#{name} (#{version})" end def prerelease? Shards::Versions.prerelease?(version) end end module DepConverter def self.from_json(parser) deps = if parser.kind.begin_object? Hash(String, String).new(parser) else Hash(String, String).new.tap do |deps| parser.read_array do parser.read_begin_array key = parser.read_string value = parser.read_string parser.read_end_array deps[key] = value end end end deps.map do |name, requirement| requirements = requirement.split(',').map(&.chomp) Gem::Dependency.new(name, requirements).as(Gem::Dependency | TestSpecification) end end end end crystal-molinillo-0.2.0/spec/spec_helper/ui.cr000066400000000000000000000003701401456133300213540ustar00rootroot00000000000000module Molinillo class TestUI include UI @output : IO? def output @output ||= if debug? STDERR else File.open("/dev/null", "w") end end end end crystal-molinillo-0.2.0/spec/spec_helper/versions.cr000066400000000000000000000141761401456133300226200ustar00rootroot00000000000000module Shards module Versions # :nodoc: struct Segment NON_ALPHANUMERIC = /[^a-zA-Z0-9]/ NATURAL_SORT_EXTRACT_NEXT_CHARS_AND_DIGITS = /^(\D*)(\d*)(.*)$/ protected getter! segment : String def initialize(@str : String) if index = @str.index('+') @str = @str[0...index] end end def next @segment, _, @str = @str.partition(NON_ALPHANUMERIC) segment end def empty? segment.empty? end def to_i? segment.to_i?(whitespace: false) end def <=>(b : self) natural_sort(segment, b.segment) end # Original natural sorting algorithm from: # https://github.com/sourcefrog/natsort/blob/master/natcmp.rb # Copyright (C) 2003 by Alan Davies . private def natural_sort(a, b) if (a_num = a.to_i?(whitespace: false)) && (b_num = b.to_i?(whitespace: false)) return a_num <=> b_num end loop do return 0 if a.empty? && b.empty? a =~ NATURAL_SORT_EXTRACT_NEXT_CHARS_AND_DIGITS a_chars, a_digits, a = $1, $2, $3 b =~ NATURAL_SORT_EXTRACT_NEXT_CHARS_AND_DIGITS b_chars, b_digits, b = $1, $2, $3 ret = a_chars <=> b_chars return ret unless ret == 0 a_num = a_digits.to_i?(whitespace: false) b_num = b_digits.to_i?(whitespace: false) if a_num && b_num ret = a_num.to_i <=> b_num.to_i return ret unless ret == 0 else ret = a_digits <=> b_digits return ret unless ret == 0 end end end def only_zeroes? return if empty? yield unless to_i? == 0 loop do self.next return if empty? yield unless to_i? == 0 end end def prerelease? segment.each_char.any?(&.ascii_letter?) end def inspect(io) @segment.inspect(io) end end def self.sort(versions) versions.sort { |a, b| compare(a, b) } end def self.compare(a, b) if a == b return 0 end a_segment = Segment.new(a) b_segment = Segment.new(b) loop do # extract next segment from version number ("1.0.2" => "1" then "0" then "2"): a_segment.next b_segment.next # accept unbalanced version numbers ("1.0" == "1.0.0.0", "1.0" < "1.0.1") if a_segment.empty? b_segment.only_zeroes? { return b_segment.prerelease? ? -1 : 1 } return 0 end # accept unbalanced version numbers ("1.0.0.0" == "1.0", "1.0.1" > "1.0") if b_segment.empty? a_segment.only_zeroes? { return a_segment.prerelease? ? 1 : -1 } return 0 end # try to convert segments to numbers: a_num = a_segment.to_i? b_num = b_segment.to_i? ret = if a_num && b_num # compare numbers (for natural 1, 2, ..., 10, 11 ordering): b_num <=> a_num elsif a_num # b is preliminary version: a_segment.only_zeroes? do return b_segment <=> a_segment if a_segment.prerelease? return -1 end return -1 elsif b_num # a is preliminary version: b_segment.only_zeroes? do return b_segment <=> a_segment if b_segment.prerelease? return 1 end return 1 else # compare strings: b_segment <=> a_segment end # if different return the result (older or newer), otherwise continue # to the next segment: return ret unless ret == 0 end end def self.prerelease?(str) str.each_char do |char| return true if char.ascii_letter? break if char == '+' end false end protected def self.without_prereleases(versions) versions.reject { |v| prerelease?(v) } end def self.resolve(versions, requirements : Enumerable(String), prereleases = false) unless prereleases || requirements.any? { |r| prerelease?(r) } versions = without_prereleases(versions) end matching_versions = requirements .map { |requirement| resolve(versions, requirement) } .reduce(versions) { |a, e| a & e } sort(matching_versions) end def self.resolve(versions, requirement : String) case requirement when "*", "" versions when /~>\s*([^\s]+)/ ver = if idx = $1.rindex('.') $1[0...idx] else $1 end versions.select { |version| matches_approximate?(version, $1, ver) } when /\s*(~>|>=|<=|>|<|=)\s*([^~<>=\s]+)\s*/ versions.select { |version| matches_operator?(version, $1, $2) } else versions.select { |version| matches_operator?(version, "=", requirement) } end end def self.matches?(version : String, requirement : String) case requirement when "*", "" true when /~>\s*([^\s]+)\d*/ ver = if idx = $1.rindex('.') $1[0...idx] else $1 end matches_approximate?(version, $1, ver) when /\s*(~>|>=|<=|>|<|!=|=)\s*([^~<>=\s]+)\s*/ matches_operator?(version, $1, $2) else matches_operator?(version, "=", requirement) end end private def self.matches_approximate?(version, requirement, ver) version.starts_with?(ver) && !version[ver.size]?.try(&.ascii_alphanumeric?) && (compare(version, requirement) <= 0) end private def self.matches_operator?(version, operator, requirement) case operator when ">=" compare(version, requirement) <= 0 when "<=" compare(version, requirement) >= 0 when ">" compare(version, requirement) < 0 when "<" compare(version, requirement) > 0 when "!=" compare(version, requirement) != 0 else compare(version, requirement) == 0 end end end end crystal-molinillo-0.2.0/spec/state_spec.cr000066400000000000000000000023561401456133300206060ustar00rootroot00000000000000require "./spec_helper" module Molinillo describe ResolutionState do describe DependencyState do it "pops a possibility state" do possibility1 = Resolver::Resolution::PossibilitySet(String, String).new(%w(), %w(possibility1)) possibility = Resolver::Resolution::PossibilitySet(String, String).new(%w(), %w(possibility)) state = DependencyState(String, String).new( "name", %w(requirement1 requirement2 requirement3), DependencyGraph(Resolver::Resolution::PossibilitySet(String, String) | String | Nil, String).new, "requirement", [possibility1, possibility], 0, {} of String => Resolver::Resolution::Conflict(String, String), [] of Resolver::Resolution::UnwindDetails(String, String) ) possibility_state = state.pop_possibility_state {% for attr in %w(name requirements activated requirement conflicts) %} possibility_state.{{ attr.id }}.should eq(state.{{ attr.id }}) {% end %} possibility_state.should be_a(PossibilityState(String, String)) possibility_state.depth.should eq(state.depth + 1) possibility_state.possibilities.should eq([possibility]) end end end end crystal-molinillo-0.2.0/src/000077500000000000000000000000001401456133300157555ustar00rootroot00000000000000crystal-molinillo-0.2.0/src/molinillo.cr000066400000000000000000000001031401456133300202730ustar00rootroot00000000000000module Molinillo VERSION = "0.2.0" end require "./molinillo/**" crystal-molinillo-0.2.0/src/molinillo/000077500000000000000000000000001401456133300177535ustar00rootroot00000000000000crystal-molinillo-0.2.0/src/molinillo/delegates/000077500000000000000000000000001401456133300217105ustar00rootroot00000000000000crystal-molinillo-0.2.0/src/molinillo/delegates/resolution_state.cr000066400000000000000000000032641401456133300256460ustar00rootroot00000000000000module Molinillo # @!visibility private module Delegates # Delegates all {Molinillo::ResolutionState} methods to a `#state` property. module ResolutionState(R, S) # (see Molinillo::ResolutionState#name) def name current_state = state || Molinillo::ResolutionState(R, S).empty current_state.name end # (see Molinillo::ResolutionState#requirements) def requirements current_state = state || Molinillo::ResolutionState(R, S).empty current_state.requirements end # (see Molinillo::ResolutionState#activated) def activated current_state = state || Molinillo::ResolutionState(R, S).empty current_state.activated end # (see Molinillo::ResolutionState#requirement) def requirement current_state = state || Molinillo::ResolutionState(R, S).empty current_state.requirement end # (see Molinillo::ResolutionState#possibilities) def possibilities current_state = state || Molinillo::ResolutionState(R, S).empty current_state.possibilities end # (see Molinillo::ResolutionState#depth) def depth current_state = state || Molinillo::ResolutionState(R, S).empty current_state.depth end # (see Molinillo::ResolutionState#conflicts) def conflicts current_state = state || Molinillo::ResolutionState(R, S).empty current_state.conflicts end # (see Molinillo::ResolutionState#unused_unwind_options) def unused_unwind_options current_state = state || Molinillo::ResolutionState(R, S).empty current_state.unused_unwind_options end end end end crystal-molinillo-0.2.0/src/molinillo/delegates/specification_provider.cr000066400000000000000000000053761401456133300270030ustar00rootroot00000000000000module Molinillo module Delegates # Delegates all {Molinillo::SpecificationProvider} methods to a # `#specification_provider` property. module SpecificationProvider # (see Molinillo::SpecificationProvider#search_for) def search_for(dependency) with_no_such_dependency_error_handling do specification_provider.search_for(dependency) end end # (see Molinillo::SpecificationProvider#dependencies_for) def dependencies_for(specification) with_no_such_dependency_error_handling do specification_provider.dependencies_for(specification) end end # (see Molinillo::SpecificationProvider#requirement_satisfied_by?) def requirement_satisfied_by?(requirement, activated, spec) with_no_such_dependency_error_handling do specification_provider.requirement_satisfied_by?(requirement, activated, spec) end end # (see Molinillo::SpecificationProvider#name_for) def name_for(dependency) with_no_such_dependency_error_handling do specification_provider.name_for(dependency) end end # (see Molinillo::SpecificationProvider#name_for_explicit_dependency_source) def name_for_explicit_dependency_source with_no_such_dependency_error_handling do specification_provider.name_for_explicit_dependency_source end end # (see Molinillo::SpecificationProvider#name_for_locking_dependency_source) def name_for_locking_dependency_source with_no_such_dependency_error_handling do specification_provider.name_for_locking_dependency_source end end # (see Molinillo::SpecificationProvider#sort_dependencies) def sort_dependencies(dependencies, activated, conflicts) with_no_such_dependency_error_handling do specification_provider.sort_dependencies(dependencies, activated, conflicts) end end # (see Molinillo::SpecificationProvider#allow_missing?) def allow_missing?(dependency) with_no_such_dependency_error_handling do specification_provider.allow_missing?(dependency) end end # Ensures any raised {NoSuchDependencyError} has its # {NoSuchDependencyError#required_by} set. # @yield private def with_no_such_dependency_error_handling yield rescue error : NoSuchDependencyError if state # TODO # vertex = activated.vertex_named(name_for(error.dependency)) # error.required_by += vertex.incoming_edges.map { |e| e.origin.name } # error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty? end raise error end end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph.cr000066400000000000000000000135521401456133300236060ustar00rootroot00000000000000class Molinillo::DependencyGraph(P, R) end require "./dependency_graph/log" require "./dependency_graph/vertex" class Molinillo::DependencyGraph(P, R) # Enumerates through the vertices of the graph. # @return [Array] The graph's vertices. def each # return vertices.values.each unless block_given? vertices.values.each { |v| yield v } end getter log : Log(P, R) getter vertices : Hash(String, Vertex(P, R)) # A directed edge of a {DependencyGraph} # @attr [Vertex] origin The origin of the directed edge # @attr [Vertex] destination The destination of the directed edge # @attr [Object] requirement The requirement the directed edge represents record Edge(P, R), origin : Vertex(P, R), destination : Vertex(P, R), requirement : R def initialize @vertices = {} of String => Vertex(P, R) @log = Log(P, R).new end # Tags the current state of the dependency as the given tag # @param [Object] tag an opaque tag for the current state of the graph # @return [Void] def tag(tag : Symbol | Reference) log.tag(self, tag) end # Rewinds the graph to the state tagged as `tag` # @param [Object] tag the tag to rewind to # @return [Void] def rewind_to(tag) log.rewind_to(self, tag) end def inspect "#" end def to_dot dot_vertices = [] of String dot_edges = [] of String vertices.each do |n, v| dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]" v.outgoing_edges.each do |e| label = e.requirement dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]" end end dot_vertices.uniq! dot_vertices.sort! dot_edges.uniq! dot_edges.sort! dot = dot_vertices.unshift("digraph G {").push("") + dot_edges.push("}") dot.join("\n") end def ==(other) super || begin return false unless vertices.keys.to_set == other.vertices.keys.to_set vertices.each do |name, vertex| other_vertex = other.vertex_named(name) return false unless other_vertex return false unless vertex.payload == other_vertex.payload return false unless other_vertex.successors.to_set == vertex.successors.to_set end true end end # @param [String] name # @param [Object] payload # @param [Array] parent_names # @param [Object] requirement the requirement that is requiring the child # @return [void] def add_child_vertex(name : String, payload : P, parent_names : Array(String?), requirement : R) root = !(parent_names.delete(nil) || true) vertex = add_vertex(name, payload, root) vertex.explicit_requirements << requirement if root parent_names.each do |parent_name| parent_vertex = vertex_named!(parent_name) add_edge(parent_vertex, vertex, requirement) end vertex end # Adds a vertex with the given name, or updates the existing one. # @param [String] name # @param [Object] payload # @return [Vertex] the vertex that was added to `self` def add_vertex(name : String, payload : P, root : Bool = false) log.add_vertex(self, name, payload, root) end # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively # removing any non-root vertices that were orphaned in the process # @param [String] name # @return [Array] the vertices which have been detached def detach_vertex_named(name) log.detach_vertex_named(self, name) end # @param [String] name # @return [Vertex,nil] the vertex with the given name def vertex_named(name) : Vertex(P, R)? vertices[name]? end # @param [String] name # @return [Vertex,nil] the vertex with the given name def vertex_named!(name) : Vertex(P, R) vertices[name] end # @param [String] name # @return [Vertex,nil] the root vertex with the given name def root_vertex_named(name) : Vertex(P, R)? vertex = vertex_named(name) vertex if vertex && vertex.root end # Adds a new {Edge} to the dependency graph # @param [Vertex] origin # @param [Vertex] destination # @param [Object] requirement the requirement that this edge represents # @return [Edge] the added edge def add_edge(origin : Vertex(P, R), destination : Vertex(P, R), requirement : R) if destination.path_to?(origin) raise CircularDependencyError(P, R).new(path(destination, origin)) end add_edge_no_circular(origin, destination, requirement) end # Sets the payload of the vertex with the given name # @param [String] name the name of the vertex # @param [Object] payload the payload # @return [Void] def set_payload(name, payload) log.set_payload(self, name, payload) end # Adds a new {Edge} to the dependency graph without checking for # circularity. # @param (see #add_edge) # @return (see #add_edge) private def add_edge_no_circular(origin, destination, requirement) log.add_edge_no_circular(self, origin.name, destination.name, requirement) end # Returns the path between two vertices # @raise [ArgumentError] if there is no path between the vertices # @param [Vertex] from # @param [Vertex] to # @return [Array] the shortest path from `from` to `to` def path(from, to) distances = Hash(String, Int32).new(vertices.size + 1) distances[from.name] = 0 predecessors = {} of Vertex(P, R) => Vertex(P, R) each do |vertex| vertex.successors.each do |successor| if distances[successor.name] > distances[vertex.name] + 1 distances[successor.name] = distances[vertex.name] + 1 predecessors[successor] = vertex end end end path = [to] while before = predecessors[to]? path << before to = before break if to == from end unless path.last == from raise ArgumentError.new("There is no path from #{from.name} to #{to.name}") end path.reverse end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/000077500000000000000000000000001401456133300232525ustar00rootroot00000000000000crystal-molinillo-0.2.0/src/molinillo/dependency_graph/action.cr000066400000000000000000000013031401456133300250520ustar00rootroot00000000000000module Molinillo # An action that modifies a {DependencyGraph} that is reversible. # @abstract abstract class DependencyGraph::Action(P, R) # Performs the action on the given graph. # @param [DependencyGraph] graph the graph to perform the action on. # @return [Void] abstract def up(graph : DependencyGraph(P, R)) # Reverses the action on the given graph. # @param [DependencyGraph] graph the graph to reverse the action on. # @return [Void] abstract def down(graph : DependencyGraph(P, R)) # @return [Action,Nil] The previous action property previous : Action(P, R)? # @return [Action,Nil] The next action property next : Action(P, R)? end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/add_edge_no_circular.cr000066400000000000000000000017261401456133300277020ustar00rootroot00000000000000require "./action" class Molinillo::DependencyGraph class AddEdgeNoCircular(P, R) < Action(P, R) getter origin : String getter destination : String getter requirement : R def initialize(@origin : String, @destination : String, @requirement : R) end def up(graph) edge = make_edge(graph) edge.origin.outgoing_edges << edge edge.destination.incoming_edges << edge edge end def down(graph) edge = make_edge(graph) delete_first(edge.origin.outgoing_edges, edge) delete_first(edge.destination.incoming_edges, edge) end # @param [DependencyGraph] graph the graph to find vertices from # @return [Edge] The edge this action adds def make_edge(graph) Edge(P, R).new(graph.vertex_named!(origin), graph.vertex_named!(destination), requirement) end private def delete_first(array, item) return unless index = array.index(item) array.delete_at(index) end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/add_vertex.cr000066400000000000000000000014771401456133300257360ustar00rootroot00000000000000require "./action" class Molinillo::DependencyGraph class AddVertex(P, R) < Action(P, R) getter name : String getter payload : P getter root : Bool @existing : {payload: P, root: Bool}? def initialize(@name, @payload : P, @root) end def up(graph) if existing = graph.vertices[name]? @existing = {payload: existing.payload, root: existing.root} end vertex = existing || Vertex(P, R).new(name, payload) graph.vertices[vertex.name] = vertex vertex.payload ||= payload vertex.root ||= root vertex end def down(graph) if existing = @existing vertex = graph.vertices[name] vertex.payload = existing[:payload] vertex.root = existing[:root] else graph.vertices.delete(name) end end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/delete_edge.cr000066400000000000000000000013741401456133300260330ustar00rootroot00000000000000require "./action" class Molinillo::DependencyGraph class DeleteEdge(P, R) < Action(P, R) getter origin_name : String getter destination_name : String getter requirement : R def initialize(@origin_name, @destination_name, @requirement) end def up(graph) edge = make_edge(graph) edge.origin.outgoing_edges.delete(edge) edge.destination.incoming_edges.delete(edge) end def down(graph) edge = make_edge(graph) edge.origin.outgoing_edges << edge edge.destination.incoming_edges << edge edge end private def make_edge(graph) Edge(P, R).new( graph.vertex_named(origin_name), graph.vertex_named(destination_name), requirement ) end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/detach_vertex_named.cr000066400000000000000000000017721401456133300276000ustar00rootroot00000000000000require "./action" class Molinillo::DependencyGraph class DetachVertexNamed(P, R) < Action(P, R) getter name : String @vertex : Vertex(P, R)? def initialize(@name) end def up(graph) return [] of Vertex(P, R) unless vertex = @vertex = graph.vertices.delete(name) removed_vertices = [vertex] of Vertex(P, R) vertex.outgoing_edges.each do |e| v = e.destination v.incoming_edges.delete(e) if !v.root && v.incoming_edges.empty? removed_vertices.concat graph.detach_vertex_named(v.name) end end vertex.incoming_edges.each do |e| v = e.origin v.outgoing_edges.delete(e) end removed_vertices end def down(graph) return unless vertex = @vertex graph.vertices[vertex.name] = vertex vertex.outgoing_edges.each do |e| e.destination.incoming_edges << e end vertex.incoming_edges.each do |e| e.origin.outgoing_edges << e end end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/log.cr000066400000000000000000000045031401456133300243630ustar00rootroot00000000000000require "./add_vertex" require "./add_edge_no_circular" class Molinillo::DependencyGraph::Log(P, R) @current_action : Action(P, R)? @first_action : Action(P, R)? def tag(graph, tag) push_action(graph, Tag(P, R).new(tag)) end def add_vertex(graph, name : String, payload : P, root) push_action(graph, AddVertex(P, R).new(name, payload, root)) end def detach_vertex_named(graph, name) push_action(graph, DetachVertexNamed(P, R).new(name)) end def add_edge_no_circular(graph, origin, destination, requirement) push_action(graph, AddEdgeNoCircular(P, R).new(origin, destination, requirement)) end # {include:DependencyGraph#delete_edge} # @param [Graph] graph the graph to perform the action on # @param [String] origin_name # @param [String] destination_name # @param [Object] requirement # @return (see DependencyGraph#delete_edge) def delete_edge(graph, origin_name, destination_name, requirement) push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement)) end # @macro action def set_payload(graph, name, payload) push_action(graph, SetPayload(P, R).new(name, payload)) end # Pops the most recent action from the log and undoes the action # @param [DependencyGraph] graph # @return [Action] the action that was popped off the log def pop!(graph) return unless action = @current_action unless @current_action = action.previous @first_action = nil end action.down(graph) action end # Enumerates each action in the log # @yield [Action] def each action = @first_action loop do break unless action yield action action = action.next end self end def rewind_to(graph, tag) tag_value = Tag::Value.new(tag) loop do action = pop!(graph) raise "No tag #{tag.inspect} found" unless action break if action.is_a?(Tag(P, R)) && action.tag == tag_value end end # Adds the given action to the log, running the action # @param [DependencyGraph] graph # @param [Action] action # @return The value returned by `action.up` private def push_action(graph, action) action.previous = @current_action if current_action = @current_action current_action.next = action end @current_action = action @first_action ||= action action.up(graph) end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/set_payload.cr000066400000000000000000000006651401456133300261130ustar00rootroot00000000000000require "./action" class Molinillo::DependencyGraph class SetPayload(P, R) < Action(P, R) getter name : String getter payload : P @old_payload : P? def initialize(@name, @payload) end def up(graph) vertex = graph.vertex_named!(name) @old_payload = vertex.payload vertex.payload = payload end def down(graph) graph.vertex_named!(name).payload = @old_payload end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/tag.cr000066400000000000000000000012471401456133300243570ustar00rootroot00000000000000require "./action" class Molinillo::DependencyGraph class Tag(P, R) < Action(P, R) abstract struct Value def self.new(value : Reference) ReferenceValue.new(value) end def self.new(value : Symbol) OtherValue.new(value) end end struct ReferenceValue < Value @value : UInt64 def initialize(value : Reference) @value = value.object_id end end struct OtherValue < Value @value : Symbol def initialize(@value) end end getter tag : Value def up(graph) end def down(graph) end def initialize(tag) @tag = Value.new(tag) end end end crystal-molinillo-0.2.0/src/molinillo/dependency_graph/vertex.cr000066400000000000000000000033401401456133300251150ustar00rootroot00000000000000class Molinillo::DependencyGraph::Vertex(P, R) property root = false property name : String property payload : P getter explicit_requirements : Array(R) getter outgoing_edges : Array(Edge(P, R)) getter incoming_edges : Array(Edge(P, R)) def initialize(@name, @payload : P) @explicit_requirements = Array(R).new @outgoing_edges = Array(Edge(P, R)).new @incoming_edges = Array(Edge(P, R)).new end # @return [Array] all of the requirements that required # this vertex def requirements (incoming_edges.map(&.requirement) + explicit_requirements).uniq end # @return [Array] the vertices of {#graph} that have an edge with # `self` as their {Edge#destination} def predecessors incoming_edges.map &.origin end # @return [Array] the vertices of {#graph} that have an edge with # `self` as their {Edge#origin} def successors outgoing_edges.map &.destination end def ==(other) super || ( name == other.name && payload == other.payload && successors.to_set == other.successors.to_set ) end def_hash @name # Is there a path from `self` to `other` following edges in the # dependency graph? # @return true iff there is a path following edges within this {#graph} def path_to?(other) _path_to?(other) end # @param [Vertex] other the vertex to check if there's a path to # @param [Set] visited the vertices of {#graph} that have been visited # @return [Boolean] whether there is a path to `other` from `self` protected def _path_to?(other, visited = Set(self).new) return false unless visited.add?(self) return true if self == other successors.any? { |v| v._path_to?(other, visited) } end end crystal-molinillo-0.2.0/src/molinillo/errors.cr000066400000000000000000000137271401456133300216270ustar00rootroot00000000000000require "./delegates/specification_provider" module Molinillo # An error that occurred during the resolution process class ResolverError < Exception; end # An error caused by searching for a dependency that is completely unknown, # i.e. has no versions available whatsoever. class NoSuchDependencyError < ResolverError # @return [Object] the dependency that could not be found getter dependency : String # @return [Array] the specifications that depended upon {#dependency} getter required_by : Array(String) # Initializes a new error with the given missing dependency. # @param [Object] dependency @see {#dependency} # @param [Array] required_by @see {#required_by} def initialize(dependency, required_by = [] of S) @dependency = dependency @required_by = required_by.uniq super end # The error message for the missing dependency, including the specifications # that had this dependency. def message sources = required_by.map { |r| "`#{r}`" }.join(" and ") message = "Unable to find a specification for `#{dependency}`" message += " depended upon by #{sources}" unless sources.empty? message end end # An error caused by attempting to fulfil a dependency that was circular # # @note This exception will be thrown iff a {Vertex} is added to a # {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an # existing {DependencyGraph::Vertex} class CircularDependencyError(P, R) < ResolverError # [Set] the dependencies responsible for causing the error getter vertices : Array(DependencyGraph::Vertex(P, R)) # Initializes a new error with the given circular vertices. # @param [Array] vertices the vertices in the dependency # that caused the error def initialize(@vertices) super "There is a circular dependency between #{vertices.map(&.name).join(" and ")}" # @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set end end # An error caused by conflicts in version class VersionConflict(R, S) < ResolverError # @return [{String => Resolution::Conflict}] the conflicts that caused # resolution to fail getter conflicts : Hash(String, Resolver::Resolution::Conflict(R, S)) # @return [SpecificationProvider] the specification provider used during # resolution getter specification_provider : SpecificationProvider(R, S) # Initializes a new error with the given version conflicts. # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} # @param [SpecificationProvider] specification_provider see {#specification_provider} def initialize(conflicts, specification_provider) pairs = [] of {R, S | String} conflicts.values.flatten.flat_map(&.requirements).each do |conflicting| conflicting.each do |source, conflict_requirements| conflict_requirements.each do |c| pairs << {c, source} end end end super "Unable to satisfy the following requirements:\n\n" \ "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" @conflicts = conflicts @specification_provider = specification_provider end include Delegates::SpecificationProvider # @return [String] An error message that includes requirement trees, # which is much more detailed & customizable than the default message # @param [Hash] opts the options to create a message with. # @option opts [String] :solver_name The user-facing name of the solver # @option opts [String] :possibility_type The generic name of a possibility # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements # @option opts [Proc] :additional_message_for_conflict A proc that appends additional # messages for each conflict # @option opts [Proc] :version_for_spec A proc that returns the version number for a # possibility def message_with_trees(opts = {} of Symbol => String) solver_name = opts.delete(:solver_name) { self.class.name.split("::").first } possibility_type = opts.delete(:possibility_type) { "possibility named" } reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc { } } version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do proc do |name, _conflict| %(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":) end end conflicts.sort.reduce("".dup) do |o, (name, conflict)| o << "\n" << incompatible_version_message_for_conflict.call(name, conflict) << "\n" if conflict.locked_requirement o << %( In snapshot (#{name_for_locking_dependency_source}):\n) o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) o << %(\n) end o << %( In #{name_for_explicit_dependency_source}:\n) trees = reduce_trees.call(conflict.requirement_trees) o << trees.map do |tree| t = "".dup depth = 2 tree.each do |req| t << " " * depth << req.to_s unless tree.last == req if spec = conflict.activated_by_name[name_for(req)] t << %( was resolved to #{version_for_spec.call(spec)}, which) end t << %( depends on) end t << %(\n) depth += 1 end t end.join("\n") additional_message_for_conflict.call(o, name, conflict) o end.strip end end end crystal-molinillo-0.2.0/src/molinillo/modules/000077500000000000000000000000001401456133300214235ustar00rootroot00000000000000crystal-molinillo-0.2.0/src/molinillo/modules/specification_provider.cr000066400000000000000000000073501401456133300265100ustar00rootroot00000000000000module Molinillo # Provides information about specifcations and dependencies to the resolver, # allowing the {Resolver} class to remain generic while still providing power # and flexibility. # # This module contains the methods that users of Molinillo must to implement, # using knowledge of their own model classes. module SpecificationProvider(R, S) # Search for the specifications that match the given dependency. # The specifications in the returned array will be considered in reverse # order, so the latest version ought to be last. # @note This method should be 'pure', i.e. the return value should depend # only on the `dependency` parameter. # # @param [Object] dependency # @return [Array] the specifications that satisfy the given # `dependency`. def search_for(dependency : R) [] of S end # Returns the dependencies of `specification`. # @note This method should be 'pure', i.e. the return value should depend # only on the `specification` parameter. # # @param [Object] specification # @return [Array] the dependencies that are required by the given # `specification`. def dependencies_for(specification : S) [] of R end # Determines whether the given `requirement` is satisfied by the given # `spec`, in the context of the current `activated` dependency graph. # # @param [Object] requirement # @param [DependencyGraph] activated the current dependency graph in the # resolution process. # @param [Object] spec # @return [Boolean] whether `requirement` is satisfied by `spec` in the # context of the current `activated` dependency graph. def requirement_satisfied_by?(requirement : R, activated : DependencyGraph, spec : S) true end # Returns the name for the given `dependency`. # @note This method should be 'pure', i.e. the return value should depend # only on the `dependency` parameter. # # @param [Object] dependency # @return [String] the name for the given `dependency`. def name_for(dependency : R) dependency.to_s end # @return [String] the name of the source of explicit dependencies, i.e. # those passed to {Resolver#resolve} directly. def name_for_explicit_dependency_source "user-specified dependency" end # @return [String] the name of the source of 'locked' dependencies, i.e. # those passed to {Resolver#resolve} directly as the `base` def name_for_locking_dependency_source "Lockfile" end # Sort dependencies so that the ones that are easiest to resolve are first. # Easiest to resolve is (usually) defined by: # 1) Is this dependency already activated? # 2) How relaxed are the requirements? # 3) Are there any conflicts for this dependency? # 4) How many possibilities are there to satisfy this dependency? # # @param [Array] dependencies # @param [DependencyGraph] activated the current dependency graph in the # resolution process. # @param [{String => Array}] conflicts # @return [Array] a sorted copy of `dependencies`. def sort_dependencies(dependencies : Array(R), activated : DependencyGraph, conflicts) dependencies.sort_by do |dependency| name = name_for(dependency) { activated.vertex_named!(name).payload ? 0 : 1, conflicts[name]? ? 0 : 1, } end end # Returns whether this dependency, which has no possible matching # specifications, can safely be ignored. # # @param [Object] dependency # @return [Boolean] whether this dependency can safely be skipped. def allow_missing?(dependency : R) false end end end crystal-molinillo-0.2.0/src/molinillo/modules/ui.cr000066400000000000000000000034141401456133300223700ustar00rootroot00000000000000module Molinillo # Conveys information about the resolution process to a user. module UI # The {IO} object that should be used to print output. `STDOUT`, by default. # # @return [IO] def output STDOUT end # Called roughly every {#progress_rate}, this method should convey progress # to the user. # # @return [void] def indicate_progress output.print '.' unless debug? end # How often progress should be conveyed to the user via # {#indicate_progress}, in seconds. A third of a second, by default. # # @return [Float] def progress_rate 0.33 end # Called before resolution begins. # # @return [void] def before_resolution output.print "Resolving dependencies..." end # Called after resolution ends (either successfully or with an error). # By default, prints a newline. # # @return [void] def after_resolution output.puts end # Conveys debug information to the user. # # @param [Integer] depth the current depth of the resolution process. # @return [void] def debug(depth = 0) if debug? debug_info = yield debug_info = debug_info.inspect unless debug_info.is_a?(String) debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } debug_info.each { |line| output.puts line } end end @debug_mode : Bool? # Whether or not debug messages should be printed. # By default, whether or not the `MOLINILLO_DEBUG` environment variable is # set. # # @return [Boolean] def debug? debug_mode = @debug_mode if debug_mode == nil @debug_mode = ENV.has_key?("MOLINILLO_DEBUG") else debug_mode end end end end crystal-molinillo-0.2.0/src/molinillo/resolution.cr000066400000000000000000001065271401456133300225170ustar00rootroot00000000000000require "./delegates/*" module Molinillo class Resolver(R, S) # A specific resolution from a given {Resolver} class Resolution(R, S) # A conflict that the resolution process encountered # @attr [Object] requirement the requirement that immediately led to the conflict # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict # @attr [Object, nil] existing the existing spec that was in conflict with # the {#possibility} # @attr [Object] possibility_set the set of specs that was unable to be # activated due to a conflict. # @attr [Object] locked_requirement the relevant locking requirement. # @attr [Array>] requirement_trees the different requirement # trees that led to every requirement for the conflicting name. # @attr [{String=>Object}] activated_by_name the already-activated specs. # @attr [Object] underlying_error an error that has occurred during resolution, and # will be raised at the end of it if no resolution is found. class Conflict(R, S) getter requirement : R getter requirements : Hash(String | S, Array(R)) getter existing : S? getter possibility_set : PossibilitySet(R, S)? getter locked_requirement : R? getter requirement_trees : Array(Array(R)) getter activated_by_name : Hash(String, S) getter underlying_error : Exception? def initialize(@requirement, @requirements, @existing, @possibility_set, @locked_requirement, @requirement_trees, @activated_by_name, @underlying_error) end # @return [Object] a spec that was unable to be activated due to a conflict def possibility if s = possibility_set s.latest_version end end end # A collection of possibility states that share the same dependencies class PossibilitySet(R, S) # @attr [Array] dependencies the dependencies for this set of possibilities getter dependencies : Array(R) # @attr [Array] possibilities the possibilities getter possibilities : Array(S) def initialize(@dependencies, @possibilities) end # String representation of the possibility set, for debugging def to_s(io) io << "[#{possibilities.join(", ")}]" end # @return [Object] most up-to-date dependency in the possibility set def latest_version possibilities.last end def latest_version? possibilities.last? end end # Details of the state to unwind to when a conflict occurs, and the cause of the unwind # @attr [Integer] state_index the index of the state to unwind to # @attr [Object] state_requirement the requirement of the state we're unwinding to # @attr [Array] requirement_tree for the requirement we're relaxing # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict # @attr [Array] requirement_trees for the conflict # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind class UnwindDetails(R, S) getter state_index : Int32 getter state_requirement : R? getter requirement_tree : Array(R) getter conflicting_requirements : Array(R) getter requirement_trees : Array(Array(R)) getter requirements_unwound_to_instead : Array(R?) include Comparable(UnwindDetails(R, S)) def initialize(@state_index, @state_requirement, @requirement_tree, @conflicting_requirements, @requirement_trees, @requirements_unwound_to_instead) end # We compare UnwindDetails when choosing which state to unwind to. If # two options have the same state_index we prefer the one most # removed from a requirement that caused the conflict. Both options # would unwind to the same state, but a `grandparent` option will # filter out fewer of its possibilities after doing so - where a state # is both a `parent` and a `grandparent` to requirements that have # caused a conflict this is the correct behaviour. # @param [UnwindDetail] other UnwindDetail to be compared # @return [Integer] integer specifying ordering def <=>(other) if state_index > other.state_index 1 elsif state_index == other.state_index reversed_requirement_tree_index <=> other.reversed_requirement_tree_index else -1 end end # @return [Integer] index of state requirement in reversed requirement tree # (the conflicting requirement itself will be at position 0) def reversed_requirement_tree_index @reversed_requirement_tree_index ||= if state_requirement requirement_tree.reverse.index(state_requirement).not_nil! else 999_999 end end # @return [Boolean] where the requirement of the state we're unwinding # to directly caused the conflict. Note: in this case, it is # impossible for the state we're unwinding to to be a parent of # any of the other conflicting requirements (or we would have # circularity) def unwinding_to_primary_requirement? requirement_tree.last == state_requirement end @requirements_to_avoid : Array(R)? # @return [Array] array of sub-dependencies to avoid when choosing a # new possibility for the state we've unwound to. Only relevant for # non-primary unwinds def sub_dependencies_to_avoid @requirements_to_avoid ||= requirement_trees.map do |tree| index = tree.index(state_requirement) tree[index + 1] if index end.compact end @all_requirements : Array(R)? # @return [Array] array of all the requirements that led to the need for # this unwind def all_requirements @all_requirements ||= requirement_trees.flatten end end # @return [SpecificationProvider] the provider that knows about # dependencies, requirements, specifications, versions, etc. getter specification_provider : SpecificationProvider(R, S) # @return [UI] the UI that knows how to communicate feedback about the # resolution process back to the user getter resolver_ui : UI # @return [DependencyGraph] the base dependency graph to which # dependencies should be 'locked' getter base : DependencyGraph(R, R) # @return [Array] the dependencies that were explicitly required getter original_requested : Array(R) # Initializes a new resolution. # @param [SpecificationProvider] specification_provider # see {#specification_provider} # @param [UI] resolver_ui see {#resolver_ui} # @param [Array] requested see {#original_requested} # @param [DependencyGraph] base see {#base} def initialize(specification_provider, resolver_ui, requested, base) @specification_provider = specification_provider @resolver_ui = resolver_ui @original_requested = requested @base = base @states = Array(ResolutionState(R, S)).new @iteration_counter = 0 @parents_of = Hash(R, Array(Int32)).new { |h, k| h[k] = [] of Int32 } end # Resolves the {#original_requested} dependencies into a full dependency # graph # @raise [ResolverError] if successful resolution is impossible # @return [DependencyGraph] the dependency graph of successfully resolved # dependencies def resolve start_resolution while st = state break if !st.requirement && st.requirements.empty? indicate_progress if st.is_a?(DependencyState) # DependencyState debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.size} remaining)" } st.pop_possibility_state.tap do |s| if s states.push(s) activated.tag(s) end end end process_topmost_state end resolve_activated_specs ensure end_resolution end # @return [Integer] the number of resolver iterations in between calls to # {#resolver_ui}'s {UI#indicate_progress} method private property iteration_rate : Int32? # @return [Time] the time at which resolution began private property started_at : Time? # @return [Array] the stack of states for the resolution private getter states : Array(ResolutionState(R, S)) # Sets up the resolution process # @return [void] private def start_resolution @started_at = Time.local push_initial_state debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" } resolver_ui.before_resolution end private def resolve_activated_specs activated.vertices.each do |_, vertex| next unless payload = vertex.payload latest_version = check_possibility_set(vertex).possibilities.reverse_each.find do |possibility| vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } end activated.set_payload(vertex.name, latest_version) end activated end # Ends the resolution process # @return [void] private def end_resolution resolver_ui.after_resolution debug do "Finished resolution (#{@iteration_counter} steps) " \ "(Took #{(ended_at = Time.local) - @started_at.not_nil!} seconds) (#{ended_at})" end debug { "Unactivated: " + activated.vertices.reject { |_n, v| v.payload }.keys.join(", ") } if state debug { "Activated: " + activated.vertices.select { |_n, v| v.payload }.keys.join(", ") } if state end include Molinillo::Delegates::ResolutionState(R, S) include Molinillo::Delegates::SpecificationProvider # Processes the topmost available {RequirementState} on the stack # @return [void] private def process_topmost_state if possibilities.last? attempt_to_activate else create_conflict unwind_for_conflict end rescue underlying_error : CircularDependencyError create_conflict(underlying_error) unwind_for_conflict end # @return [Object] the current possibility that the resolution is trying # to activate private def possibility possibilities.last end # @return [RequirementState] the current state the resolution is # operating upon private def state states.last? end private def state! states.last end # Creates and pushes the initial state for the resolution, based upon the # {#requested} dependencies # @return [void] private def push_initial_state graph = DependencyGraph(PossibilitySet(R, S) | S | Nil, R).new.tap do |dg| original_requested.each do |requested| vertex = dg.add_vertex(name_for(requested), nil, true) vertex.explicit_requirements << requested end dg.tag(:initial_state) end push_state_for_requirements(original_requested, true, graph) end # Unwinds the states stack because a conflict has been encountered # @return [void] private def unwind_for_conflict details_for_unwind = build_details_for_unwind unwind_options = unused_unwind_options debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } conflicts.tap do |c| sliced_states = states.delete_at((details_for_unwind.state_index + 1)..-1) raise_error_unless_state(c) activated.rewind_to(sliced_states.first || :initial_state) if sliced_states state!.conflicts = c state!.unused_unwind_options = unwind_options filter_possibilities_after_unwind(details_for_unwind) index = states.size - 1 @parents_of.each { |_, a| a.reject! { |i| i >= index } } state!.unused_unwind_options.reject! { |uw| uw.state_index >= index } end end # Raises a VersionConflict error, or any underlying error, if there is no # current state # @return [void] private def raise_error_unless_state(conflicts) return if state error = conflicts.values.map(&.underlying_error).compact.first? raise error || VersionConflict(R, S).new(conflicts, specification_provider) end # @return [UnwindDetails] Details of the nearest index to which we could unwind private def build_details_for_unwind # Get the possible unwinds for the current conflict current_conflict = conflicts[name] binding_requirements = binding_requirements_for_conflict(current_conflict) unwind_details = unwind_options_for_requirements(binding_requirements) last_detail_for_current_unwind = unwind_details.sort.last current_detail = last_detail_for_current_unwind # Look for past conflicts that could be unwound to affect the # requirement tree for the current conflict relevant_unused_unwinds = unused_unwind_options.select do |alternative| intersecting_requirements = last_detail_for_current_unwind.all_requirements & alternative.requirements_unwound_to_instead next if intersecting_requirements.empty? # Find the highest index unwind whilst looping through current_detail = alternative if alternative > current_detail alternative end # Add the current unwind options to the `unused_unwind_options` array. # The "used" option will be filtered out during `unwind_for_conflict`. state!.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } # Update the requirements_unwound_to_instead on any relevant unused unwinds relevant_unused_unwinds.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } unwind_details.each { |d| d.requirements_unwound_to_instead << current_detail.state_requirement } current_detail end # @param [Array] binding_requirements array of requirements that combine to create a conflict # @return [Array] array of UnwindDetails that have a chance # of resolving the passed requirements private def unwind_options_for_requirements(binding_requirements) unwind_details = [] of UnwindDetails(R, S) trees = [] of Array(R) binding_requirements.reverse_each do |r| partial_tree = [r] trees << partial_tree unwind_details << UnwindDetails(R, S).new(-1, nil, partial_tree, binding_requirements, trees, [] of R?) # If this requirement has alternative possibilities, check if any would # satisfy the other requirements that created this conflict requirement_state = find_state_for(r) if conflict_fixing_possibilities?(requirement_state, binding_requirements) unwind_details << UnwindDetails(R, S).new( states.index(requirement_state).not_nil!, r, partial_tree, binding_requirements, trees, [] of R? ) end # Next, look at the parent of this requirement, and check if the requirement # could have been avoided if an alternative PossibilitySet had been chosen parent_r = parent_of(r) next if parent_r.nil? partial_tree.unshift(parent_r) requirement_state = find_state_for(parent_r).not_nil! if requirement_state.possibilities.any? { |set| !set.dependencies.includes?(r) } unwind_details << UnwindDetails(R, S).new( states.index(requirement_state).not_nil!, parent_r, partial_tree, binding_requirements, trees, [] of R? ) end # Finally, look at the grandparent and up of this requirement, looking # for any possibilities that wouldn't create their parent requirement grandparent_r = parent_of(parent_r) until grandparent_r.nil? partial_tree.unshift(grandparent_r) requirement_state = find_state_for(grandparent_r).not_nil! if requirement_state.possibilities.any? { |set| !set.dependencies.includes?(parent_r) } unwind_details << UnwindDetails(R, S).new( states.index(requirement_state).not_nil!, grandparent_r, partial_tree, binding_requirements, trees, [] of R? ) end parent_r = grandparent_r grandparent_r = parent_of(parent_r) end end unwind_details end # @param [DependencyState] state # @param [Array] binding_requirements array of requirements # @return [Boolean] whether or not the given state has any possibilities # that could satisfy the given requirements private def conflict_fixing_possibilities?(state, binding_requirements) return false unless state state.possibilities.any? do |possibility_set| possibility_set.possibilities.any? do |poss| possibility_satisfies_requirements?(poss, binding_requirements) end end end # Filter's a state's possibilities to remove any that would not fix the # conflict we've just rewound from # @param [UnwindDetails] unwind_details details of the conflict just # unwound from # @return [void] private def filter_possibilities_after_unwind(unwind_details) return unless state && !state!.possibilities.empty? if unwind_details.unwinding_to_primary_requirement? filter_possibilities_for_primary_unwind(unwind_details) else filter_possibilities_for_parent_unwind(unwind_details) end end # Filter's a state's possibilities to remove any that would not satisfy # the requirements in the conflict we've just rewound from # @param [UnwindDetails] unwind_details details of the conflict just unwound from # @return [void] private def filter_possibilities_for_primary_unwind(unwind_details) unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } unwinds_to_state << unwind_details unwind_requirement_sets = unwinds_to_state.map(&.conflicting_requirements) state!.possibilities.reject! do |possibility_set| possibility_set.possibilities.none? do |poss| unwind_requirement_sets.any? do |requirements| possibility_satisfies_requirements?(poss, requirements) end end end end # @param [Object] possibility a single possibility # @param [Array] requirements an array of requirements # @return [Boolean] whether the possibility satisfies all of the # given requirements private def possibility_satisfies_requirements?(possibility, requirements) name = name_for(possibility) activated.tag(:swap) activated.set_payload(name, possibility) if activated.vertex_named(name) satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } activated.rewind_to(:swap) satisfied end # Filter's a state's possibilities to remove any that would (eventually) # create a requirement in the conflict we've just rewound from # @param [UnwindDetails] unwind_details details of the conflict just unwound from # @return [void] private def filter_possibilities_for_parent_unwind(unwind_details) unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } unwinds_to_state << unwind_details primary_unwinds = unwinds_to_state.select(&.unwinding_to_primary_requirement?).uniq parent_unwinds = unwinds_to_state.uniq - primary_unwinds allowed_possibility_sets = primary_unwinds.flat_map do |unwind| states[unwind.state_index].possibilities.select do |possibility_set| possibility_set.possibilities.any? do |poss| possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) end end end requirements_to_avoid = parent_unwinds.flat_map(&.sub_dependencies_to_avoid) state!.possibilities.reject! do |possibility_set| !allowed_possibility_sets.includes?(possibility_set) && (requirements_to_avoid - possibility_set.dependencies).empty? end end # @param [Conflict] conflict # @return [Array] minimal array of requirements that would cause the passed # conflict to occur. private def binding_requirements_for_conflict(conflict) return [conflict.requirement] if conflict.possibility.nil? possible_binding_requirements = conflict.requirements.values.flatten.uniq # When there’s a `CircularDependency` error the conflicting requirement # (the one causing the circular) won’t be `conflict.requirement` # (which won’t be for the right state, because we won’t have created it, # because it’s circular). # We need to make sure we have that requirement in the conflict’s list, # otherwise we won’t be able to unwind properly, so we just return all # the requirements for the conflict. return possible_binding_requirements if conflict.underlying_error possibilities = search_for(conflict.requirement) # If all the requirements together don't filter out all possibilities, # then the only two requirements we need to consider are the initial one # (where the dependency's version was first chosen) and the last if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact end # Loop through the possible binding requirements, removing each one # that doesn't bind. Use a `reverse_each` as we want the earliest set of # binding requirements, and don't use `reject!` as we wish to refine the # array *on each iteration*. binding_requirements = possible_binding_requirements.dup possible_binding_requirements.reverse_each do |req| next if req == conflict.requirement unless binding_requirement_in_set?(req, binding_requirements, possibilities) binding_requirements -= [req] end end binding_requirements end # @param [Object] requirement we wish to check # @param [Array] possible_binding_requirements array of requirements # @param [Array] possibilities array of possibilities the requirements will be used to filter # @return [Boolean] whether or not the given requirement is required to filter # out all elements of the array of possibilities. private def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) possibilities.any? do |poss| possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) end end # @param [Object] requirement # @return [Object] the requirement that led to `requirement` being added # to the list of requirements. private def parent_of(requirement) return unless requirement return unless index = @parents_of[requirement].last? return unless parent_state = @states[index] parent_state.requirement end # @param [String] name # @return [Object] the requirement that led to a version of a possibility # with the given name being activated. private def requirement_for_existing_name(name) return nil unless vertex = activated.vertex_named(name) return nil unless vertex.payload states.find { |s| s.name == name }.not_nil!.requirement end # @param [Object] requirement # @return [ResolutionState] the state whose `requirement` is the given # `requirement`. private def find_state_for(requirement) return nil unless requirement states.find { |i| requirement == i.requirement } end # @param [Object] underlying_error # @return [Conflict] a {Conflict} that reflects the failure to activate # the {#possibility} in conjunction with the current {#state} private def create_conflict(underlying_error = nil) vertex = activated.vertex_named!(name) locked_requirement = locked_requirement_named(name) requirements = Hash(String | S, Array(R)).new unless vertex.explicit_requirements.empty? requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements end requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement vertex.incoming_edges.each do |edge| (requirements[check_possibility_set(edge.origin).latest_version] ||= [] of R).unshift(edge.requirement) end activated_by_name = {} of String => S activated.each { |v| activated_by_name[v.name] = check_possibility_set(v).latest_version if v.payload } conflicts[name.not_nil!] = Conflict(R, S).new( requirement.not_nil!, requirements, if vertex.payload check_possibility_set(vertex).latest_version end, possibilities.last?, locked_requirement, requirement_trees, activated_by_name, underlying_error ) end # @return [Array>] The different requirement # trees that led to every requirement for the current spec. private def requirement_trees vertex = activated.vertex_named!(name) vertex.requirements.map { |r| requirement_tree_for(r) } end # @param [Object] requirement # @return [Array] the list of requirements that led to # `requirement` being required. private def requirement_tree_for(requirement) tree = [] of R while requirement tree.unshift(requirement) requirement = parent_of(requirement) end tree end @progress_rate : Float64? # Indicates progress roughly once every second # @return [void] def indicate_progress @iteration_counter += 1 # @progress_rate ||= resolver_ui.progress_rate # if iteration_rate.nil? # if Time.now - @started_at.not_nil! >= @progress_rate.not_nil! # self.iteration_rate = @iteration_counter # end # end # if iteration_rate && (@iteration_counter % iteration_rate) == 0 # resolver_ui.indicate_progress # end end # Calls the {#resolver_ui}'s {UI#debug} method # @param [Integer] depth the depth of the {#states} stack # @param [Proc] block a block that yields a {#to_s} # @return [void] private def debug(depth = 0) resolver_ui.debug(depth) { yield } end # Attempts to activate the current {#possibility} # @return [void] private def attempt_to_activate debug(depth) { "Attempting to activate " + possibility.to_s } existing_vertex = activated.vertex_named!(name) if existing_vertex.payload debug(depth) { "Found existing spec (#{existing_vertex.payload})" } attempt_to_filter_existing_spec(existing_vertex) else latest = possibility.latest_version possibility.possibilities.select! do |possibility| requirement_satisfied_by?(requirement.not_nil!, activated.not_nil!, possibility) end if !possibility.latest_version? # ensure there's a possibility for better error messages possibility.possibilities << latest if latest create_conflict unwind_for_conflict else activate_new_spec end end end # Attempts to update the existing vertex's `PossibilitySet` with a filtered version # @return [void] private def attempt_to_filter_existing_spec(vertex) filtered_set = filtered_possibility_set(vertex) if !filtered_set.possibilities.empty? activated.set_payload(name.not_nil!, filtered_set) new_requirements = requirements.dup push_state_for_requirements(new_requirements, false) else create_conflict debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } unwind_for_conflict end end # Generates a filtered version of the existing vertex's `PossibilitySet` using the # current state's `requirement` # @param [Object] vertex existing vertex # @return [PossibilitySet] filtered possibility set private def filtered_possibility_set(vertex) payload = check_possibility_set(vertex) PossibilitySet(R, S).new(payload.dependencies, payload.possibilities & possibility.possibilities) end # @param [String] requirement_name the spec name to search for # @return [Object] the locked spec named `requirement_name`, if one # is found on {#base} private def locked_requirement_named(requirement_name) vertex = base.vertex_named(requirement_name) vertex && vertex.payload end # Add the current {#possibility} to the dependency graph of the current # {#state} # @return [void] private def activate_new_spec conflicts.delete(name) debug(depth) { "Activated #{name} at #{possibility}" } activated.set_payload(name.not_nil!, possibility) require_nested_dependencies_for(possibility) end # Requires the dependencies that the recently activated spec has # @param [Object] possibility_set the PossibilitySet that has just been # activated # @return [void] private def require_nested_dependencies_for(possibility_set) nested_dependencies = dependencies_for(possibility_set.latest_version) debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(", ")})" } nested_dependencies.each do |d| activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) parent_index = states.size - 1 parents = @parents_of[d] parents << parent_index if parents.empty? end push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?) end # Pushes a new {DependencyState} that encapsulates both existing and new # requirements # @param [Array] new_requirements # @param [Boolean] requires_sort # @param [Object] new_activated # @return [void] private def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort new_requirement = nil loop do new_requirement = new_requirements.shift? break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } end new_name = new_requirement ? name_for(new_requirement) : "" possibilities = possibilities_for_requirement(new_requirement) handle_missing_or_push_dependency_state DependencyState(R, S).new( new_name, new_requirements, new_activated, new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup ) end # Checks a proposed requirement with any existing locked requirement # before generating an array of possibilities for it. # @param [Object] requirement the proposed requirement # @param [Object] activated # @return [Array] possibilities private def possibilities_for_requirement(requirement, activated = self.activated) return [] of PossibilitySet(R, S) unless requirement if locked_requirement_named(name_for(requirement)) return locked_requirement_possibility_set(requirement, activated) end group_possibilities(search_for(requirement)) end # @param [Object] requirement the proposed requirement # @param [Object] activated # @return [Array] possibility set containing only the locked requirement, if any private def locked_requirement_possibility_set(requirement, activated = self.activated) all_possibilities = search_for(requirement) locked_requirement = locked_requirement_named(name_for(requirement)).not_nil! # Longwinded way to build a possibilities array with either the locked # requirement or nothing in it. Required, since the API for # locked_requirement isn't guaranteed. locked_possibilities = all_possibilities.select do |possibility| requirement_satisfied_by?(locked_requirement, activated, possibility) end group_possibilities(locked_possibilities) end # Build an array of PossibilitySets, with each element representing a group of # dependency versions that all have the same sub-dependency version constraints # and are contiguous. # @param [Array] possibilities an array of possibilities # @return [Array] an array of possibility sets private def group_possibilities(possibilities) possibility_sets = [] of PossibilitySet(R, S) current_possibility_set = nil possibilities.reverse_each do |possibility| dependencies = dependencies_for(possibility) if current_possibility_set && current_possibility_set.dependencies == dependencies current_possibility_set.possibilities.unshift(possibility) else possibility_sets.unshift(PossibilitySet(R, S).new(dependencies, [possibility])) current_possibility_set = possibility_sets.first end end possibility_sets end # Pushes a new {DependencyState}. # If the {#specification_provider} says to # {SpecificationProvider#allow_missing?} that particular requirement, and # there are no possibilities for that requirement, then `state` is not # pushed, and the vertex in {#activated} is removed, and we continue # resolving the remaining requirements. # @param [DependencyState] state # @return [void] private def handle_missing_or_push_dependency_state(state) if (req = state.requirement) && state.possibilities.empty? && allow_missing?(req) state.activated.detach_vertex_named(state.name.not_nil!) push_state_for_requirements(state.requirements.dup, false, state.activated) else states.push(state).tap { activated.tag(state) } end end private def check_possibility_set(vertex) payload = vertex.payload raise "BUG: unexpected payload: #{payload}" unless payload.is_a?(PossibilitySet) payload end end end end crystal-molinillo-0.2.0/src/molinillo/resolver.cr000066400000000000000000000026111401456133300221420ustar00rootroot00000000000000# require "./resolution" module Molinillo # This class encapsulates a dependency resolver. # The resolver is responsible for determining which set of dependencies to # activate, with feedback from the {#specification_provider} # # class Resolver(R, S) # @return [SpecificationProvider] the specification provider used # in the resolution process getter specification_provider : SpecificationProvider(R, S) # @return [UI] the UI module used to communicate back to the user # during the resolution process getter resolver_ui : UI # Initializes a new resolver. # @param [SpecificationProvider] specification_provider # see {#specification_provider} # @param [UI] resolver_ui # see {#resolver_ui} def initialize(@specification_provider, @resolver_ui) end # Resolves the requested dependencies into a {DependencyGraph}, # locking to the base dependency graph (if specified) # @param [Array] requested an array of 'requested' dependencies that the # {#specification_provider} can understand # @param [DependencyGraph,nil] base the base dependency graph to which # dependencies should be 'locked' def resolve(requested : Array(R), base = DependencyGraph(R, R).new) Resolution(R, S).new( specification_provider, resolver_ui, requested, base) .resolve end end end crystal-molinillo-0.2.0/src/molinillo/state.cr000066400000000000000000000051461401456133300214270ustar00rootroot00000000000000module Molinillo # A state that a {Resolution} can be in # @attr [String] name the name of the current requirement # @attr [Array] requirements currently unsatisfied requirements # @attr [DependencyGraph] activated the graph of activated dependencies # @attr [Object] requirement the current requirement # @attr [Object] possibilities the possibilities to satisfy the current requirement # @attr [Integer] depth the depth of the resolution # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name # @attr [Array] unused_unwind_options unwinds for previous conflicts that weren't explored class ResolutionState(R, S) property name : String? property requirements : Array(R) property activated : DependencyGraph(Resolver::Resolution::PossibilitySet(R, S)? | S, R) property requirement : R? property possibilities : Array(Resolver::Resolution::PossibilitySet(R, S)) property depth : Int32 property conflicts : Hash(String, Resolver::Resolution::Conflict(R, S)) property unused_unwind_options : Array(Resolver::Resolution::UnwindDetails(R, S)) def initialize(@name, @requirements, @activated, @requirement, @possibilities, @depth, @conflicts, @unused_unwind_options) end # Returns an empty resolution state # @return [ResolutionState] an empty state def self.empty new(nil, Array(R).new, DependencyGraph(Resolver::Resolution::PossibilitySet(R, S)? | S, R).new, nil, Array(Resolver::Resolution::PossibilitySet(R, S)).new, 0, Hash(String, Resolver::Resolution::Conflict(R, S)).new, Array(Resolver::Resolution::UnwindDetails(R, S)).new) end end # A state that encapsulates a set of {#requirements} with an {Array} of # possibilities class DependencyState(R, S) < ResolutionState(R, S) # Removes a possibility from `self` # @return [PossibilityState] a state with a single possibility, # the possibility that was removed from `self` def pop_possibility_state new_pos = if possibilities.size > 0 [possibilities.pop] else [] of Resolver::Resolution::PossibilitySet(R, S) end PossibilityState(R, S).new( name, requirements.dup, activated, requirement, new_pos, depth + 1, conflicts.dup, unused_unwind_options.dup ).tap do |state| state.activated.tag(state) end end end # A state that encapsulates a single possibility to fulfill the given # {#requirement} class PossibilityState(R, S) < ResolutionState(R, S) end end