class Bundler::Definition
Attributes
Public Class Methods
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 28 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 55 def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = [], gemfiles = []) if [true, false].include?(unlock) @unlocking_bundler = false @unlocking = unlock else unlock = unlock.dup @unlocking_bundler = unlock.delete(:bundler) unlock.delete_if {|_k, v| Array(v).empty? } @unlocking = !unlock.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 @locked_specs_incomplete_for_platform = false 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 @unlock[:gems] ||= [] @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 Bundler.frozen_bundle? converge_path_sources_to_gemspec_sources @path_changes = converge_paths @source_changes = converge_sources unless @unlock[:lock_shared_dependencies] eager_unlock = expand_dependencies(@unlock[:gems], true) @unlock[:gems] = @locked_specs.for(eager_unlock, [], false, false, false).map(&:name) end @dependency_changes = converge_dependencies @local_changes = converge_locals @requires = compute_requires end
Public Instance Methods
# File bundler/definition.rb, line 521 def add_current_platform current_platforms.each {|platform| add_platform(platform) } end
# File bundler/definition.rb, line 511 def add_platform(platform) @new_platform ||= !@platforms.include?(platform) @platforms |= [platform] end
# File bundler/definition.rb, line 230 def current_dependencies dependencies.select(&:should_include?) end
# File bundler/definition.rb, line 392 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")[:global] "bundle config unset frozen" elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? "bundle config unset deployment" else "bundle install --no-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}" } gemfile_sources = sources.lock_sources new_sources = gemfile_sources - @locked_sources deleted_sources = @locked_sources - gemfile_sources new_deps = @dependencies - @locked_deps.values deleted_deps = @locked_deps.values - @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 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)| next unless (dep.nil? && !lock_source.nil?) || (!dep.nil? && !lock_source.nil? && !lock_source.can_lock?(dep)) gemfile_source_name = (dep && dep.source) || "no specified source" lockfile_source_name = lock_source || "no specified source" 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\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 529 def find_indexed_specs(current_spec) index[current_spec.name].select {|spec| spec.match_platform(current_spec.platform) }.sort_by(&:version) end
# File bundler/definition.rb, line 525 def find_resolved_spec(current_spec) specs.find_by_name_and_platform(current_spec.name, current_spec.platform) end
# File bundler/definition.rb, line 133 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 323 def groups dependencies.map(&:groups).flatten.uniq end
# File bundler/definition.rb, line 315 def has_local_dependencies? !sources.path_sources.empty? || !sources.git_sources.empty? end
# File bundler/definition.rb, line 311 def has_rubygems_remotes? sources.rubygems_sources.any? {|s| s.remotes.any? } end
# File bundler/definition.rb, line 267 def index @index ||= Index.build do |idx| dependency_names = @dependencies.map(&:name) sources.all_sources.each do |source| source.dependency_names = dependency_names - pinned_spec_names(source) idx.add_source source.specs dependency_names.concat(source.unmet_deps).uniq! end double_check_for_index(idx, dependency_names) end end
# File bundler/definition.rb, line 327 def lock(file, preserve_unknown_sections = false) 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 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
# File bundler/definition.rb, line 358 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
# File bundler/definition.rb, line 366 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 375 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 201 def missing_specs missing = [] resolve.materialize(requested_dependencies, missing) missing end
# File bundler/definition.rb, line 207 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 @index = nil @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 197 def new_platform? @new_platform end
# File bundler/definition.rb, line 189 def new_specs specs - @locked_specs end
# File bundler/definition.rb, line 536 def nothing_changed? !@source_changes && !@dependency_changes && !@new_platform && !@path_changes && !@local_changes && !@locked_specs_incomplete_for_platform end
# File bundler/definition.rb, line 516 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 193 def removed_specs @locked_specs - specs end
# File bundler/definition.rb, line 222 def requested_specs @requested_specs ||= begin groups = requested_groups groups.map!(&:to_sym) specs_for(groups) end 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 245 def resolve @resolve ||= begin last_resolve = converge_locked_specs resolve = 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}") last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve, gem_version_promoter, additional_base_requirements_for_resolve, platforms) end # filter out gems that _can_ be installed on multiple platforms, but don't need # to be resolve.for(expand_dependencies(dependencies, true), [], false, false, false) end end
# File bundler/definition.rb, line 154 def resolve_remotely! raise "Specs already loaded" if @specs @remote = true sources.remote! specs end
# File bundler/definition.rb, line 148 def resolve_with_cache! raise "Specs already loaded" if @specs sources.cached! specs end
# File bundler/definition.rb, line 319 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 167 def specs @specs ||= begin begin specs = resolve.materialize(requested_dependencies) rescue GemNotFound => e # Handle yanked gem gem_name, gem_version = extract_gem_info(e) locked_gem = @locked_specs[gem_name].last raise if locked_gem.nil? || locked_gem.version.to_s != gem_version || !@remote raise GemNotFound, "Your bundle is locked to #{locked_gem}, but that version could not " \ "be found in any of the sources listed in your Gemfile. If you haven't changed sources, " \ "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 unless specs["bundler"].any? bundler = sources.metadata_source.specs.search(Gem::Dependency.new("bundler", VERSION)).last specs["bundler"] = bundler end specs end end
# File bundler/definition.rb, line 234 def specs_for(groups) deps = dependencies.select {|d| (d.groups & groups).any? } deps.delete_if {|d| !d.should_include? } specs.for(expand_dependencies(deps)) end
# File bundler/definition.rb, line 387 def to_lock require_relative "lockfile_generator" LockfileGenerator.generate(self) end
# File bundler/definition.rb, line 540 def unlocking? @unlocking end
# File bundler/definition.rb, line 499 def validate_platforms! return if @platforms.any? do |bundle_platform| Bundler.rubygems.platforms.any? do |local_platform| MatchPlatform.platforms_match?(bundle_platform, local_platform) end end raise ProductionError, "Your bundle only supports platforms #{@platforms.map(&:to_s)} " \ "but your local platforms are #{Bundler.rubygems.platforms.map(&:to_s)}, and " \ "there's no compatible match between those two lists." end
# File bundler/definition.rb, line 474 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
# File bundler/definition.rb, line 469 def validate_runtime! validate_ruby! validate_platforms! end