molinillo-0.4.3/0000755000004100000410000000000012662434254013554 5ustar www-datawww-datamolinillo-0.4.3/spec/0000755000004100000410000000000012662434254014506 5ustar www-datawww-datamolinillo-0.4.3/spec/resolver_integration_specs/0000755000004100000410000000000012662434254022147 5ustar www-datawww-datamolinillo-0.4.3/spec/resolver_integration_specs/index_from_rubygems.rb0000755000004100000410000000417012662434254026550 0ustar www-datawww-data#!/usr/bin/env ruby require 'json' require 'open-uri' require 'set' GEMS = %w(rails capybara bundler github-linguist).freeze VERSION_PATTERN = /\A [0-9]+\.[0-9]+\.[0-9]+ (?# Number component) ([-][0-9a-z-]+(\.[0-9a-z-]+)*)? (?# Pre-release component) ([+][0-9a-z-]+(\.[0-9a-z-]+)*)? (?# Build component) \Z/xi def coerce_to_semver(version) return version if version.sub(/^(\S+\s+)/, '') =~ VERSION_PATTERN return "#{Regexp.last_match[1]}#{Regexp.last_match[2]}" if version =~ /^(\S+\s+)? (\d+\.\d+\.\d+) (?: \.\d+)*$/ix parts = version.split(/[\.-]/, 4) 4.times do |i| if parts[i] =~ /-?([a-zA-Z])/ parts << '0' until parts.size >= 3 parts[i].sub!(/-?([a-zA-Z]+)/, '') parts[i] = '0' if parts[i].empty? parts[3] = Regexp.last_match[1] + parts[i..-1].join('') end end semver = parts[0..2].join('.') semver.sub!(/([a-zA-Z])/, '-\1') semver += '-' + parts[-1] if parts.size > 3 semver end def coerce_dependencies_to_semver(deps) dependencies = {} deps.sort_by(&:first).each do |name, req| dependencies[name] = req.split(',').map { |r| coerce_to_semver(r) }.join(',') end dependencies end gems = Set.new(GEMS) downloaded_gems = Set.new specs = [] loop do size = gems.size (gems ^ downloaded_gems).each_slice(200) do |g| specs += JSON.load open("http://bundler.rubygems.org/api/v1/dependencies.json?gems=#{g.join(',')}") end downloaded_gems.merge(gems) gems.merge(specs.flat_map { |s| s['dependencies'].map(&:first) }) break if gems.size == size end specs.reject! { |s| s['platform'] != 'ruby' } specs.uniq! { |s| [s['name'], s['number']] } specs.sort_by! { |s| s['name'].downcase } specs = specs.group_by { |s| s['name'] }.values.map do |spec| [spec.first['name'], spec.flat_map do |s| { 'name' => s['name'], 'version' => coerce_to_semver(s['number']), 'dependencies' => coerce_dependencies_to_semver(s['dependencies']) } end.uniq { |s| s['version'] }.sort_by { |s| Gem::Version.new(s['version']) } ] end specs = Hash[specs] json = JSON.generate(specs) File.open("index/rubygems-#{Date.today}.json", 'w') { |f| f.write json } molinillo-0.4.3/spec/spec_helper/0000755000004100000410000000000012662434254016777 5ustar www-datawww-datamolinillo-0.4.3/spec/spec_helper/index.rb0000644000004100000410000000346512662434254020443 0ustar www-datawww-datamodule Molinillo class TestIndex attr_accessor :specs include SpecificationProvider def initialize(fixture_name) File.open(FIXTURE_INDEX_DIR + (fixture_name + '.json'), 'r') do |fixture| self.specs = JSON.load(fixture).reduce(Hash.new([])) do |specs_by_name, (name, versions)| specs_by_name.tap do |specs| specs[name] = versions.map { |s| TestSpecification.new s }.sort_by(&:version) end end end end def requirement_satisfied_by?(requirement, _activated, spec) case requirement when TestSpecification VersionKit::Dependency.new(requirement.name, requirement.version).satisfied_by?(spec.version) when VersionKit::Dependency requirement.satisfied_by?(spec.version) end end def search_for(dependency) @search_for ||= {} @search_for[dependency] ||= begin pre_release = dependency_pre_release?(dependency) specs[dependency.name].select do |spec| (pre_release ? true : !spec.version.pre_release?) && dependency.satisfied_by?(spec.version) end end end def name_for(dependency) dependency.name end def dependencies_for(dependency) dependency.dependencies end def sort_dependencies(dependencies, activated, conflicts) dependencies.sort_by do |d| [ activated.vertex_named(d.name).payload ? 0 : 1, dependency_pre_release?(d) ? 0 : 1, conflicts[d.name] ? 0 : 1, activated.vertex_named(d.name).payload ? 0 : search_for(d).count, ] end end private def dependency_pre_release?(dependency) dependency.requirement_list.requirements.any? do |r| VersionKit::Version.new(r.reference_version).pre_release? end end end end molinillo-0.4.3/spec/spec_helper/ui.rb0000644000004100000410000000017712662434254017746 0ustar www-datawww-datamodule Molinillo class TestUI include UI def output @output ||= File.open('/dev/null', 'w') end end end molinillo-0.4.3/spec/spec_helper/specification.rb0000644000004100000410000000127512662434254022151 0ustar www-datawww-datamodule Molinillo class TestSpecification attr_accessor :name, :version, :dependencies def initialize(hash) self.name = hash['name'] self.version = VersionKit::Version.new(hash['version']) self.dependencies = hash['dependencies'].map do |(name, requirement)| VersionKit::Dependency.new(name, requirement.split(',').map(&:chomp)) end end def ==(other) name == other.name && version == other.version && dependencies == other.dependencies end def to_s "#{name} (#{version})" end def inspect "#<#{self.class} name=#{name} version=#{version} dependencies=[#{dependencies.join(', ')}]>" end end end molinillo-0.4.3/spec/state_spec.rb0000644000004100000410000000156212662434254017171 0ustar www-datawww-datarequire File.expand_path('../spec_helper', __FILE__) module Molinillo describe ResolutionState do describe DependencyState do before do @state = described_class.new( 'name', %w(requirement1 requirement2 requirement3), DependencyGraph.new, 'requirement', %w(possibility1 possibility), 0, {} ) end it 'pops a possibility state' do possibility_state = @state.pop_possibility_state %w(name requirements activated requirement conflicts).each do |attr| expect(possibility_state.send(attr)).to eq(@state.send(attr)) end expect(possibility_state).to be_a(PossibilityState) expect(possibility_state.depth).to eq(@state.depth + 1) expect(possibility_state.possibilities).to eq(%w(possibility)) end end end end molinillo-0.4.3/spec/spec_helper.rb0000644000004100000410000000203212662434254017321 0ustar www-datawww-datarequire 'bundler/setup' # Set up coverage analysis #-----------------------------------------------------------------------------# if (ENV['CI'] || ENV['GENERATE_COVERAGE']) && RUBY_VERSION >= '2.0.0' && Bundler.current_ruby.mri? require 'simplecov' require 'codeclimate-test-reporter' if ENV['CI'] SimpleCov.formatter = CodeClimate::TestReporter::Formatter elsif ENV['GENERATE_COVERAGE'] SimpleCov.formatter = SimpleCov::Formatter::HTMLFormatter end SimpleCov.start do add_filter '/vendor/' add_filter '/lib/molinillo/modules/' end CodeClimate::TestReporter.start end # Set up #-----------------------------------------------------------------------------# require 'pathname' ROOT = Pathname.new(File.expand_path('../../', __FILE__)) $LOAD_PATH.unshift((ROOT + 'lib').to_s) $LOAD_PATH.unshift((ROOT + 'spec').to_s) require 'version_kit' require 'molinillo' RSpec.configure do |config| # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = '.rspec_status' end molinillo-0.4.3/spec/resolver_spec.rb0000644000004100000410000001165512662434254017716 0ustar www-datawww-datarequire File.expand_path('../spec_helper', __FILE__) require 'json' require 'pathname' module Molinillo FIXTURE_DIR = Pathname.new('spec/resolver_integration_specs') FIXTURE_INDEX_DIR = FIXTURE_DIR + 'index' FIXTURE_CASE_DIR = FIXTURE_DIR + 'case' class TestCase require File.expand_path('../spec_helper/index', __FILE__) require File.expand_path('../spec_helper/specification', __FILE__) require File.expand_path('../spec_helper/ui', __FILE__) attr_accessor :name, :requested, :base, :conflicts, :resolver, :result, :index def initialize(fixture_path) File.open(fixture_path) do |fixture| JSON.load(fixture).tap do |test_case| self.name = test_case['name'] self.index = TestIndex.new(test_case['index'] || 'awesome') self.requested = test_case['requested'].map do |(name, reqs)| VersionKit::Dependency.new name, reqs.split(',').map(&:chomp) end add_dependencies_to_graph = lambda do |graph, parent, hash| name = hash['name'] version = VersionKit::Version.new(hash['version']) dependency = index.specs[name].find { |s| s.version == version } node = 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 hash['dependencies'].each do |dep| add_dependencies_to_graph.call(graph, node, dep) end end self.result = test_case['resolved'].reduce(DependencyGraph.new) do |graph, r| graph.tap do |g| add_dependencies_to_graph.call(g, nil, r) end end self.base = test_case['base'].reduce(DependencyGraph.new) do |graph, r| graph.tap do |g| add_dependencies_to_graph.call(g, nil, r) end end self.conflicts = test_case['conflicts'].to_set end end self.resolver = Resolver.new(index, TestUI.new) end end describe Resolver do describe 'dependency resolution' do Dir.glob(FIXTURE_CASE_DIR + '**/*.json').map do |fixture| test_case = TestCase.new(fixture) it test_case.name do resolve = lambda { test_case.resolver.resolve(test_case.requested, test_case.base) } if test_case.conflicts.any? expect { resolve.call }.to raise_error do |error| expect(error).to be_a(ResolverError) names = case error when CircularDependencyError error.dependencies.map(&:name) when VersionConflict error.conflicts.keys end.to_set expect(names).to eq(test_case.conflicts) end else result = resolve.call pretty_dependencies = lambda do |dg| dg.vertices.values.map { |v| "#{v.name} (#{v.payload && v.payload.version})" } end expect(pretty_dependencies.call(result)).to contain_exactly(*pretty_dependencies.call(test_case.result)) expect(result).to eq(test_case.result) end end end end describe 'in general' do before do @resolver = described_class.new(TestIndex.new('awesome'), TestUI.new) end it 'can resolve a list of 0 requirements' do expect(@resolver.resolve([], DependencyGraph.new)).to eq(DependencyGraph.new) end it 'includes the source of a user-specified unsatisfied dependency' do expect do @resolver.resolve([VersionKit::Dependency.new('missing', '3.0')], DependencyGraph.new) end.to raise_error(VersionConflict, /required by `user-specified dependency`/) end it 'can handle when allow_missing? returns true for the only requirement' do dep = VersionKit::Dependency.new('missing', '3.0') allow(@resolver.specification_provider).to receive(:allow_missing?).with(dep).and_return(true) expect(@resolver.resolve([dep], DependencyGraph.new).to_a).to be_empty end it 'can handle when allow_missing? returns true for a nested requirement' do dep = VersionKit::Dependency.new('actionpack', '1.2.3') allow(@resolver.specification_provider).to receive(:allow_missing?). with(have_attributes(:name => 'activesupport')).and_return(true) allow(@resolver.specification_provider).to receive(:search_for). with(have_attributes(:name => 'activesupport')).and_return([]) allow(@resolver.specification_provider).to receive(:search_for). with(have_attributes(:name => 'actionpack')).and_call_original resolved = @resolver.resolve([dep], DependencyGraph.new) expect(resolved.map(&:payload).map(&:to_s)).to eq(['actionpack (1.2.3)']) end end end end molinillo-0.4.3/spec/dependency_graph_spec.rb0000644000004100000410000000516012662434254021346 0ustar www-datawww-datarequire File.expand_path('../spec_helper', __FILE__) module Molinillo describe DependencyGraph do describe 'in general' do before do @graph = described_class.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') end it 'returns root vertices by name' do expect(@graph.root_vertex_named('Root')).to eq(@root) end it 'returns vertices by name' do expect(@graph.vertex_named('Root')).to eq(@root) expect(@graph.vertex_named('Child')).to eq(@child) end it 'returns nil for non-existant root vertices' do expect(@graph.root_vertex_named('missing')).to be_nil end it 'returns nil for non-existant vertices' do expect(@graph.vertex_named('missing')).to be_nil end end describe 'detaching a node' do before do @graph = described_class.new end it 'detaches a root vertex without successors' do root = @graph.add_vertex('root', 'root', true) @graph.detach_vertex_named(root.name) expect(@graph.vertex_named(root.name)).to be_nil expect(@graph.vertices).to be_empty end it 'detaches a root vertex with successors' do root = @graph.add_vertex('root', 'root', true) child = @graph.add_child_vertex('child', 'child', %w(root), 'child') @graph.detach_vertex_named(root.name) expect(@graph.vertex_named(root.name)).to be_nil expect(@graph.vertex_named(child.name)).to be_nil expect(@graph.vertices).to be_empty end it 'detaches a root vertex with successors with other parents' do 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) expect(@graph.vertex_named(root.name)).to be_nil expect(@graph.vertex_named(child.name)).to eq(child) expect(child.predecessors).to eq([root2]) expect(@graph.vertices.count).to eq(2) end it 'detaches a vertex with predecessors' do parent = @graph.add_vertex('parent', 'parent', true) child = @graph.add_child_vertex('child', 'child', %w(parent), 'child') @graph.detach_vertex_named(child.name) expect(@graph.vertex_named(child.name)).to be_nil expect(@graph.vertices).to eq(parent.name => parent) expect(parent.outgoing_edges).to be_empty end end end end molinillo-0.4.3/lib/0000755000004100000410000000000012662434254014322 5ustar www-datawww-datamolinillo-0.4.3/lib/molinillo/0000755000004100000410000000000012662434254016320 5ustar www-datawww-datamolinillo-0.4.3/lib/molinillo/errors.rb0000644000004100000410000000537012662434254020166 0ustar www-datawww-data# frozen_string_literal: true module Molinillo # An error that occurred during the resolution process class ResolverError < StandardError; 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 attr_accessor :dependency # @return [Array] the specifications that depended upon {#dependency} attr_accessor :required_by # 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 = []) @dependency = dependency @required_by = required_by 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 < ResolverError # [Set] the dependencies responsible for causing the error attr_reader :dependencies # Initializes a new error with the given circular vertices. # @param [Array] nodes the nodes in the dependency # that caused the error def initialize(nodes) super "There is a circular dependency between #{nodes.map(&:name).join(' and ')}" @dependencies = nodes.map(&:payload).to_set end end # An error caused by conflicts in version class VersionConflict < ResolverError # @return [{String => Resolution::Conflict}] the conflicts that caused # resolution to fail attr_reader :conflicts # Initializes a new error with the given version conflicts. # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} def initialize(conflicts) pairs = [] conflicts.values.flatten.map(&:requirements).flatten.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 end end end molinillo-0.4.3/lib/molinillo/resolver.rb0000644000004100000410000000302712662434254020510 0ustar www-datawww-data# frozen_string_literal: true require 'molinillo/dependency_graph' 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 require 'molinillo/resolution' # @return [SpecificationProvider] the specification provider used # in the resolution process attr_reader :specification_provider # @return [UI] the UI module used to communicate back to the user # during the resolution process attr_reader :resolver_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) @specification_provider = specification_provider @resolver_ui = 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, base = DependencyGraph.new) Resolution.new(specification_provider, resolver_ui, requested, base). resolve end end end molinillo-0.4.3/lib/molinillo/modules/0000755000004100000410000000000012662434254017770 5ustar www-datawww-datamolinillo-0.4.3/lib/molinillo/modules/ui.rb0000644000004100000410000000322312662434254020732 0ustar www-datawww-data# frozen_string_literal: true module 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) output.puts debug_info.split("\n").map { |s| ' ' * depth + s } end end # Whether or not debug messages should be printed. # By default, whether or not the `MOLINILLO_DEBUG` environment variable is # set. # # @return [Boolean] def debug? return @debug_mode if defined?(@debug_mode) @debug_mode = ENV['MOLINILLO_DEBUG'] end end end molinillo-0.4.3/lib/molinillo/modules/specification_provider.rb0000644000004100000410000000725512662434254025060 0ustar www-datawww-data# frozen_string_literal: true module 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 # 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) [] 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) [] 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, activated, spec) 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) 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, activated, 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) false end end end molinillo-0.4.3/lib/molinillo/resolution.rb0000644000004100000410000004274412662434254021063 0ustar www-datawww-data# frozen_string_literal: true module Molinillo class Resolver # A specific resolution from a given {Resolver} class Resolution # 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 the spec 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. Conflict = Struct.new( :requirement, :requirements, :existing, :possibility, :locked_requirement, :requirement_trees, :activated_by_name ) # @return [SpecificationProvider] the provider that knows about # dependencies, requirements, specifications, versions, etc. attr_reader :specification_provider # @return [UI] the UI that knows how to communicate feedback about the # resolution process back to the user attr_reader :resolver_ui # @return [DependencyGraph] the base dependency graph to which # dependencies should be 'locked' attr_reader :base # @return [Array] the dependencies that were explicitly required attr_reader :original_requested # 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 = [] @iteration_counter = 0 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 state break unless state.requirements.any? || state.requirement indicate_progress if state.respond_to?(:pop_possibility_state) # DependencyState debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } state.pop_possibility_state.tap { |s| states.push(s) if s } end process_topmost_state end activated.freeze ensure end_resolution end # @return [Integer] the number of resolver iterations in between calls to # {#resolver_ui}'s {UI#indicate_progress} method attr_accessor :iteration_rate private :iteration_rate # @return [Time] the time at which resolution began attr_accessor :started_at private :started_at # @return [Array] the stack of states for the resolution attr_accessor :states private :states private # Sets up the resolution process # @return [void] def start_resolution @started_at = Time.now handle_missing_or_push_dependency_state(initial_state) debug { "Starting resolution (#{@started_at})" } resolver_ui.before_resolution end # Ends the resolution process # @return [void] def end_resolution resolver_ui.after_resolution debug do "Finished resolution (#{@iteration_counter} steps) " \ "(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})" end debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state end require 'molinillo/state' require 'molinillo/modules/specification_provider' ResolutionState.new.members.each do |member| define_method member do |*args, &block| current_state = state || ResolutionState.empty current_state.send(member, *args, &block) end end SpecificationProvider.instance_methods(false).each do |instance_method| define_method instance_method do |*args, &block| begin specification_provider.send(instance_method, *args, &block) rescue NoSuchDependencyError => error if state 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 end end end # Processes the topmost available {RequirementState} on the stack # @return [void] def process_topmost_state if possibility attempt_to_activate else create_conflict if state.is_a? PossibilityState unwind_for_conflict until possibility && state.is_a?(DependencyState) end end # @return [Object] the current possibility that the resolution is trying # to activate def possibility possibilities.last end # @return [RequirementState] the current state the resolution is # operating upon def state states.last end # Creates the initial state for the resolution, based upon the # {#requested} dependencies # @return [DependencyState] the initial state for the resolution def initial_state graph = DependencyGraph.new.tap do |dg| original_requested.each { |r| dg.add_vertex(name_for(r), nil, true).tap { |v| v.explicit_requirements << r } } end requirements = sort_dependencies(original_requested, graph, {}) initial_requirement = requirements.shift DependencyState.new( initial_requirement && name_for(initial_requirement), requirements, graph, initial_requirement, initial_requirement && search_for(initial_requirement), 0, {} ) end # Unwinds the states stack because a conflict has been encountered # @return [void] def unwind_for_conflict debug(depth) { "Unwinding for conflict: #{requirement}" } conflicts.tap do |c| states.slice!((state_index_for_unwind + 1)..-1) raise VersionConflict.new(c) unless state state.conflicts = c end end # @return [Integer] The index to which the resolution should unwind in the # case of conflict. def state_index_for_unwind current_requirement = requirement existing_requirement = requirement_for_existing_name(name) until current_requirement.nil? current_state = find_state_for(current_requirement) return states.index(current_state) if state_any?(current_state) current_requirement = parent_of(current_requirement) end until existing_requirement.nil? existing_state = find_state_for(existing_requirement) return states.index(existing_state) if state_any?(existing_state) existing_requirement = parent_of(existing_requirement) end -1 end # @return [Object] the requirement that led to `requirement` being added # to the list of requirements. def parent_of(requirement) return nil unless requirement seen = false state = states.reverse_each.find do |s| seen ||= s.requirement == requirement || s.requirements.include?(requirement) seen && s.requirement != requirement && !s.requirements.include?(requirement) end state && state.requirement end # @return [Object] the requirement that led to a version of a possibility # with the given name being activated. def requirement_for_existing_name(name) return nil unless activated.vertex_named(name).payload states.reverse_each.find { |s| !s.activated.vertex_named(name).payload }.requirement end # @return [ResolutionState] the state whose `requirement` is the given # `requirement`. def find_state_for(requirement) return nil unless requirement states.reverse_each.find { |i| requirement == i.requirement && i.is_a?(DependencyState) } end # @return [Boolean] whether or not the given state has any possibilities # left. def state_any?(state) state && state.possibilities.any? end # @return [Conflict] a {Conflict} that reflects the failure to activate # the {#possibility} in conjunction with the current {#state} def create_conflict vertex = activated.vertex_named(name) requirements = { name_for_explicit_dependency_source => vertex.explicit_requirements, name_for_locking_dependency_source => Array(locked_requirement_named(name)), } vertex.incoming_edges.each { |edge| (requirements[edge.origin.payload] ||= []).unshift(edge.requirement) } conflicts[name] = Conflict.new( requirement, Hash[requirements.select { |_, r| !r.empty? }], vertex.payload, possibility, locked_requirement_named(name), requirement_trees, Hash[activated.map { |v| [v.name, v.payload] }.select(&:last)] ) end # @return [Array>] The different requirement # trees that led to every requirement for the current spec. def requirement_trees vertex = activated.vertex_named(name) vertex.requirements.map { |r| requirement_tree_for(r) } end # @return [Array] the list of requirements that led to # `requirement` being required. def requirement_tree_for(requirement) tree = [] while requirement tree.unshift(requirement) requirement = parent_of(requirement) end tree end # 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 >= @progress_rate 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] def debug(depth = 0, &block) resolver_ui.debug(depth, &block) end # Attempts to activate the current {#possibility} # @return [void] def attempt_to_activate debug(depth) { 'Attempting to activate ' + possibility.to_s } existing_node = activated.vertex_named(name) if existing_node.payload debug(depth) { "Found existing spec (#{existing_node.payload})" } attempt_to_activate_existing_spec(existing_node) else attempt_to_activate_new_spec end end # Attempts to activate the current {#possibility} (given that it has # already been activated) # @return [void] def attempt_to_activate_existing_spec(existing_node) existing_spec = existing_node.payload if requirement_satisfied_by?(requirement, activated, existing_spec) new_requirements = requirements.dup push_state_for_requirements(new_requirements, false) else return if attempt_to_swap_possibility create_conflict debug(depth) { "Unsatisfied by existing spec (#{existing_node.payload})" } unwind_for_conflict end end # Attempts to swp the current {#possibility} with the already-activated # spec with the given name # @return [Boolean] Whether the possibility was swapped into {#activated} def attempt_to_swap_possibility swapped = activated.dup vertex = swapped.vertex_named(name) vertex.payload = possibility return unless vertex.requirements. all? { |r| requirement_satisfied_by?(r, swapped, possibility) } return unless new_spec_satisfied? actual_vertex = activated.vertex_named(name) actual_vertex.payload = possibility fixup_swapped_children(actual_vertex) activate_spec end # Ensures there are no orphaned successors to the given {vertex}. # @param [DependencyGraph::Vertex] vertex the vertex to fix up. # @return [void] def fixup_swapped_children(vertex) payload = vertex.payload dep_names = dependencies_for(payload).map(&method(:name_for)) vertex.successors.each do |succ| if !dep_names.include?(succ.name) && !succ.root? && succ.predecessors.to_a == [vertex] debug(depth) { "Removing orphaned spec #{succ.name} after swapping #{name}" } activated.detach_vertex_named(succ.name) requirements.delete_if { |r| name_for(r) == succ.name } end end end # Attempts to activate the current {#possibility} (given that it hasn't # already been activated) # @return [void] def attempt_to_activate_new_spec if new_spec_satisfied? activate_spec else create_conflict unwind_for_conflict end end # @return [Boolean] whether the current spec is satisfied as a new # possibility. def new_spec_satisfied? locked_requirement = locked_requirement_named(name) requested_spec_satisfied = requirement_satisfied_by?(requirement, activated, possibility) locked_spec_satisfied = !locked_requirement || requirement_satisfied_by?(locked_requirement, activated, possibility) debug(depth) { 'Unsatisfied by requested spec' } unless requested_spec_satisfied debug(depth) { 'Unsatisfied by locked spec' } unless locked_spec_satisfied requested_spec_satisfied && locked_spec_satisfied 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} 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] def activate_spec conflicts.delete(name) debug(depth) { 'Activated ' + name + ' at ' + possibility.to_s } vertex = activated.vertex_named(name) vertex.payload = possibility require_nested_dependencies_for(possibility) end # Requires the dependencies that the recently activated spec has # @param [Object] activated_spec the specification that has just been # activated # @return [void] def require_nested_dependencies_for(activated_spec) nested_dependencies = dependencies_for(activated_spec) debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } nested_dependencies.each { |d| activated.add_child_vertex(name_for(d), nil, [name_for(activated_spec)], d) } push_state_for_requirements(requirements + nested_dependencies, nested_dependencies.size > 0) end # Pushes a new {DependencyState} that encapsulates both existing and new # requirements # @param [Array] new_requirements # @return [void] def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated.dup) new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort new_requirement = new_requirements.shift new_name = new_requirement ? name_for(new_requirement) : '' possibilities = new_requirement ? search_for(new_requirement) : [] handle_missing_or_push_dependency_state DependencyState.new( new_name, new_requirements, new_activated, new_requirement, possibilities, depth, conflicts.dup ) 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 node in {#activated} is removed, and we continue # resolving the remaining requirements. # @param [DependencyState] state # @return [void] def handle_missing_or_push_dependency_state(state) if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement) state.activated.detach_vertex_named(state.name) push_state_for_requirements(state.requirements.dup, false, state.activated) else states.push state end end end end end molinillo-0.4.3/lib/molinillo/dependency_graph.rb0000644000004100000410000002225412662434254022151 0ustar www-datawww-data# frozen_string_literal: true require 'set' require 'tsort' module Molinillo # A directed acyclic graph that is tuned to hold named dependencies class DependencyGraph include Enumerable # Enumerates through the vertices of the graph. # @return [Array] The graph's vertices. def each vertices.values.each { |v| yield v } end include TSort # @visibility private alias_method :tsort_each_node, :each # @visibility private def tsort_each_child(vertex, &block) vertex.successors.each(&block) end # Topologically sorts the given vertices. # @param [Enumerable] vertices the vertices to be sorted, which must # all belong to the same graph. # @return [Array] The sorted vertices. def self.tsort(vertices) TSort.tsort( lambda { |b| vertices.each(&b) }, lambda { |v, &b| (v.successors & vertices).each(&b) } ) end # 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 Edge = Struct.new(:origin, :destination, :requirement) # @return [{String => Vertex}] the vertices of the dependency graph, keyed # by {Vertex#name} attr_reader :vertices # Initializes an empty dependency graph def initialize @vertices = {} end # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices} # are properly copied. # @param [DependencyGraph] other the graph to copy. def initialize_copy(other) super @vertices = {} traverse = lambda do |new_v, old_v| return if new_v.outgoing_edges.size == old_v.outgoing_edges.size old_v.outgoing_edges.each do |edge| destination = add_vertex(edge.destination.name, edge.destination.payload) add_edge_no_circular(new_v, destination, edge.requirement) traverse.call(destination, edge.destination) end end other.vertices.each do |name, vertex| new_vertex = add_vertex(name, vertex.payload, vertex.root?) new_vertex.explicit_requirements.replace(vertex.explicit_requirements) traverse.call(new_vertex, vertex) end end # @return [String] a string suitable for debugging def inspect "#{self.class}:#{vertices.values.inspect}" end # @return [Boolean] whether the two dependency graphs are equal, determined # by a recursive traversal of each {#root_vertices} and its # {Vertex#successors} def ==(other) return false unless other vertices.each do |name, vertex| other_vertex = other.vertex_named(name) return false unless other_vertex return false unless other_vertex.successors.map(&:name).to_set == vertex.successors.map(&:name).to_set 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, payload, parent_names, requirement) vertex = add_vertex(name, payload) parent_names.each do |parent_name| unless parent_name vertex.root = true next end parent_node = vertex_named(parent_name) add_edge(parent_node, 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, payload, root = false) vertex = vertices[name] ||= Vertex.new(name, payload) vertex.payload ||= payload vertex.root ||= root vertex 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 [void] def detach_vertex_named(name) return unless vertex = vertices.delete(name) vertex.outgoing_edges.each do |e| v = e.destination v.incoming_edges.delete(e) detach_vertex_named(v.name) unless v.root? || v.predecessors.any? end vertex.incoming_edges.each do |e| v = e.origin v.outgoing_edges.delete(e) end end # @param [String] name # @return [Vertex,nil] the vertex with the given name def vertex_named(name) vertices[name] end # @param [String] name # @return [Vertex,nil] the root vertex with the given name def root_vertex_named(name) 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, destination, requirement) if destination.path_to?(origin) raise CircularDependencyError.new([origin, destination]) end add_edge_no_circular(origin, destination, requirement) end private # Adds a new {Edge} to the dependency graph without checking for # circularity. def add_edge_no_circular(origin, destination, requirement) edge = Edge.new(origin, destination, requirement) origin.outgoing_edges << edge destination.incoming_edges << edge edge end # A vertex in a {DependencyGraph} that encapsulates a {#name} and a # {#payload} class Vertex # @return [String] the name of the vertex attr_accessor :name # @return [Object] the payload the vertex holds attr_accessor :payload # @return [Arrary] the explicit requirements that required # this vertex attr_reader :explicit_requirements # @return [Boolean] whether the vertex is considered a root vertex attr_accessor :root alias_method :root?, :root # Initializes a vertex with the given name and payload. # @param [String] name see {#name} # @param [Object] payload see {#payload} def initialize(name, payload) @name = name @payload = payload @explicit_requirements = [] @outgoing_edges = [] @incoming_edges = [] end # @return [Array] all of the requirements that required # this vertex def requirements incoming_edges.map(&:requirement) + explicit_requirements end # @return [Array] the edges of {#graph} that have `self` as their # {Edge#origin} attr_accessor :outgoing_edges # @return [Array] the edges of {#graph} that have `self` as their # {Edge#destination} attr_accessor :incoming_edges # @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} where `self` is a # {#descendent?} def recursive_predecessors vertices = predecessors vertices += vertices.map(&:recursive_predecessors).flatten(1) vertices.uniq! vertices end # @return [Array] the vertices of {#graph} that have an edge with # `self` as their {Edge#origin} def successors outgoing_edges.map(&:destination) end # @return [Array] the vertices of {#graph} where `self` is an # {#ancestor?} def recursive_successors vertices = successors vertices += vertices.map(&:recursive_successors).flatten(1) vertices.uniq! vertices end # @return [String] a string suitable for debugging def inspect "#{self.class}:#{name}(#{payload.inspect})" end # @return [Boolean] whether the two vertices are equal, determined # by a recursive traversal of each {Vertex#successors} def ==(other) shallow_eql?(other) && successors.to_set == other.successors.to_set end # @param [Vertex] other the other vertex to compare to # @return [Boolean] whether the two vertices are equal, determined # solely by {#name} and {#payload} equality def shallow_eql?(other) other && name == other.name && payload == other.payload end alias_method :eql?, :== # @return [Fixnum] a hash for the vertex based upon its {#name} def hash name.hash end # 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) equal?(other) || successors.any? { |v| v.path_to?(other) } end alias_method :descendent?, :path_to? # Is there a path from `other` to `self` following edges in the # dependency graph? # @return true iff there is a path following edges within this {#graph} def ancestor?(other) other.path_to?(self) end alias_method :is_reachable_from?, :ancestor? end end end molinillo-0.4.3/lib/molinillo/gem_metadata.rb0000644000004100000410000000015412662434254021255 0ustar www-datawww-data# frozen_string_literal: true module Molinillo # The version of Molinillo. VERSION = '0.4.3'.freeze end molinillo-0.4.3/lib/molinillo/state.rb0000644000004100000410000000304712662434254017771 0ustar www-datawww-data# frozen_string_literal: true module 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 [Set] conflicts unresolved conflicts ResolutionState = Struct.new( :name, :requirements, :activated, :requirement, :possibilities, :depth, :conflicts ) class ResolutionState # Returns an empty resolution state # @return [ResolutionState] an empty state def self.empty new(nil, [], DependencyGraph.new, nil, nil, 0, Set.new) end end # A state that encapsulates a set of {#requirements} with an {Array} of # possibilities class DependencyState < ResolutionState # Removes a possibility from `self` # @return [PossibilityState] a state with a single possibility, # the possibility that was removed from `self` def pop_possibility_state PossibilityState.new( name, requirements.dup, activated.dup, requirement, [possibilities.pop], depth + 1, conflicts.dup ) end end # A state that encapsulates a single possibility to fulfill the given # {#requirement} class PossibilityState < ResolutionState end end molinillo-0.4.3/lib/molinillo.rb0000644000004100000410000000043112662434254016643 0ustar www-datawww-data# frozen_string_literal: true require 'molinillo/gem_metadata' require 'molinillo/errors' require 'molinillo/resolver' require 'molinillo/modules/ui' require 'molinillo/modules/specification_provider' # Molinillo is a generic dependency resolution algorithm. module Molinillo end molinillo-0.4.3/metadata.yml0000644000004100000410000000460112662434254016060 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: molinillo version: !ruby/object:Gem::Version version: 0.4.3 platform: ruby authors: - Samuel E. Giddins autorequire: bindir: bin cert_chain: [] date: 2016-02-18 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: bundler requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.5' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version version: '1.5' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' description: email: - segiddins@segiddins.me executables: [] extensions: [] extra_rdoc_files: [] files: - LICENSE - README.md - lib/molinillo.rb - lib/molinillo/dependency_graph.rb - lib/molinillo/errors.rb - lib/molinillo/gem_metadata.rb - lib/molinillo/modules/specification_provider.rb - lib/molinillo/modules/ui.rb - lib/molinillo/resolution.rb - lib/molinillo/resolver.rb - lib/molinillo/state.rb - spec/dependency_graph_spec.rb - spec/resolver_integration_specs/index_from_rubygems.rb - spec/resolver_spec.rb - spec/spec_helper.rb - spec/spec_helper/index.rb - spec/spec_helper/specification.rb - spec/spec_helper/ui.rb - spec/state_spec.rb homepage: https://github.com/CocoaPods/Molinillo licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.5.2 signing_key: specification_version: 4 summary: Provides support for dependency resolution test_files: - spec/dependency_graph_spec.rb - spec/resolver_integration_specs/index_from_rubygems.rb - spec/resolver_spec.rb - spec/spec_helper/index.rb - spec/spec_helper/specification.rb - spec/spec_helper/ui.rb - spec/spec_helper.rb - spec/state_spec.rb has_rdoc: molinillo-0.4.3/LICENSE0000644000004100000410000000215512662434254014564 0ustar www-datawww-dataThis project is licensed under the MIT license. 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. molinillo-0.4.3/README.md0000644000004100000410000000264112662434254015036 0ustar www-datawww-data# Molinillo [![Build Status](https://img.shields.io/travis/CocoaPods/Molinillo/master.svg?style=flat)](https://travis-ci.org/CocoaPods/Molinillo) [![Coverage](https://img.shields.io/codeclimate/coverage/github/CocoaPods/Molinillo.svg?style=flat)](https://codeclimate.com/github/CocoaPods/Molinillo) [![Code Climate](https://img.shields.io/codeclimate/github/CocoaPods/Molinillo.svg?style=flat)](https://codeclimate.com/github/CocoaPods/Molinillo) A generic dependency-resolution implementation. ## Installation Add this line to your application's Gemfile: ```ruby gem 'molinillo', :git => 'https://github.com/CocoaPods/Molinillo' ``` And then execute: ```bash $ bundle install ``` Or install it yourself as: ```bash $ gem install molinillo ``` ## Usage See the [ARCHITECTURE](ARCHITECTURE.md) file for an overview and look at the test suite for example usage. Better documentation and examples are forthcoming. ## 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 pull request ## The Name [Molinillo](http://en.wikipedia.org/wiki/Molinillo_(whisk)) is a special whisk used in Mexico in the preparation of beverages such as hot chocolate. Much like a dependency resolver, a molinillo helps take a list of ingredients and turn it into a delicious concoction!