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
@locked_specs_incomplete_for_platform = false
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
@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-09-29 14:53:29 -04:00
@locked_specs_incomplete_for_platform = ! @locked_specs . for ( expand_dependencies ( requested_dependencies & locked_dependencies ) , true , true )
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