diff options
Diffstat (limited to 'lib/bundler/definition.rb')
-rw-r--r-- | lib/bundler/definition.rb | 730 |
1 files changed, 433 insertions, 297 deletions
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index da180ce255..c8faf77b3b 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -16,10 +16,10 @@ module Bundler :locked_deps, :locked_gems, :platforms, - :requires, :ruby_version, :lockfile, - :gemfiles + :gemfiles, + :locked_checksums ) # Given a gemfile and lockfile creates a Bundler definition @@ -70,27 +70,34 @@ module Bundler @unlock = unlock @optional_groups = optional_groups @remote = false + @prefer_local = false @specs = nil @ruby_version = ruby_version @gemfiles = gemfiles @lockfile = lockfile @lockfile_contents = String.new + @locked_bundler_version = nil - @locked_ruby_version = nil + @resolved_bundler_version = nil + + @locked_ruby_version = nil @new_platform = nil + @removed_platform = nil - if lockfile && File.exist?(lockfile) + if lockfile_exists? @lockfile_contents = Bundler.read_file(lockfile) @locked_gems = LockfileParser.new(@lockfile_contents) @locked_platforms = @locked_gems.platforms @platforms = @locked_platforms.dup @locked_bundler_version = @locked_gems.bundler_version @locked_ruby_version = @locked_gems.ruby_version + @originally_locked_specs = SpecSet.new(@locked_gems.specs) + @locked_checksums = @locked_gems.checksums if unlock != true @locked_deps = @locked_gems.dependencies - @locked_specs = SpecSet.new(@locked_gems.specs) + @locked_specs = @originally_locked_specs @locked_sources = @locked_gems.sources else @unlock = {} @@ -104,8 +111,10 @@ module Bundler @locked_gems = nil @locked_deps = {} @locked_specs = SpecSet.new([]) + @originally_locked_specs = @locked_specs @locked_sources = [] @locked_platforms = [] + @locked_checksums = nil end locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } @@ -127,7 +136,7 @@ module Bundler end @unlocking ||= @unlock[:ruby] ||= (!@locked_ruby_version ^ !@ruby_version) - add_current_platform unless current_ruby_platform_locked? || Bundler.frozen_bundle? + add_current_platform unless Bundler.frozen_bundle? converge_path_sources_to_gemspec_sources @path_changes = converge_paths @@ -136,35 +145,18 @@ module Bundler if @unlock[:conservative] @unlock[:gems] ||= @dependencies.map(&:name) else - eager_unlock = expand_dependencies(@unlock[:gems] || [], true) - @unlock[:gems] = @locked_specs.for(eager_unlock, false, false).map(&:name) + eager_unlock = (@unlock[:gems] || []).map {|name| Dependency.new(name, ">= 0") } + @unlock[:gems] = @locked_specs.for(eager_unlock, false, platforms).map(&:name).uniq end @dependency_changes = converge_dependencies @local_changes = converge_locals - @locked_specs_incomplete_for_platform = !@locked_specs.for(expand_dependencies(requested_dependencies & locked_dependencies), true, true) - - @requires = compute_requires + check_lockfile end def gem_version_promoter - @gem_version_promoter ||= begin - locked_specs = - if unlocking? && @locked_specs.empty? && !@lockfile_contents.empty? - # Definition uses an empty set of locked_specs to indicate all gems - # are unlocked, but GemVersionPromoter needs the locked_specs - # for conservative comparison. - Bundler::SpecSet.new(@locked_gems.specs) - else - @locked_specs - end - GemVersionPromoter.new(locked_specs, @unlock[:gems]) - end - end - - def multisource_allowed? - @multisource_allowed + @gem_version_promoter ||= GemVersionPromoter.new end def resolve_only_locally! @@ -184,6 +176,23 @@ module Bundler resolve end + def resolution_mode=(options) + if options["local"] + @remote = false + else + @remote = true + @prefer_local = options["prefer-local"] + end + end + + def setup_sources_for_resolve + if @remote == false + sources.cached! + else + sources.remote! + end + end + # For given dependency list returns a SpecSet with Gemspec of all the required # dependencies. # 1. The method first resolves the dependencies specified in Gemfile @@ -213,6 +222,8 @@ module Bundler true rescue BundlerError => e @resolve = nil + @resolver = nil + @resolution_packages = nil @specs = nil @gem_version_promoter = nil @@ -229,8 +240,17 @@ module Bundler end def current_dependencies + filter_relevant(dependencies) + end + + def current_locked_dependencies + filter_relevant(locked_dependencies) + end + + def filter_relevant(dependencies) + platforms_array = [generic_local_platform].freeze dependencies.select do |d| - d.should_include? && !d.gem_platforms(@platforms).empty? + d.should_include? && !d.gem_platforms(platforms_array).empty? end end @@ -238,17 +258,31 @@ module Bundler @locked_deps.values end + def new_deps + @new_deps ||= @dependencies - locked_dependencies + end + + def deleted_deps + @deleted_deps ||= locked_dependencies - @dependencies + end + def specs_for(groups) - groups = requested_groups if groups.empty? + return specs if groups.empty? deps = dependencies_for(groups) - materialize(expand_dependencies(deps)) + materialize(deps) end def dependencies_for(groups) groups.map!(&:to_sym) - current_dependencies.reject do |d| - (d.groups & groups).empty? + deps = current_dependencies # always returns a new array + deps.select! do |d| + if RUBY_VERSION >= "3.1" + d.groups.intersect?(groups) + else + !(d.groups & groups).empty? + end end + deps end # Resolve all the dependencies specified in Gemfile. It ensures that @@ -257,20 +291,24 @@ module Bundler # # @return [SpecSet] resolved dependencies def resolve - @resolve ||= begin - last_resolve = converge_locked_specs - if Bundler.frozen_bundle? - Bundler.ui.debug "Frozen, using resolution from the lockfile" - last_resolve - elsif !unlocking? && nothing_changed? - Bundler.ui.debug("Found no changes, using resolution from the lockfile") - last_resolve + @resolve ||= if Bundler.frozen_bundle? + Bundler.ui.debug "Frozen, using resolution from the lockfile" + @locked_specs + elsif no_resolve_needed? + if deleted_deps.any? + Bundler.ui.debug "Some dependencies were deleted, using a subset of the resolution from the lockfile" + SpecSet.new(filter_specs(@locked_specs, @dependencies - deleted_deps)) else - # Run a resolve against the locally available gems - Bundler.ui.debug("Found changes from the lockfile, re-resolving dependencies because #{change_reason}") - expanded_dependencies = expand_dependencies(dependencies + metadata_dependencies, @remote) - Resolver.resolve(expanded_dependencies, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) + Bundler.ui.debug "Found no changes, using resolution from the lockfile" + if @removed_platform || @locked_gems.may_include_redundant_platform_specific_gems? + SpecSet.new(filter_specs(@locked_specs, @dependencies)) + else + @locked_specs + end end + else + Bundler.ui.debug "Found changes from the lockfile, re-resolving dependencies because #{change_reason}" + start_resolution end end @@ -282,45 +320,26 @@ module Bundler dependencies.map(&:groups).flatten.uniq end - def lock(file, preserve_unknown_sections = false) - return if Definition.no_lock - - contents = to_lock - - # Convert to \r\n if the existing lock has them - # i.e., Windows with `git config core.autocrlf=true` - contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match("\r\n") - - if @locked_bundler_version - locked_major = @locked_bundler_version.segments.first - current_major = Gem::Version.create(Bundler::VERSION).segments.first + def lock(file_or_preserve_unknown_sections = false, preserve_unknown_sections_or_unused = false) + if [true, false, nil].include?(file_or_preserve_unknown_sections) + target_lockfile = lockfile || Bundler.default_lockfile + preserve_unknown_sections = file_or_preserve_unknown_sections + else + target_lockfile = file_or_preserve_unknown_sections + preserve_unknown_sections = preserve_unknown_sections_or_unused - if updating_major = locked_major < current_major - Bundler.ui.warn "Warning: the lockfile is being updated to Bundler #{current_major}, " \ - "after which you will be unable to return to Bundler #{@locked_bundler_version.segments.first}." + suggestion = if target_lockfile == lockfile + "To fix this warning, remove it from the `Definition#lock` call." + else + "Instead, instantiate a new definition passing `#{target_lockfile}`, and call `lock` without a file argument on that definition" end - end - - preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) - return if file && File.exist?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + msg = "`Definition#lock` was passed a target file argument. #{suggestion}" - if Bundler.frozen_bundle? - Bundler.ui.error "Cannot write a changed lockfile while frozen." - return + Bundler::SharedHelpers.major_deprecation 2, msg end - SharedHelpers.filesystem_access(file) do |p| - File.open(p, "wb") {|f| f.puts(contents) } - end - end - - def locked_bundler_version - if @locked_bundler_version && @locked_bundler_version < Gem::Version.new(Bundler::VERSION) - new_version = Bundler::VERSION - end - - new_version || @locked_bundler_version || Bundler::VERSION + write_lock(target_lockfile, preserve_unknown_sections) end def locked_ruby_version @@ -344,27 +363,16 @@ module Bundler end end + def bundler_version_to_lock + @resolved_bundler_version || Bundler.gem_version + end + def to_lock require_relative "lockfile_generator" LockfileGenerator.generate(self) end def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) - msg = String.new - msg << "You are trying to install in deployment mode after changing\n" \ - "your Gemfile. Run `bundle install` elsewhere and add the\n" \ - "updated #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} to version control." - - unless explicit_flag - suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any? - "bundle config unset frozen" - elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? - "bundle config unset deployment" - end - msg << "\n\nIf this is a development machine, remove the #{Bundler.default_gemfile} " \ - "freeze \nby running `#{suggested_command}`." - end - added = [] deleted = [] changed = [] @@ -374,52 +382,40 @@ module Bundler added.concat new_platforms.map {|p| "* platform: #{p}" } deleted.concat deleted_platforms.map {|p| "* platform: #{p}" } - gemfile_sources = sources.lock_sources - - new_sources = gemfile_sources - @locked_sources - deleted_sources = @locked_sources - gemfile_sources - - new_deps = @dependencies - locked_dependencies - deleted_deps = locked_dependencies - @dependencies - - # Check if it is possible that the source is only changed thing - if (new_deps.empty? && deleted_deps.empty?) && (!new_sources.empty? && !deleted_sources.empty?) - new_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } - deleted_sources.reject! {|source| (source.path? && source.path.exist?) || equivalent_rubygems_remotes?(source) } - end - - if @locked_sources != gemfile_sources - if new_sources.any? - added.concat new_sources.map {|source| "* source: #{source}" } - end - - if deleted_sources.any? - deleted.concat deleted_sources.map {|source| "* source: #{source}" } - end - end - added.concat new_deps.map {|d| "* #{pretty_dep(d)}" } if new_deps.any? - if deleted_deps.any? - deleted.concat deleted_deps.map {|d| "* #{pretty_dep(d)}" } - end + deleted.concat deleted_deps.map {|d| "* #{pretty_dep(d)}" } if deleted_deps.any? both_sources = Hash.new {|h, k| h[k] = [] } - @dependencies.each {|d| both_sources[d.name][0] = d } - @locked_deps.each {|name, d| both_sources[name][1] = d.source } + current_dependencies.each {|d| both_sources[d.name][0] = d } + current_locked_dependencies.each {|d| both_sources[d.name][1] = d } + + both_sources.each do |name, (dep, lock_dep)| + next if dep.nil? || lock_dep.nil? - both_sources.each do |name, (dep, lock_source)| - next if lock_source.nil? || (dep && lock_source.can_lock?(dep)) - gemfile_source_name = (dep && dep.source) || "no specified source" - lockfile_source_name = lock_source - changed << "* #{name} from `#{gemfile_source_name}` to `#{lockfile_source_name}`" + gemfile_source = dep.source || default_source + lock_source = lock_dep.source || default_source + next if lock_source.include?(gemfile_source) + + gemfile_source_name = dep.source ? gemfile_source.to_gemfile : "no specified source" + lockfile_source_name = lock_dep.source ? lock_source.to_gemfile : "no specified source" + changed << "* #{name} from `#{lockfile_source_name}` to `#{gemfile_source_name}`" end reason = change_reason - msg << "\n\n#{reason.split(", ").map(&:capitalize).join("\n")}" unless reason.strip.empty? + msg = String.new + msg << "#{reason.capitalize.strip}, but the lockfile can't be updated because frozen mode is set" msg << "\n\nYou have added to the Gemfile:\n" << added.join("\n") if added.any? msg << "\n\nYou have deleted from the Gemfile:\n" << deleted.join("\n") if deleted.any? msg << "\n\nYou have changed in the Gemfile:\n" << changed.join("\n") if changed.any? - msg << "\n" + msg << "\n\nRun `bundle install` elsewhere and add the updated #{SharedHelpers.relative_gemfile_path} to version control.\n" + + unless explicit_flag + suggested_command = unless Bundler.settings.locations("frozen").keys.include?(:env) + "bundle config set frozen false" + end + msg << "If this is a development machine, remove the #{SharedHelpers.relative_lockfile_path} " \ + "freeze by running `#{suggested_command}`." if suggested_command + end raise ProductionError, msg if added.any? || deleted.any? || changed.any? || !nothing_changed? end @@ -458,8 +454,8 @@ module Bundler return if current_platform_locked? raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \ - "but your local platform is #{Bundler.local_platform}. " \ - "Add the current platform to the lockfile with `bundle lock --add-platform #{Bundler.local_platform}` and try again." + "but your local platform is #{local_platform}. " \ + "Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again." end def add_platform(platform) @@ -468,7 +464,9 @@ module Bundler end def remove_platform(platform) - return if @platforms.delete(Gem::Platform.new(platform)) + removed_platform = @platforms.delete(Gem::Platform.new(platform)) + @removed_platform ||= removed_platform + return if removed_platform raise InvalidOption, "Unable to remove the platform `#{platform}` since the only platforms are #{@platforms.join ", "}" end @@ -482,7 +480,19 @@ module Bundler private :sources def nothing_changed? - !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@locked_specs_incomplete_for_platform + !@source_changes && + !@dependency_changes && + !@new_platform && + !@path_changes && + !@local_changes && + !@missing_lockfile_dep && + !@unlocking_bundler && + !@locked_spec_with_missing_deps && + !@locked_spec_with_invalid_deps + end + + def no_resolve_needed? + !unlocking? && nothing_changed? end def unlocking? @@ -491,6 +501,80 @@ module Bundler private + def should_add_extra_platforms? + !lockfile_exists? && generic_local_platform_is_ruby? && !Bundler.settings[:force_ruby_platform] + end + + def lockfile_exists? + file_exists?(lockfile) + end + + def file_exists?(file) + file && File.exist?(file) + end + + def write_lock(file, preserve_unknown_sections) + return if Definition.no_lock + + contents = to_lock + + # Convert to \r\n if the existing lock has them + # i.e., Windows with `git config core.autocrlf=true` + contents.gsub!(/\n/, "\r\n") if @lockfile_contents.match?("\r\n") + + if @locked_bundler_version + locked_major = @locked_bundler_version.segments.first + current_major = bundler_version_to_lock.segments.first + + updating_major = locked_major < current_major + end + + preserve_unknown_sections ||= !updating_major && (Bundler.frozen_bundle? || !(unlocking? || @unlocking_bundler)) + + if file_exists?(file) && lockfiles_equal?(@lockfile_contents, contents, preserve_unknown_sections) + return if Bundler.frozen_bundle? + SharedHelpers.filesystem_access(file) { FileUtils.touch(file) } + return + end + + if Bundler.frozen_bundle? + Bundler.ui.error "Cannot write a changed lockfile while frozen." + return + end + + SharedHelpers.filesystem_access(file) do |p| + File.open(p, "wb") {|f| f.puts(contents) } + end + end + + def resolver + @resolver ||= Resolver.new(resolution_packages, gem_version_promoter) + end + + def expanded_dependencies + dependencies_with_bundler + metadata_dependencies + end + + def dependencies_with_bundler + return dependencies unless @unlocking_bundler + return dependencies if dependencies.map(&:name).include?("bundler") + + [Dependency.new("bundler", @unlocking_bundler)] + dependencies + end + + def resolution_packages + @resolution_packages ||= begin + last_resolve = converge_locked_specs + remove_invalid_platforms!(current_dependencies) + packages = Resolver::Base.new(source_requirements, expanded_dependencies, last_resolve, @platforms, locked_specs: @originally_locked_specs, unlock: @unlock[:gems], prerelease: gem_version_promoter.pre?) + additional_base_requirements_for_resolve(packages, last_resolve) + end + end + + def filter_specs(specs, deps) + SpecSet.new(specs).for(deps, false, platforms) + end + def materialize(dependencies) specs = resolve.materialize(dependencies) missing_specs = specs.missing_specs @@ -505,34 +589,83 @@ module Bundler "removed in order to install." end - raise GemNotFound, "Could not find #{missing_specs.map(&:full_name).join(", ")} in any of the sources" + missing_specs_list = missing_specs.group_by(&:source).map do |source, missing_specs_for_source| + "#{missing_specs_for_source.map(&:full_name).join(", ")} in #{source}" + end + + raise GemNotFound, "Could not find #{missing_specs_list.join(" nor ")}" end - unless specs["bundler"].any? - bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last - specs["bundler"] = bundler + incomplete_specs = specs.incomplete_specs + loop do + break if incomplete_specs.empty? + + Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies") + setup_sources_for_resolve + resolution_packages.delete(incomplete_specs) + @resolve = start_resolution + specs = resolve.materialize(dependencies) + + still_incomplete_specs = specs.incomplete_specs + + if still_incomplete_specs == incomplete_specs + package = resolution_packages.get_package(incomplete_specs.first.name) + resolver.raise_not_found! package + end + + incomplete_specs = still_incomplete_specs end + bundler = sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last + specs["bundler"] = bundler + specs end + def start_resolution + result = SpecSet.new(resolver.start) + + @resolved_bundler_version = result.find {|spec| spec.name == "bundler" }&.version + @platforms = result.add_extra_platforms!(platforms) if should_add_extra_platforms? + + result.complete_platforms!(platforms) + + SpecSet.new(result.for(dependencies, false, @platforms)) + end + def precompute_source_requirements_for_indirect_dependencies? - @remote && sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source? + sources.non_global_rubygems_sources.all?(&:dependency_api_available?) && !sources.aggregate_global_source? + end + + def pin_locally_available_names(source_requirements) + source_requirements.each_with_object({}) do |(name, original_source), new_source_requirements| + local_source = original_source.dup + local_source.local_only! + + new_source_requirements[name] = if local_source.specs.search(name).any? + local_source + else + original_source + end + end end def current_ruby_platform_locked? - return false unless generic_local_platform == Gem::Platform::RUBY + return false unless generic_local_platform_is_ruby? + return false if Bundler.settings[:force_ruby_platform] && !@platforms.include?(Gem::Platform::RUBY) current_platform_locked? end def current_platform_locked? @platforms.any? do |bundle_platform| - MatchPlatform.platforms_match?(bundle_platform, Bundler.local_platform) + MatchPlatform.platforms_match?(bundle_platform, local_platform) end end def add_current_platform + return if current_ruby_platform_locked? + add_platform(local_platform) end @@ -554,12 +687,15 @@ module Bundler [@new_platform, "you added a new platform to your gemfile"], [@path_changes, "the gemspecs for path gems changed"], [@local_changes, "the gemspecs for git local gems changed"], - [@locked_specs_incomplete_for_platform, "the lockfile does not have all gems needed for the current platform"], + [@missing_lockfile_dep, "your lock file is missing \"#{@missing_lockfile_dep}\""], + [@unlocking_bundler, "an update to the version of Bundler itself was requested"], + [@locked_spec_with_missing_deps, "your lock file includes \"#{@locked_spec_with_missing_deps}\" but not some of its dependencies"], + [@locked_spec_with_invalid_deps, "your lockfile does not satisfy dependencies of \"#{@locked_spec_with_invalid_deps}\""], ].select(&:first).map(&:last).join(", ") end - def pretty_dep(dep, source = false) - SharedHelpers.pretty_dependency(dep, source) + def pretty_dep(dep) + SharedHelpers.pretty_dependency(dep) end # Check if the specs of the given source changed @@ -581,8 +717,7 @@ module Bundler locked_index = Index.new locked_index.use(@locked_specs.select {|s| source.can_lock?(s) }) - # order here matters, since Index#== is checking source.specs.include?(locked_index) - locked_index != source.specs + !locked_index.subset?(source.specs) rescue PathError, GitError => e Bundler.ui.debug "Assuming that #{source} has not changed since fetching its specs errored (#{e})" false @@ -596,8 +731,8 @@ module Bundler Bundler.settings.local_overrides.map do |k, v| spec = @dependencies.find {|s| s.name == k } - source = spec && spec.source - if source && source.respond_to?(:local_override!) + source = spec&.source + if source&.respond_to?(:local_override!) source.unlock! if @unlock[:gems].include?(spec.name) locals << [source, source.local_override!(v)] end @@ -609,6 +744,39 @@ module Bundler !sources_with_changes.each {|source| @unlock[:sources] << source.name }.empty? end + def check_lockfile + @missing_lockfile_dep = nil + + @locked_spec_with_invalid_deps = nil + @locked_spec_with_missing_deps = nil + + missing = [] + invalid = [] + + @locked_specs.each do |s| + validation = @locked_specs.validate_deps(s) + + missing << s if validation == :missing + invalid << s if validation == :invalid + end + + if missing.any? + @locked_specs.delete(missing) + + @locked_spec_with_missing_deps = missing.first.name + elsif !@dependency_changes + @missing_lockfile_dep = current_dependencies.find do |d| + @locked_specs[d.name].empty? && d.name != "bundler" + end&.name + end + + if invalid.any? + @locked_specs.delete(invalid) + + @locked_spec_with_invalid_deps = invalid.first.name + end + end + def converge_paths sources.path_sources.any? do |source| specs_changed?(source) @@ -640,6 +808,11 @@ module Bundler changes = sources.replace_sources!(@locked_sources) sources.all_sources.each do |source| + # has to be done separately, because we want to keep the locked checksum + # store for a source, even when doing a full update + if @locked_checksums && @locked_gems && locked_source = @locked_gems.sources.find {|s| s == source && !s.equal?(source) } + source.checksum_store.merge!(locked_source.checksum_store) + end # If the source is unlockable and the current command allows an unlock of # the source (for example, you are doing a `bundle update <foo>` of a git-pinned # gem), unlock it. For git sources, this means to unlock the revision, which @@ -655,25 +828,16 @@ module Bundler end def converge_dependencies - frozen = Bundler.frozen_bundle? - (@dependencies + locked_dependencies).each do |dep| - locked_source = @locked_deps[dep.name] - # This is to make sure that if bundler is installing in deployment mode and - # after locked_source and sources don't match, we still use locked_source. - if frozen && !locked_source.nil? && - locked_source.respond_to?(:source) && locked_source.source.instance_of?(Source::Path) && locked_source.source.path.exist? - dep.source = locked_source.source - elsif dep.source + changes = false + + @dependencies.each do |dep| + if dep.source dep.source = sources.get(dep.source) end - end - changes = false - # We want to know if all match, but don't want to check all entries - # This means we need to return false if any dependency doesn't match - # the lock or doesn't exist in the lock. - @dependencies.each do |dependency| - unless locked_dep = @locked_deps[dependency.name] + next if unlocking? + + unless locked_dep = @locked_deps[dep.name] changes = true next end @@ -684,11 +848,11 @@ module Bundler # directive, the lockfile dependencies and resolved dependencies end up # with a mismatch on #type. Work around that by setting the type on the # dep from the lockfile. - locked_dep.instance_variable_set(:@type, dependency.type) + locked_dep.instance_variable_set(:@type, dep.type) # We already know the name matches from the hash lookup # so we only need to check the requirement now - changes ||= dependency.requirement != locked_dep.requirement + changes ||= dep.requirement != locked_dep.requirement end changes @@ -698,139 +862,91 @@ module Bundler # commonly happen if the Gemfile has changed since the lockfile was last # generated def converge_locked_specs - deps = [] + converged = converge_specs(@locked_specs) - # Build a list of dependencies that are the same in the Gemfile - # and Gemfile.lock. If the Gemfile modified a dependency, but - # the gem in the Gemfile.lock still satisfies it, this is fine - # too. - @dependencies.each do |dep| - locked_dep = @locked_deps[dep.name] + resolve = SpecSet.new(converged.reject {|s| @unlock[:gems].include?(s.name) }) - # If the locked_dep doesn't match the dependency we're looking for then we ignore the locked_dep - locked_dep = nil unless locked_dep == dep + diff = nil - if in_locked_deps?(dep, locked_dep) || satisfies_locked_spec?(dep) - deps << dep - elsif dep.source.is_a?(Source::Path) && dep.current_platform? && (!locked_dep || dep.source != locked_dep.source) - @locked_specs.each do |s| - @unlock[:gems] << s.name if s.source == dep.source - end + # Now, we unlock any sources that do not have anymore gems pinned to it + sources.all_sources.each do |source| + next unless source.respond_to?(:unlock!) - dep.source.unlock! if dep.source.respond_to?(:unlock!) - dep.source.specs.each {|s| @unlock[:gems] << s.name } + unless resolve.any? {|s| s.source == source } + diff ||= @locked_specs.to_a - resolve.to_a + source.unlock! if diff.any? {|s| s.source == source } end end + resolve + end + + def converge_specs(specs) converged = [] - @locked_specs.each do |s| - # Replace the locked dependency's source with the equivalent source from the Gemfile + deps = [] + + @specs_that_changed_sources = [] + + specs.each do |s| + name = s.name dep = @dependencies.find {|d| s.satisfies?(d) } - s.source = (dep && dep.source) || sources.get(s.source) unless multisource_allowed? + lockfile_source = s.source - # Don't add a spec to the list if its source is expired. For example, - # if you change a Git gem to RubyGems. - next if s.source.nil? - next if @unlock[:sources].include?(s.source.name) + if dep + gemfile_source = dep.source || default_source + + @specs_that_changed_sources << s if gemfile_source != lockfile_source + deps << dep if !dep.source || lockfile_source.include?(dep.source) + @unlock[:gems] << name if lockfile_source.include?(dep.source) && lockfile_source != gemfile_source + + # Replace the locked dependency's source with the equivalent source from the Gemfile + s.source = gemfile_source + else + # Replace the locked dependency's source with the default source, if the locked source is no longer in the Gemfile + s.source = default_source unless sources.get(lockfile_source) + end - # If the spec is from a path source and it doesn't exist anymore - # then we unlock it. + next if @unlock[:sources].include?(s.source.name) # Path sources have special logic if s.source.instance_of?(Source::Path) || s.source.instance_of?(Source::Gemspec) new_specs = begin s.source.specs - rescue PathError, GitError + rescue PathError # if we won't need the source (according to the lockfile), - # don't error if the path/git source isn't available - next if @locked_specs. - for(requested_dependencies, false, true). + # don't error if the path source isn't available + next if specs. + for(requested_dependencies, false). none? {|locked_spec| locked_spec.source == s.source } raise end new_spec = new_specs[s].first - - # If the spec is no longer in the path source, unlock it. This - # commonly happens if the version changed in the gemspec - next unless new_spec - - s.dependencies.replace(new_spec.dependencies) + if new_spec + s.dependencies.replace(new_spec.dependencies) + else + # If the spec is no longer in the path source, unlock it. This + # commonly happens if the version changed in the gemspec + @unlock[:gems] << name + end end - converged << s - end - - resolve = SpecSet.new(converged) - resolve = SpecSet.new(resolve.for(expand_dependencies(deps, true), false, false).reject{|s| @unlock[:gems].include?(s.name) }) - diff = nil - - # Now, we unlock any sources that do not have anymore gems pinned to it - sources.all_sources.each do |source| - next unless source.respond_to?(:unlock!) - - unless resolve.any? {|s| s.source == source } - diff ||= @locked_specs.to_a - resolve.to_a - source.unlock! if diff.any? {|s| s.source == source } + if dep.nil? && requested_dependencies.find {|d| name == d.name } + @unlock[:gems] << s.name + else + converged << s end end - resolve - end - - def in_locked_deps?(dep, locked_dep) - # Because the lockfile can't link a dep to a specific remote, we need to - # treat sources as equivalent anytime the locked dep has all the remotes - # that the Gemfile dep does. - locked_dep && locked_dep.source && dep.source && locked_dep.source.include?(dep.source) - end - - def satisfies_locked_spec?(dep) - @locked_specs[dep].any? {|s| s.satisfies?(dep) && (!dep.source || s.source.include?(dep.source)) } + filter_specs(converged, deps) end def metadata_dependencies - @metadata_dependencies ||= begin - ruby_versions = ruby_version_requirements(@ruby_version) - [ - Dependency.new("Ruby\0", ruby_versions), - Dependency.new("RubyGems\0", Gem::VERSION), - ] - end - end - - def ruby_version_requirements(ruby_version) - return [] unless ruby_version - if ruby_version.patchlevel - [ruby_version.to_gem_version_with_patchlevel] - else - ruby_version.versions.map do |version| - requirement = Gem::Requirement.new(version) - if requirement.exact? - "~> #{version}.0" - else - requirement - end - end - end - end - - def expand_dependencies(dependencies, remote = false) - deps = [] - dependencies.each do |dep| - dep = Dependency.new(dep, ">= 0") unless dep.respond_to?(:name) - next unless remote || dep.current_platform? - target_platforms = dep.gem_platforms(remote ? @platforms : [generic_local_platform]) - deps += expand_dependency_with_platforms(dep, target_platforms) - end - deps - end - - def expand_dependency_with_platforms(dep, platforms) - platforms.map do |p| - DepProxy.get_proxy(dep, p) - end + @metadata_dependencies ||= [ + Dependency.new("Ruby\0", Bundler::RubyVersion.system.gem_version), + Dependency.new("RubyGems\0", Gem::VERSION), + ] end def source_requirements @@ -838,27 +954,54 @@ module Bundler # specs will be available later when the resolver knows where to # look for that gemspec (or its dependencies) source_requirements = if precompute_source_requirements_for_indirect_dependencies? - { :default => sources.default_source }.merge(source_map.all_requirements) + all_requirements = source_map.all_requirements + all_requirements = pin_locally_available_names(all_requirements) if @prefer_local + { default: default_source }.merge(all_requirements) else - { :default => Source::RubygemsAggregate.new(sources, source_map) }.merge(source_map.direct_requirements) + { default: Source::RubygemsAggregate.new(sources, source_map) }.merge(source_map.direct_requirements) end + source_requirements.merge!(source_map.locked_requirements) unless @remote metadata_dependencies.each do |dep| source_requirements[dep.name] = sources.metadata_source end - source_requirements[:default_bundler] = source_requirements["bundler"] || sources.default_source - source_requirements["bundler"] = sources.metadata_source # needs to come last to override + + default_bundler_source = source_requirements["bundler"] || default_source + + if @unlocking_bundler + default_bundler_source.add_dependency_names("bundler") + else + source_requirements[:default_bundler] = default_bundler_source + source_requirements["bundler"] = sources.metadata_source # needs to come last to override + end + + verify_changed_sources! source_requirements end + def default_source + sources.default_source + end + + def verify_changed_sources! + @specs_that_changed_sources.each do |s| + if s.source.specs.search(s.name).empty? + raise GemNotFound, "Could not find gem '#{s.name}' in #{s.source}" + end + end + end + def requested_groups - groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with] + values = groups - Bundler.settings[:without] - @optional_groups + Bundler.settings[:with] + values &= Bundler.settings[:only] unless Bundler.settings[:only].empty? + values end def lockfiles_equal?(current, proposed, preserve_unknown_sections) if preserve_unknown_sections sections_to_ignore = LockfileParser.sections_to_ignore(@locked_bundler_version) sections_to_ignore += LockfileParser.unknown_sections_in_lockfile(current) - sections_to_ignore += LockfileParser::ENVIRONMENT_VERSION_SECTIONS + sections_to_ignore << LockfileParser::RUBY + sections_to_ignore << LockfileParser::BUNDLED unless @unlocking_bundler pattern = /#{Regexp.union(sections_to_ignore)}\n(\s{2,}.*\n)+/ whitespace_cleanup = /\n{2,}/ current = current.gsub(pattern, "\n").gsub(whitespace_cleanup, "\n\n").strip @@ -867,39 +1010,32 @@ module Bundler current == proposed end - def compute_requires - dependencies.reduce({}) do |requires, dep| - next requires unless dep.should_include? - requires[dep.name] = Array(dep.autorequire || dep.name).map do |file| - # Allow `require: true` as an alias for `require: <name>` - file == true ? dep.name : file - end - requires + def additional_base_requirements_for_resolve(resolution_packages, last_resolve) + return resolution_packages unless @locked_gems && !sources.expired_sources?(@locked_gems.sources) + converge_specs(@originally_locked_specs - last_resolve).each do |locked_spec| + next if locked_spec.source.is_a?(Source::Path) + resolution_packages.base_requirements[locked_spec.name] = Gem::Requirement.new(">= #{locked_spec.version}") end + resolution_packages end - def additional_base_requirements_for_resolve - return [] unless @locked_gems && unlocking? && !sources.expired_sources?(@locked_gems.sources) - dependencies_by_name = dependencies.inject({}) {|memo, dep| memo.update(dep.name => dep) } - @locked_gems.specs.reduce({}) do |requirements, locked_spec| - name = locked_spec.name - dependency = dependencies_by_name[name] - next requirements if @locked_gems.dependencies[name] != dependency - next requirements if dependency && dependency.source.is_a?(Source::Path) - dep = Gem::Dependency.new(name, ">= #{locked_spec.version}") - requirements[name] = DepProxy.get_proxy(dep, locked_spec.platform) - requirements - end.values - end + def remove_invalid_platforms!(dependencies) + return if Bundler.frozen_bundle? - def equivalent_rubygems_remotes?(source) - return false unless source.is_a?(Source::Rubygems) + platforms.reverse_each do |platform| + next if local_platform == platform || + (@new_platform && platforms.last == platform) || + @path_changes || + @dependency_changes || + !@originally_locked_specs.incomplete_for_platform?(dependencies, platform) - Bundler.settings[:allow_deployment_source_credential_changes] && source.equivalent_remotes?(sources.rubygems_remotes) + remove_platform(platform) + add_current_platform if platform == Gem::Platform::RUBY + end end def source_map - @source_map ||= SourceMap.new(sources, dependencies) + @source_map ||= SourceMap.new(sources, dependencies, @locked_specs) end end end |