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]
# File bundler/definition.rb, line 31 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
# File bundler/definition.rb, line 58 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) @unlocking = unlock.any? {|_k, v| !Array(v).empty? } 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 @new_platform = nil if lockfile && File.exist?(lockfile) @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 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 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? 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." Bundler::SharedHelpers.major_deprecation 2, msg end @sources.merged_gem_lockfile_sections!(locked_gem_sources.first) end @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) add_current_platform unless current_ruby_platform_locked? || Bundler.frozen_bundle? converge_path_sources_to_gemspec_sources @path_changes = converge_paths @source_changes = converge_sources 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) end @dependency_changes = converge_dependencies @local_changes = converge_locals @locked_specs_incomplete_for_platform = !@locked_specs.for(requested_dependencies & expand_dependencies(locked_dependencies), true, true) @requires = compute_requires end
# File bundler/definition.rb, line 437 def add_platform(platform) @new_platform ||= !@platforms.include?(platform) @platforms |= [platform] end
# File bundler/definition.rb, line 226 def current_dependencies dependencies.select do |d| d.should_include? && !d.gem_platforms(@platforms).empty? end end
# File bundler/definition.rb, line 242 def dependencies_for(groups) groups.map!(&:to_sym) deps = current_dependencies.reject do |d| (d.groups & groups).empty? end expand_dependencies(deps) end
# File bundler/definition.rb, line 337 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 = [] 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}" } new_deps = @dependencies - locked_dependencies deleted_deps = locked_dependencies - @dependencies added.concat new_deps.map {|d| "* #{pretty_dep(d)}" } if new_deps.any? 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_dependencies.each do |d| next if !Bundler.feature_flag.bundler_3_mode? && @locked_specs[d.name].empty? both_sources[d.name][1] = d end both_sources.each do |name, (dep, lock_dep)| next if dep.nil? || lock_dep.nil? gemfile_source = dep.source || sources.default_source lock_source = lock_dep.source || sources.default_source next if lock_source.include?(gemfile_source) gemfile_source_name = dep.source ? gemfile_source.identifier : "no specified source" lockfile_source_name = lock_dep.source ? lock_source.identifier : "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 << "\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" raise ProductionError, msg if added.any? || deleted.any? || changed.any? || !nothing_changed? end
# File bundler/definition.rb, line 150 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
# File bundler/definition.rb, line 277 def groups dependencies.map(&:groups).flatten.uniq end
# File bundler/definition.rb, line 281 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 updating_major = locked_major < current_major 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
# File bundler/definition.rb, line 232 def locked_dependencies @locked_deps.values end
# File bundler/definition.rb, line 311 def locked_ruby_version return unless ruby_version if @unlock[:ruby] || !@locked_ruby_version Bundler::RubyVersion.system else @locked_ruby_version end end
# File bundler/definition.rb, line 320 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
# File bundler/definition.rb, line 200 def missing_specs resolve.materialize(requested_dependencies).missing_specs end
# File bundler/definition.rb, line 204 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
# File bundler/definition.rb, line 447 def most_specific_locked_platform @platforms.min_by do |bundle_platform| platform_specificity_match(bundle_platform, local_platform) end end
# File bundler/definition.rb, line 192 def new_specs specs - @locked_specs end
# File bundler/definition.rb, line 456 def nothing_changed? !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@locked_specs_incomplete_for_platform end
# File bundler/definition.rb, line 442 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
# File bundler/definition.rb, line 196 def removed_specs @locked_specs - specs end
# File bundler/definition.rb, line 222 def requested_dependencies dependencies_for(requested_groups) end
# File bundler/definition.rb, line 218 def requested_specs specs_for(requested_groups) end
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
# File bundler/definition.rb, line 255 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 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, true) Resolver.resolve(expanded_dependencies, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) end end end
# File bundler/definition.rb, line 165 def resolve_only_locally! @remote = false sources.local_only! resolve end
# File bundler/definition.rb, line 176 def resolve_remotely! @remote = true sources.remote! resolve end
# File bundler/definition.rb, line 171 def resolve_with_cache! sources.cached! resolve end
# File bundler/definition.rb, line 273 def spec_git_paths sources.git_sources.map {|s| File.realpath(s.path) if File.exist?(s.path) }.compact 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]
# File bundler/definition.rb, line 188 def specs @specs ||= materialize(requested_dependencies) end
# File bundler/definition.rb, line 236 def specs_for(groups) return specs if groups.empty? deps = dependencies_for(groups) materialize(deps) end
# File bundler/definition.rb, line 332 def to_lock require_relative "lockfile_generator" LockfileGenerator.generate(self) end
# File bundler/definition.rb, line 460 def unlocking? @unlocking end
# File bundler/definition.rb, line 429 def validate_platforms! 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." end
# File bundler/definition.rb, line 404 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