2018-11-02 19:07:56 -04:00
# frozen_string_literal: true
2019-06-01 05:49:40 -04:00
require_relative " lockfile_parser "
2018-11-02 19:07:56 -04:00
module Bundler
class Definition
include GemHelpers
2021-01-03 20:11:34 -05:00
class << self
# Do not create or modify a lockfile (Makes #lock a noop)
attr_accessor :no_lock
end
2018-11-02 19:07:56 -04:00
attr_reader (
:dependencies ,
:locked_deps ,
:locked_gems ,
:platforms ,
:requires ,
:ruby_version ,
:lockfile ,
:gemfiles
)
# Given a gemfile and lockfile creates a Bundler definition
#
# @param gemfile [Pathname] Path to Gemfile
# @param lockfile [Pathname,nil] Path to Gemfile.lock
# @param unlock [Hash, Boolean, nil] Gems that have been requested
# to be updated or true if all gems should be updated
# @return [Bundler::Definition]
def self . build ( gemfile , lockfile , unlock )
unlock || = { }
gemfile = Pathname . new ( gemfile ) . expand_path
raise GemfileNotFound , " #{ gemfile } not found " unless gemfile . file?
Dsl . evaluate ( gemfile , lockfile , unlock )
end
#
# How does the new system work?
#
# * Load information from Gemfile and Lockfile
# * Invalidate stale locked specs
# * All specs from stale source are stale
# * All specs that are reachable only through a stale
# dependency are stale.
# * If all fresh dependencies are satisfied by the locked
# specs, then we can try to resolve locally.
#
# @param lockfile [Pathname] Path to Gemfile.lock
# @param dependencies [Array(Bundler::Dependency)] array of dependencies from Gemfile
# @param sources [Bundler::SourceList]
# @param unlock [Hash, Boolean, nil] Gems that have been requested
# to be updated or true if all gems should be updated
# @param ruby_version [Bundler::RubyVersion, nil] Requested Ruby Version
# @param optional_groups [Array(String)] A list of optional groups
def initialize ( lockfile , dependencies , sources , unlock , ruby_version = nil , optional_groups = [ ] , gemfiles = [ ] )
if [ true , false ] . include? ( unlock )
@unlocking_bundler = false
@unlocking = unlock
else
@unlocking_bundler = unlock . delete ( :bundler )
2021-07-07 01:07:29 -04:00
@unlocking = unlock . any? { | _k , v | ! Array ( v ) . empty? }
2018-11-02 19:07:56 -04:00
end
@dependencies = dependencies
@sources = sources
@unlock = unlock
@optional_groups = optional_groups
@remote = false
@specs = nil
@ruby_version = ruby_version
@gemfiles = gemfiles
@lockfile = lockfile
@lockfile_contents = String . new
@locked_bundler_version = nil
@locked_ruby_version = nil
2020-05-08 01:19:04 -04:00
@new_platform = nil
2018-11-02 19:07:56 -04:00
if lockfile && File . exist? ( lockfile )
@lockfile_contents = Bundler . read_file ( lockfile )
@locked_gems = LockfileParser . new ( @lockfile_contents )
@locked_platforms = @locked_gems . platforms
2021-02-01 10:17:16 -05:00
@platforms = @locked_platforms . dup
2018-11-02 19:07:56 -04:00
@locked_bundler_version = @locked_gems . bundler_version
@locked_ruby_version = @locked_gems . ruby_version
if unlock != true
@locked_deps = @locked_gems . dependencies
@locked_specs = SpecSet . new ( @locked_gems . specs )
@locked_sources = @locked_gems . sources
else
@unlock = { }
@locked_deps = { }
@locked_specs = SpecSet . new ( [ ] )
@locked_sources = [ ]
end
else
@unlock = { }
@platforms = [ ]
@locked_gems = nil
@locked_deps = { }
@locked_specs = SpecSet . new ( [ ] )
@locked_sources = [ ]
@locked_platforms = [ ]
end
2021-07-07 01:07:29 -04:00
locked_gem_sources = @locked_sources . select { | s | s . is_a? ( Source :: Rubygems ) }
@multisource_allowed = locked_gem_sources . size == 1 && locked_gem_sources . first . multiple_remotes? && Bundler . frozen_bundle?
2021-04-14 23:47:04 -04:00
2021-05-28 06:47:49 -04:00
if @multisource_allowed
unless sources . aggregate_global_source?
msg = " Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure. "
2021-04-14 23:47:04 -04:00
2021-05-28 06:47:49 -04:00
Bundler :: SharedHelpers . major_deprecation 2 , msg
end
2021-04-14 23:47:04 -04:00
2021-07-07 01:07:29 -04:00
@sources . merged_gem_lockfile_sections! ( locked_gem_sources . first )
2021-04-14 23:47:04 -04:00
end
2018-11-02 19:07:56 -04:00
@unlock [ :sources ] || = [ ]
@unlock [ :ruby ] || = if @ruby_version && locked_ruby_version_object
@ruby_version . diff ( locked_ruby_version_object )
end
@unlocking || = @unlock [ :ruby ] || = ( ! @locked_ruby_version ^ ! @ruby_version )
2020-12-22 18:45:19 -05:00
add_current_platform unless current_ruby_platform_locked? || Bundler . frozen_bundle?
2018-11-02 19:07:56 -04:00
converge_path_sources_to_gemspec_sources
@path_changes = converge_paths
@source_changes = converge_sources
2021-05-28 06:47:49 -04:00
if @unlock [ :conservative ]
@unlock [ :gems ] || = @dependencies . map ( & :name )
else
eager_unlock = expand_dependencies ( @unlock [ :gems ] || [ ] , true )
2021-07-23 17:49:13 -04:00
@unlock [ :gems ] = @locked_specs . for ( eager_unlock , false , false ) . map ( & :name )
2018-11-02 19:07:56 -04:00
end
@dependency_changes = converge_dependencies
@local_changes = converge_locals
[rubygems/rubygems] Fix `bundle install` crash due to an incorrectly incomplete resolve
In case we have a corrupted lockfile that claims to support a platform, but
it's missing platform specific gems for it, bundler has a check that
detects the situation and forces a re-resolve. The result of this check
is kept under the `@locked_specs_incomplete_for_platformn` instance
variable in `Definition`.
The installer, however, calls `Definition#nothing_changed?` before this
instance variable has been filled, so the result of it is actually
incorrect here since it will claim that nothing has changed, but
something has changed (locked specs are incomplete for the current
platform).
The consequence of this incorrect result is that the installer thinks it
can go on without re-resolving, resulting in the incomplete resolution
from the lockfile being used, and in a crash being triggered due to
that.
The solution is to make sure the `@locked_specs_incomplete_for_platform`
instance variable is filled before `nothing_changed?` gets called.
Moving it to `initialize` makes the most sense, not because it's the
best place for it (we can refactor this later), but because all of the
other "outdated definition" checks are already set there.
https://github.com/rubygems/rubygems/commit/708afdd789
2021-09-29 15:11:23 -04:00
@locked_specs_incomplete_for_platform = ! @locked_specs . for ( expand_dependencies ( requested_dependencies & locked_dependencies ) , true , true )
2018-11-02 19:07:56 -04:00
@requires = compute_requires
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
2021-05-28 06:47:49 -04:00
def multisource_allowed?
@multisource_allowed
end
2021-08-18 03:58:51 -04:00
def resolve_only_locally!
@remote = false
sources . local_only!
resolve
end
2018-11-02 19:07:56 -04:00
def resolve_with_cache!
sources . cached!
2021-04-21 07:54:29 -04:00
resolve
2018-11-02 19:07:56 -04:00
end
def resolve_remotely!
@remote = true
sources . remote!
2021-04-21 07:54:29 -04:00
resolve
2018-11-02 19:07:56 -04:00
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
# 2. After that it tries and fetches gemspec of resolved dependencies
#
# @return [Bundler::SpecSet]
def specs
2021-07-23 17:06:29 -04:00
@specs || = materialize ( requested_dependencies )
2018-11-02 19:07:56 -04:00
end
def new_specs
specs - @locked_specs
end
def removed_specs
@locked_specs - specs
end
def missing_specs
2021-07-23 17:49:13 -04:00
resolve . materialize ( requested_dependencies ) . missing_specs
2018-11-02 19:07:56 -04:00
end
def missing_specs?
missing = missing_specs
return false if missing . empty?
Bundler . ui . debug " The definition is missing #{ missing . map ( & :full_name ) } "
true
rescue BundlerError = > e
@resolve = nil
@specs = nil
@gem_version_promoter = nil
Bundler . ui . debug " The definition is missing dependencies, failed to resolve & materialize locally ( #{ e } ) "
true
end
def requested_specs
2021-07-13 07:58:08 -04:00
specs_for ( requested_groups )
2018-11-02 19:07:56 -04:00
end
2020-10-15 00:20:25 -04:00
def requested_dependencies
2021-07-13 07:58:08 -04:00
dependencies_for ( requested_groups )
2020-10-15 00:20:25 -04:00
end
2018-11-02 19:07:56 -04:00
def current_dependencies
2020-05-08 01:19:04 -04:00
dependencies . select do | d |
d . should_include? && ! d . gem_platforms ( @platforms ) . empty?
end
2018-11-02 19:07:56 -04:00
end
2021-09-29 14:07:08 -04:00
def locked_dependencies
@locked_deps . values
end
2018-11-02 19:07:56 -04:00
def specs_for ( groups )
2021-07-13 07:58:08 -04:00
groups = requested_groups if groups . empty?
2020-05-08 01:19:04 -04:00
deps = dependencies_for ( groups )
2021-07-23 17:06:29 -04:00
materialize ( expand_dependencies ( deps ) )
2018-11-02 19:07:56 -04:00
end
2020-10-15 00:20:25 -04:00
def dependencies_for ( groups )
2021-07-13 07:58:08 -04:00
groups . map! ( & :to_sym )
2020-10-15 00:20:25 -04:00
current_dependencies . reject do | d |
( d . groups & groups ) . empty?
end
end
2018-11-02 19:07:56 -04:00
# Resolve all the dependencies specified in Gemfile. It ensures that
# dependencies that have been already resolved via locked file and are fresh
# are reused when resolving dependencies
#
# @return [SpecSet] resolved dependencies
def resolve
@resolve || = begin
last_resolve = converge_locked_specs
2021-02-01 10:17:16 -05:00
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
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 )
end
2018-11-02 19:07:56 -04:00
end
end
def spec_git_paths
2019-09-04 12:20:09 -04:00
sources . git_sources . map { | s | File . realpath ( s . path ) if File . exist? ( s . path ) } . compact
2018-11-02 19:07:56 -04:00
end
def groups
dependencies . map ( & :groups ) . flatten . uniq
end
def lock ( file , preserve_unknown_sections = false )
2021-01-03 20:11:34 -05:00
return if Definition . no_lock
2018-11-02 19:07:56 -04:00
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 " )
2019-04-14 02:01:35 -04:00
if @locked_bundler_version
2018-11-02 19:07:56 -04:00
locked_major = @locked_bundler_version . segments . first
current_major = Gem :: Version . create ( Bundler :: VERSION ) . segments . first
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 } . "
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 )
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 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
end
def locked_ruby_version
return unless ruby_version
if @unlock [ :ruby ] || ! @locked_ruby_version
Bundler :: RubyVersion . system
else
@locked_ruby_version
end
end
def locked_ruby_version_object
return unless @locked_ruby_version
@locked_ruby_version_object || = begin
unless version = RubyVersion . from_string ( @locked_ruby_version )
raise LockfileError , " The Ruby version #{ @locked_ruby_version } from " \
" #{ @lockfile } could not be parsed. " \
" Try running bundle update --ruby to resolve this. "
end
version
end
end
def to_lock
2019-06-01 05:49:40 -04:00
require_relative " lockfile_generator "
2018-11-02 19:07:56 -04:00
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
2020-05-25 06:18:44 -04:00
suggested_command = if Bundler . settings . locations ( " frozen " ) . keys . & ( [ :global , :local ] ) . any?
2019-04-14 02:01:35 -04:00
" bundle config unset frozen "
2018-11-02 19:07:56 -04:00
elsif Bundler . settings . locations ( " deployment " ) . keys . & ( [ :global , :local ] ) . any?
2019-04-14 02:01:35 -04:00
" bundle config unset deployment "
2018-11-02 19:07:56 -04:00
end
msg << " \n \n If this is a development machine, remove the #{ Bundler . default_gemfile } " \
" freeze \n by running ` #{ suggested_command } `. "
end
added = [ ]
deleted = [ ]
changed = [ ]
new_platforms = @platforms - @locked_platforms
deleted_platforms = @locked_platforms - @platforms
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
2021-09-29 14:07:08 -04:00
new_deps = @dependencies - locked_dependencies
deleted_deps = locked_dependencies - @dependencies
2018-11-02 19:07:56 -04:00
# 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
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 }
both_sources . each do | name , ( dep , lock_source ) |
2020-05-08 01:19:04 -04:00
next if lock_source . nil? || ( dep && lock_source . can_lock? ( dep ) )
2018-11-02 19:07:56 -04:00
gemfile_source_name = ( dep && dep . source ) || " no specified source "
2020-05-08 01:19:04 -04:00
lockfile_source_name = lock_source
2018-11-02 19:07:56 -04:00
changed << " * #{ name } from ` #{ gemfile_source_name } ` to ` #{ lockfile_source_name } ` "
end
reason = change_reason
msg << " \n \n #{ reason . split ( " , " ) . map ( & :capitalize ) . join ( " \n " ) } " unless reason . strip . empty?
msg << " \n \n You have added to the Gemfile: \n " << added . join ( " \n " ) if added . any?
msg << " \n \n You have deleted from the Gemfile: \n " << deleted . join ( " \n " ) if deleted . any?
msg << " \n \n You have changed in the Gemfile: \n " << changed . join ( " \n " ) if changed . any?
msg << " \n "
raise ProductionError , msg if added . any? || deleted . any? || changed . any? || ! nothing_changed?
end
def validate_runtime!
validate_ruby!
validate_platforms!
end
def validate_ruby!
return unless ruby_version
if diff = ruby_version . diff ( Bundler :: RubyVersion . system )
problem , expected , actual = diff
msg = case problem
when :engine
" Your Ruby engine is #{ actual } , but your Gemfile specified #{ expected } "
when :version
" Your Ruby version is #{ actual } , but your Gemfile specified #{ expected } "
when :engine_version
" Your #{ Bundler :: RubyVersion . system . engine } version is #{ actual } , but your Gemfile specified #{ ruby_version . engine } #{ expected } "
when :patchlevel
if ! expected . is_a? ( String )
" The Ruby patchlevel in your Gemfile must be a string "
else
" Your Ruby patchlevel is #{ actual } , but your Gemfile specified #{ expected } "
end
end
raise RubyVersionMismatch , msg
end
end
def validate_platforms!
2020-12-22 18:45:19 -05:00
return if current_platform_locked?
2018-11-02 19:07:56 -04:00
raise ProductionError , " Your bundle only supports platforms #{ @platforms . map ( & :to_s ) } " \
2020-12-22 18:45:19 -05:00
" 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. "
2018-11-02 19:07:56 -04:00
end
def add_platform ( platform )
@new_platform || = ! @platforms . include? ( platform )
@platforms |= [ platform ]
end
def remove_platform ( platform )
return if @platforms . delete ( Gem :: Platform . new ( platform ) )
raise InvalidOption , " Unable to remove the platform ` #{ platform } ` since the only platforms are #{ @platforms . join " , " } "
end
2020-12-22 18:45:19 -05:00
def most_specific_locked_platform
@platforms . min_by do | bundle_platform |
platform_specificity_match ( bundle_platform , local_platform )
end
end
2018-11-02 19:07:56 -04:00
attr_reader :sources
private :sources
def nothing_changed?
! @source_changes && ! @dependency_changes && ! @new_platform && ! @path_changes && ! @local_changes && ! @locked_specs_incomplete_for_platform
end
def unlocking?
@unlocking
end
2020-10-15 00:20:25 -04:00
private
2018-11-02 19:07:56 -04:00
2021-07-23 17:06:29 -04:00
def materialize ( dependencies )
specs = resolve . materialize ( dependencies )
2021-07-23 17:49:13 -04:00
missing_specs = specs . missing_specs
if missing_specs . any?
missing_specs . each do | s |
locked_gem = @locked_specs [ s . name ] . last
next if locked_gem . nil? || locked_gem . version != s . version || ! @remote
raise GemNotFound , " Your bundle is locked to #{ locked_gem } from #{ locked_gem . source } , but that version can " \
" no longer be found in that source. That means the author of #{ locked_gem } has removed it. " \
" You'll need to update your bundle to a version other than #{ locked_gem } that hasn't been " \
" removed in order to install. "
end
raise GemNotFound , " Could not find #{ missing_specs . map ( & :full_name ) . join ( " , " ) } in any of the sources "
end
2021-07-23 17:06:29 -04:00
2021-07-13 07:58:08 -04:00
unless specs [ " bundler " ] . any?
bundler = sources . metadata_source . specs . search ( Gem :: Dependency . new ( " bundler " , VERSION ) ) . last
specs [ " bundler " ] = bundler
end
specs
end
2021-05-28 06:47:49 -04:00
def precompute_source_requirements_for_indirect_dependencies?
2021-07-31 09:05:29 -04:00
@remote && sources . non_global_rubygems_sources . all? ( & :dependency_api_available? ) && ! sources . aggregate_global_source?
2021-05-28 06:47:49 -04:00
end
2020-12-22 18:45:19 -05:00
def current_ruby_platform_locked?
return false unless generic_local_platform == Gem :: Platform :: RUBY
current_platform_locked?
end
def current_platform_locked?
@platforms . any? do | bundle_platform |
MatchPlatform . platforms_match? ( bundle_platform , Bundler . local_platform )
end
end
2020-12-08 02:36:29 -05:00
def add_current_platform
2020-12-14 18:32:54 -05:00
add_platform ( local_platform )
2019-06-19 09:29:02 -04:00
end
2018-11-02 19:07:56 -04:00
def change_reason
if unlocking?
unlock_reason = @unlock . reject { | _k , v | Array ( v ) . empty? } . map do | k , v |
if v == true
k . to_s
else
v = Array ( v )
" #{ k } : ( #{ v . join ( " , " ) } ) "
end
end . join ( " , " )
return " bundler is unlocking #{ unlock_reason } "
end
[
[ @source_changes , " the list of sources changed " ] ,
[ @dependency_changes , " the dependencies in your gemfile changed " ] ,
[ @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 " ] ,
] . select ( & :first ) . map ( & :last ) . join ( " , " )
end
def pretty_dep ( dep , source = false )
SharedHelpers . pretty_dependency ( dep , source )
end
# Check if the specs of the given source changed
# according to the locked source.
def specs_changed? ( source )
locked = @locked_sources . find { | s | s == source }
! locked || dependencies_for_source_changed? ( source , locked ) || specs_for_source_changed? ( source )
end
def dependencies_for_source_changed? ( source , locked_source = source )
deps_for_source = @dependencies . select { | s | s . source == source }
2021-09-29 14:07:08 -04:00
locked_deps_for_source = locked_dependencies . select { | dep | dep . source == locked_source }
2018-11-02 19:07:56 -04:00
2021-02-01 10:17:16 -05:00
deps_for_source . uniq . sort != locked_deps_for_source . sort
2018-11-02 19:07:56 -04:00
end
def specs_for_source_changed? ( source )
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
rescue PathError , GitError = > e
Bundler . ui . debug " Assuming that #{ source } has not changed since fetching its specs errored ( #{ e } ) "
false
end
# Get all locals and override their matching sources.
# Return true if any of the locals changed (for example,
# they point to a new revision) or depend on new specs.
def converge_locals
locals = [ ]
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 . unlock! if @unlock [ :gems ] . include? ( spec . name )
locals << [ source , source . local_override! ( v ) ]
end
end
sources_with_changes = locals . select do | source , changed |
changed || specs_changed? ( source )
end . map ( & :first )
! sources_with_changes . each { | source | @unlock [ :sources ] << source . name } . empty?
end
def converge_paths
sources . path_sources . any? do | source |
specs_changed? ( source )
end
end
def converge_path_source_to_gemspec_source ( source )
return source unless source . instance_of? ( Source :: Path )
gemspec_source = sources . path_sources . find { | s | s . is_a? ( Source :: Gemspec ) && s . as_path_source == source }
gemspec_source || source
end
def converge_path_sources_to_gemspec_sources
@locked_sources . map! do | source |
converge_path_source_to_gemspec_source ( source )
end
@locked_specs . each do | spec |
spec . source && = converge_path_source_to_gemspec_source ( spec . source )
end
@locked_deps . each do | _ , dep |
dep . source && = converge_path_source_to_gemspec_source ( dep . source )
end
end
def converge_sources
# Replace the sources from the Gemfile with the sources from the Gemfile.lock,
# if they exist in the Gemfile.lock and are `==`. If you can't find an equivalent
# source in the Gemfile.lock, use the one from the Gemfile.
2021-07-07 01:07:29 -04:00
changes = sources . replace_sources! ( @locked_sources )
2018-11-02 19:07:56 -04:00
sources . all_sources . each do | source |
# 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
# will cause the `ref` used to be the most recent for the branch (or master) if
# an explicit `ref` is not used.
if source . respond_to? ( :unlock! ) && @unlock [ :sources ] . include? ( source . name )
source . unlock!
changes = true
end
end
changes
end
def converge_dependencies
frozen = Bundler . frozen_bundle?
2021-09-29 14:07:08 -04:00
( @dependencies + locked_dependencies ) . each do | dep |
2018-11-02 19:07:56 -04:00
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
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 ]
changes = true
next
end
# Gem::Dependency#== matches Gem::Dependency#type. As the lockfile
# doesn't carry a notion of the dependency type, if you use
# add_development_dependency in a gemspec that's loaded with the gemspec
# 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 )
# 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
end
changes
end
# Remove elements from the locked specs that are expired. This will most
# commonly happen if the Gemfile has changed since the lockfile was last
# generated
def converge_locked_specs
deps = [ ]
# 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 ]
# 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
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
dep . source . unlock! if dep . source . respond_to? ( :unlock! )
dep . source . specs . each { | s | @unlock [ :gems ] << s . name }
end
end
converged = [ ]
@locked_specs . each do | s |
# Replace the locked dependency's source with the equivalent source from the Gemfile
dep = @dependencies . find { | d | s . satisfies? ( d ) }
2021-07-24 12:16:32 -04:00
s . source = ( dep && dep . source ) || sources . get ( s . source ) unless multisource_allowed?
2018-11-02 19:07:56 -04:00
# 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 the spec is from a path source and it doesn't exist anymore
# then we unlock it.
# Path sources have special logic
if s . source . instance_of? ( Source :: Path ) || s . source . instance_of? ( Source :: Gemspec )
2019-06-01 05:49:40 -04:00
new_specs = begin
2018-11-02 19:07:56 -04:00
s . source . specs
rescue PathError , GitError
# 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 .
2021-07-23 17:49:13 -04:00
for ( requested_dependencies , false , true ) .
2018-11-02 19:07:56 -04:00
none? { | locked_spec | locked_spec . source == s . source }
raise
end
2019-06-01 05:49:40 -04:00
new_spec = new_specs [ s ] . first
2018-11-02 19:07:56 -04:00
# If the spec is no longer in the path source, unlock it. This
# commonly happens if the version changed in the gemspec
2019-06-01 05:49:40 -04:00
next unless new_spec
2018-11-02 19:07:56 -04:00
2019-06-01 05:49:40 -04:00
s . dependencies . replace ( new_spec . dependencies )
2018-11-02 19:07:56 -04:00
end
converged << s
end
resolve = SpecSet . new ( converged )
2021-07-23 17:49:13 -04:00
resolve = SpecSet . new ( resolve . for ( expand_dependencies ( deps , true ) , false , false ) . reject { | s | @unlock [ :gems ] . include? ( s . name ) } )
2018-11-02 19:07:56 -04:00
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 }
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 ) ) }
end
def metadata_dependencies
@metadata_dependencies || = begin
2020-10-15 00:20:25 -04:00
ruby_versions = ruby_version_requirements ( @ruby_version )
2018-11-02 19:07:56 -04:00
[
2019-04-14 02:01:35 -04:00
Dependency . new ( " Ruby \0 " , ruby_versions ) ,
Dependency . new ( " RubyGems \0 " , Gem :: VERSION ) ,
2018-11-02 19:07:56 -04:00
]
end
end
2020-10-15 00:20:25 -04:00
def ruby_version_requirements ( ruby_version )
return [ ] unless ruby_version
2018-11-02 19:07:56 -04:00
if ruby_version . patchlevel
2020-10-15 00:20:25 -04:00
[ ruby_version . to_gem_version_with_patchlevel ]
2018-11-02 19:07:56 -04:00
else
2020-10-15 00:20:25 -04:00
ruby_version . versions . map do | version |
2018-11-02 19:07:56 -04:00
requirement = Gem :: Requirement . new ( version )
if requirement . exact?
" ~> #{ version } .0 "
else
requirement
end
2020-10-15 00:20:25 -04:00
end
2018-11-02 19:07:56 -04:00
end
end
2020-12-22 18:45:19 -05:00
def expand_dependencies ( dependencies , remote = false )
2018-11-02 19:07:56 -04:00
deps = [ ]
dependencies . each do | dep |
dep = Dependency . new ( dep , " >= 0 " ) unless dep . respond_to? ( :name )
2020-10-15 00:20:25 -04:00
next unless remote || dep . current_platform?
2021-02-01 10:17:16 -05:00
target_platforms = dep . gem_platforms ( remote ? @platforms : [ generic_local_platform ] )
2020-10-15 00:20:25 -04:00
deps += expand_dependency_with_platforms ( dep , target_platforms )
2018-11-02 19:07:56 -04:00
end
deps
end
2020-10-15 00:20:25 -04:00
def expand_dependency_with_platforms ( dep , platforms )
platforms . map do | p |
2021-02-01 10:17:16 -05:00
DepProxy . get_proxy ( dep , p )
2020-05-08 01:19:04 -04:00
end
end
2018-11-02 19:07:56 -04:00
def source_requirements
# Record the specs available in each gem's source, so that those
# specs will be available later when the resolver knows where to
# look for that gemspec (or its dependencies)
2021-05-28 06:47:49 -04:00
source_requirements = if precompute_source_requirements_for_indirect_dependencies?
{ :default = > sources . default_source } . merge ( source_map . all_requirements )
else
{ :default = > Source :: RubygemsAggregate . new ( sources , source_map ) } . merge ( source_map . direct_requirements )
end
2018-11-02 19:07:56 -04:00
metadata_dependencies . each do | dep |
source_requirements [ dep . name ] = sources . metadata_source
end
2021-05-28 06:47:49 -04:00
source_requirements [ :default_bundler ] = source_requirements [ " bundler " ] || sources . default_source
2018-11-02 19:07:56 -04:00
source_requirements [ " bundler " ] = sources . metadata_source # needs to come last to override
source_requirements
end
def requested_groups
groups - Bundler . settings [ :without ] - @optional_groups + Bundler . settings [ :with ]
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
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
proposed = proposed . gsub ( pattern , " \n " ) . gsub ( whitespace_cleanup , " \n \n " ) . strip
end
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
end
end
def additional_base_requirements_for_resolve
2021-07-07 01:07:29 -04:00
return [ ] unless @locked_gems && unlocking? && ! sources . expired_sources? ( @locked_gems . sources )
2018-11-02 19:07:56 -04:00
dependencies_by_name = dependencies . inject ( { } ) { | memo , dep | memo . update ( dep . name = > dep ) }
@locked_gems . specs . reduce ( { } ) do | requirements , locked_spec |
name = locked_spec . name
2019-04-14 02:01:35 -04:00
dependency = dependencies_by_name [ name ]
next requirements if @locked_gems . dependencies [ name ] != dependency
2021-07-07 01:07:29 -04:00
next requirements if dependency && dependency . source . is_a? ( Source :: Path )
2018-11-02 19:07:56 -04:00
dep = Gem :: Dependency . new ( name , " >= #{ locked_spec . version } " )
2021-02-01 10:17:16 -05:00
requirements [ name ] = DepProxy . get_proxy ( dep , locked_spec . platform )
2018-11-02 19:07:56 -04:00
requirements
end . values
end
def equivalent_rubygems_remotes? ( source )
return false unless source . is_a? ( Source :: Rubygems )
Bundler . settings [ :allow_deployment_source_credential_changes ] && source . equivalent_remotes? ( sources . rubygems_remotes )
end
2021-02-01 10:17:16 -05:00
2021-05-28 06:47:49 -04:00
def source_map
@source_map || = SourceMap . new ( sources , dependencies )
2021-02-01 10:17:16 -05:00
end
2018-11-02 19:07:56 -04:00
end
end