diff options
Diffstat (limited to 'lib/bundler/spec_set.rb')
| -rw-r--r-- | lib/bundler/spec_set.rb | 411 |
1 files changed, 312 insertions, 99 deletions
diff --git a/lib/bundler/spec_set.rb b/lib/bundler/spec_set.rb index 7cd3021997..ae5e5cbaa9 100644 --- a/lib/bundler/spec_set.rb +++ b/lib/bundler/spec_set.rb @@ -1,67 +1,113 @@ # frozen_string_literal: true -require "tsort" -require "forwardable" -require "set" +require_relative "vendored_tsort" module Bundler class SpecSet - extend Forwardable - include TSort, Enumerable - - def_delegators :@specs, :<<, :length, :add, :remove, :size, :empty? - def_delegators :sorted, :each + include Enumerable + include TSort def initialize(specs) @specs = specs end - def for(dependencies, skip = [], check = false, match_current_platform = false, raise_on_missing = true) - handled = Set.new - deps = dependencies.dup - specs = [] - skip += ["bundler"] + def for(dependencies, platforms = [nil], legacy_platforms = [nil], skips: []) + if [true, false].include?(platforms) + Bundler::SharedHelpers.feature_removed! \ + "SpecSet#for received a `check` parameter, but that's no longer used and deprecated. " \ + "SpecSet#for always implicitly performs validation. Please remove this parameter" + end - loop do - break unless dep = deps.shift - next if !handled.add?(dep) || skip.include?(dep.name) + materialize_dependencies(dependencies, platforms, skips: skips) - if spec = spec_for_dependency(dep, match_current_platform) - specs << spec + @materializations.flat_map(&:specs).uniq + end - spec.dependencies.each do |d| - next if d.type == :development - d = DepProxy.new(d, dep.__platform) unless match_current_platform - deps << d - end - elsif check - return false - elsif raise_on_missing - raise "Unable to find a spec satisfying #{dep} in the set. Perhaps the lockfile is corrupted?" + def normalize_platforms!(deps, platforms) + remove_invalid_platforms!(deps, platforms) + add_extra_platforms!(platforms) + + platforms.map! do |platform| + next platform if platform == Gem::Platform::RUBY + + begin + Integer(platform.version) + rescue ArgumentError, TypeError + next platform end + + less_specific_platform = Gem::Platform.new([platform.cpu, platform.os, nil]) + next platform if incomplete_for_platform?(deps, less_specific_platform) + + less_specific_platform + end.uniq! + end + + def add_originally_invalid_platforms!(platforms, originally_invalid_platforms) + originally_invalid_platforms.each do |originally_invalid_platform| + platforms << originally_invalid_platform if complete_platform(originally_invalid_platform) + end + end + + def remove_invalid_platforms!(deps, platforms, skips: []) + invalid_platforms = [] + + platforms.reject! do |platform| + next false if skips.include?(platform) + + invalid = incomplete_for_platform?(deps, platform) + invalid_platforms << platform if invalid + invalid end - if spec = lookup["bundler"].first - specs << spec + invalid_platforms + end + + def add_extra_platforms!(platforms) + if @specs.empty? + platforms.concat([Gem::Platform::RUBY]).uniq + return + end + + new_platforms = all_platforms.select do |platform| + next if platforms.include?(platform) + next unless Gem::Platform.generic(platform) == Gem::Platform::RUBY + + complete_platform(platform) end + return if new_platforms.empty? - check ? true : SpecSet.new(specs) + platforms.concat(new_platforms) + return if new_platforms.include?(Bundler.local_platform) + + less_specific_platform = new_platforms.find {|platform| platform != Gem::Platform::RUBY && Bundler.local_platform === platform && platform === Bundler.local_platform } + platforms.delete(Bundler.local_platform) if less_specific_platform end - def valid_for?(deps) - self.for(deps, [], true) + def validate_deps(s) + s.runtime_dependencies.each do |dep| + next if dep.name == "bundler" + + return :missing unless names.include?(dep.name) + return :invalid if none? {|spec| dep.matches_spec?(spec) } + end + + :valid end def [](key) key = key.name if key.respond_to?(:name) - lookup[key].reverse + lookup[key]&.reverse || [] end def []=(key, value) - @specs << value - @lookup = nil - @sorted = nil - value + delete_by_name(key) + + add_spec(value) + end + + def delete(specs) + Array(specs).each {|spec| remove_spec(spec) } end def sort! @@ -76,86 +122,227 @@ module Bundler lookup.dup end - def materialize(deps, missing_specs = nil) - materialized = self.for(deps, [], false, true, !missing_specs).to_a - deps = materialized.map(&:name).uniq - materialized.map! do |s| - next s unless s.is_a?(LazySpecification) - s.source.dependency_names = deps if s.source.respond_to?(:dependency_names=) - spec = s.__materialize__ - unless spec - unless missing_specs - raise GemNotFound, "Could not find #{s.full_name} in any of the sources" - end - missing_specs << s - end - spec - end - SpecSet.new(missing_specs ? materialized.compact : materialized) + def materialize(deps) + materialize_dependencies(deps) + + SpecSet.new(materialized_specs) end # Materialize for all the specs in the spec set, regardless of what platform they're for - # This is in contrast to how for does platform filtering (and specifically different from how `materialize` calls `for` only for the current platform) # @return [Array<Gem::Specification>] def materialized_for_all_platforms - names = @specs.map(&:name).uniq @specs.map do |s| next s unless s.is_a?(LazySpecification) - s.source.dependency_names = names if s.source.respond_to?(:dependency_names=) - spec = s.__materialize__ + spec = s.materialize_for_cache raise GemNotFound, "Could not find #{s.full_name} in any of the sources" unless spec spec end end - def merge(set) - arr = sorted.dup - set.each do |set_spec| - full_name = set_spec.full_name - next if arr.any? {|spec| spec.full_name == full_name } - arr << set_spec - end - SpecSet.new(arr) + def incomplete_for_platform?(deps, platform) + incomplete_specs_for_platform(deps, platform).any? + end + + def incomplete_specs_for_platform(deps, platform) + return [] if @specs.empty? + + validation_set = self.class.new(@specs) + validation_set.for(deps, [platform]) + validation_set.incomplete_specs + end + + def missing_specs_for(deps) + materialize_dependencies(deps) + + missing_specs + end + + def missing_specs + @materializations.flat_map(&:completely_missing_specs) + end + + def partially_missing_specs + @materializations.flat_map(&:partially_missing_specs) + end + + def incomplete_specs + @materializations.flat_map(&:incomplete_specs) + end + + def insecurely_materialized_specs + materialized_specs.select(&:insecurely_materialized?) + end + + def -(other) + SharedHelpers.feature_removed! "SpecSet#- has been removed with no replacement" end def find_by_name_and_platform(name, platform) - @specs.detect {|spec| spec.name == name && spec.match_platform(platform) } + lookup[name]&.detect {|spec| spec.installable_on_platform?(platform) } + end + + def specs_with_additional_variants_from(other) + sorted | additional_variants_from(other) + end + + def delete_by_name(name) + @specs.reject! {|spec| spec.name == name } + @sorted&.reject! {|spec| spec.name == name } + return if @lookup.nil? + + @lookup[name] = nil + end + + def version_for(name) + exemplary_spec(name)&.version end def what_required(spec) - unless req = find {|s| s.dependencies.any? {|d| d.type == :runtime && d.name == spec.name } } + unless req = find {|s| s.runtime_dependencies.any? {|d| d.name == spec.name } } return [spec] end what_required(req) << spec end - private + def <<(spec) + SharedHelpers.feature_removed! "SpecSet#<< has been removed with no replacement" + end - def sorted - rake = @specs.find {|s| s.name == "rake" } - begin - @sorted ||= ([rake] + tsort).compact.uniq - rescue TSort::Cyclic => error - cgems = extract_circular_gems(error) - raise CyclicDependencyError, "Your bundle requires gems that depend" \ - " on each other, creating an infinite loop. Please remove either" \ - " gem '#{cgems[1]}' or gem '#{cgems[0]}' and try again." + def length + @specs.length + end + + def size + @specs.size + end + + def empty? + @specs.empty? + end + + def each(&b) + sorted.each(&b) + end + + def names + lookup.keys + end + + def valid?(s) + s.matches_current_metadata? && valid_dependencies?(s) + end + + def to_s + map(&:full_name).to_s + end + + private + + def materialize_dependencies(dependencies, platforms = [nil], skips: []) + handled = ["bundler"].product(platforms).map {|k| [k, true] }.to_h + deps = dependencies.product(platforms) + @materializations = [] + + loop do + break unless dep = deps.shift + + dependency = dep[0] + platform = dep[1] + name = dependency.name + + key = [name, platform] + next if handled.key?(key) + + handled[key] = true + + materialization = Materialization.new(dependency, platform, candidates: lookup[name]) + + deps.concat(materialization.dependencies) if materialization.complete? + + @materializations << materialization unless skips.include?(name) end + + @materializations end - def extract_circular_gems(error) - if Bundler.current_ruby.mri? && Bundler.current_ruby.on_19? - error.message.scan(/(\w+) \([^)]/).flatten - else - error.message.scan(/@name="(.*?)"/).flatten + def materialized_specs + @materializations.filter_map(&:materialized_spec) + end + + def complete_platform(platform) + new_specs = [] + + valid_platform = lookup.all? do |_, specs| + spec = specs.first + # The matching candidates returned by source.specs.search are remote + # specs that do not carry the override list themselves. Borrow it from + # the LazySpec we are validating so platform-variant validation honors + # the same overrides the install/resolve path already applies. + overrides = spec.is_a?(LazySpecification) ? Array(spec.overrides) : [] + matching_specs = spec.source.specs.search([spec.name, spec.version]) + platform_spec = MatchPlatform.select_best_platform_match(matching_specs, platform).find do |s| + s.matches_current_metadata_with_overrides?(overrides) && valid_dependencies?(s) + end + + if platform_spec + unless specs.include?(platform_spec) + new_lazy = LazySpecification.from_spec(platform_spec) + # Carry the overrides forward so a follow-up complete_platform + # call that picks this synthesized variant as its exemplar still + # honors the user's override list. + new_lazy.overrides = overrides if overrides.any? + new_specs << new_lazy + end + true + else + false + end + end + + if valid_platform && new_specs.any? + new_specs.each {|spec| add_spec(spec) } end + + valid_platform + end + + def all_platforms + @specs.flat_map {|spec| spec.source.specs.search([spec.name, spec.version]).map(&:platform) }.uniq + end + + def additional_variants_from(other) + other.select do |other_spec| + spec = exemplary_spec(other_spec.name) + next unless spec + + selected = spec.version == other_spec.version && valid_dependencies?(other_spec) + other_spec.source = spec.source if selected + selected + end + end + + def valid_dependencies?(s) + validate_deps(s) == :valid + end + + def sorted + @sorted ||= ([lookup["rake"]&.first] + tsort).compact.uniq + rescue TSort::Cyclic => error + cgems = extract_circular_gems(error) + raise CyclicDependencyError, "Your bundle requires gems that depend" \ + " on each other, creating an infinite loop. Please remove either" \ + " gem '#{cgems[0]}' or gem '#{cgems[1]}' and try again." + end + + def extract_circular_gems(error) + error.message.scan(/@name="(.*?)"/).flatten end def lookup @lookup ||= begin - lookup = Hash.new {|h, k| h[k] = [] } - Index.sort_specs(@specs).reverse_each do |s| - lookup[s.name] << s + lookup = {} + @specs.each do |s| + index_spec(lookup, s.name, s) end lookup end @@ -166,24 +353,50 @@ module Bundler @specs.sort_by(&:name).each {|s| yield s } end - def spec_for_dependency(dep, match_current_platform) - specs_for_platforms = lookup[dep.name] - if match_current_platform - Bundler.rubygems.platforms.reverse_each do |pl| - match = GemHelpers.select_best_platform_match(specs_for_platforms, pl) - return match if match - end - nil - else - GemHelpers.select_best_platform_match(specs_for_platforms, dep.__platform) - end - end - def tsort_each_child(s) s.dependencies.sort_by(&:name).each do |d| next if d.type == :development - lookup[d.name].each {|s2| yield s2 } + + specs_for_name = lookup[d.name] + next unless specs_for_name + + specs_for_name.each {|s2| yield s2 } + end + end + + def add_spec(spec) + @specs << spec + + name = spec.name + + @sorted&.insert(@sorted.bsearch_index {|s| s.name >= name } || @sorted.size, spec) + return if @lookup.nil? + + index_spec(@lookup, name, spec) + end + + def remove_spec(spec) + @specs.delete(spec) + @sorted&.delete(spec) + return if @lookup.nil? + + indexed_specs = @lookup[spec.name] + return unless indexed_specs + + if indexed_specs.size > 1 + @lookup[spec.name].delete(spec) + else + @lookup[spec.name] = nil end end + + def index_spec(hash, key, value) + hash[key] ||= [] + hash[key] << value + end + + def exemplary_spec(name) + self[name].first + end end end |
