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 lib/bundler/definition.rb, line 18 def self.build(gemfile, lockfile, unlock) unlock ||= {} gemfile = Pathname.new(gemfile).expand_path unless gemfile.file? raise GemfileNotFound, "#{gemfile} not found" end 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 lib/bundler/definition.rb, line 48 def initialize(lockfile, dependencies, sources, unlock, ruby_version = nil, optional_groups = []) @unlocking = unlock == true || !unlock.empty? @dependencies = dependencies @sources = sources @unlock = unlock @optional_groups = optional_groups @remote = false @specs = nil @ruby_version = ruby_version @lockfile_contents = "" @locked_bundler_version = nil if lockfile && File.exist?(lockfile) @lockfile_contents = Bundler.read_file(lockfile) locked = LockfileParser.new(@lockfile_contents) @platforms = locked.platforms @locked_bundler_version = locked.bundler_version if unlock != true @locked_deps = locked.dependencies @locked_specs = SpecSet.new(locked.specs) @locked_sources = locked.sources else @unlock = {} @locked_deps = [] @locked_specs = SpecSet.new([]) @locked_sources = [] end else @unlock = {} @platforms = [] @locked_deps = [] @locked_specs = SpecSet.new([]) @locked_sources = [] end @unlock[:gems] ||= [] @unlock[:sources] ||= [] current_platform = Bundler.rubygems.platforms.map { |p| generic(p) }.compact.last @new_platform = !@platforms.include?(current_platform) @platforms |= [current_platform] @path_changes = converge_paths eager_unlock = expand_dependencies(@unlock[:gems]) @unlock[:gems] = @locked_specs.for(eager_unlock).map { |s| s.name } @source_changes = converge_sources @dependency_changes = converge_dependencies @local_changes = converge_locals fixup_dependency_types! end
Public Instance Methods
# File lib/bundler/definition.rb, line 178 def current_dependencies dependencies.reject { |d| !d.should_include? } end
# File lib/bundler/definition.rb, line 332 def ensure_equivalent_gemfile_and_lockfile(explicit_flag = false) msg = "You are trying to install in deployment mode after changing\n" "your Gemfile. Run `bundle install` elsewhere and add the\n" "updated Gemfile.lock to version control." unless explicit_flag msg += "\n\nIf this is a development machine, remove the Gemfile " "freeze \nby running `bundle install --no-deployment`." end added = [] deleted = [] changed = [] gemfile_sources = sources.lock_sources if @locked_sources != gemfile_sources new_sources = gemfile_sources - @locked_sources deleted_sources = @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 new_deps = @dependencies - @locked_deps deleted_deps = @locked_deps - @dependencies if new_deps.any? added.concat new_deps.map { |d| "* #{pretty_dep(d)}" } end 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 { |d| both_sources[d.name][1] = d.source } both_sources.each do |name, (dep, lock_source)| if (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 end 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? end
# File lib/bundler/definition.rb, line 104 def fixup_dependency_types! # XXX This is a temporary workaround for a bug when using rubygems 1.8.15 # where 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. # Test coverage to catch a regression on this is in gemspec_spec.rb @dependencies.each do |d| if ld = @locked_deps.find { |l| l.name == d.name } ld.instance_variable_set(:@type, d.type) end end end
# File lib/bundler/definition.rb, line 240 def groups dependencies.map { |d| d.groups }.flatten.uniq end
# File lib/bundler/definition.rb, line 232 def has_local_dependencies? !sources.path_sources.empty? || !sources.git_sources.empty? end
# File lib/bundler/definition.rb, line 228 def has_rubygems_remotes? sources.rubygems_sources.any? {|s| s.remotes.any? } end
# File lib/bundler/definition.rb, line 205 def index @index ||= Index.build do |idx| dependency_names = @dependencies.map { |d| d.name } sources.all_sources.each do |source| source.dependency_names = dependency_names.dup idx.add_source source.specs dependency_names -= pinned_spec_names(source.specs) dependency_names.push(*source.unmet_deps).uniq! end end end
# File lib/bundler/definition.rb, line 244 def lock(file, preserve_bundled_with = 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_bundled_with ||= !updating_major && (Bundler.settings[:frozen] || !@unlocking) return if lockfiles_equal?(@lockfile_contents, contents, preserve_bundled_with) if Bundler.settings[:frozen] Bundler.ui.error "Cannot write a changed lockfile while frozen." return end File.open(file, 'wb'){|f| f.puts(contents) } rescue Errno::EACCES raise Bundler::InstallError, "There was an error while trying to write to Gemfile.lock. It is likely that \n" "you need to allow write permissions for the file at path: \n" "#{File.expand_path(file)}" end
Returns the version of Bundler that is creating or has created Gemfile.lock. Used in to_lock.
# File lib/bundler/definition.rb, line 279 def lock_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 lib/bundler/definition.rb, line 164 def missing_specs missing = [] resolve.materialize(requested_dependencies, missing) missing end
# File lib/bundler/definition.rb, line 160 def new_platform? @new_platform end
# File lib/bundler/definition.rb, line 152 def new_specs specs - @locked_specs end
# File lib/bundler/definition.rb, line 156 def removed_specs @locked_specs - specs end
# File lib/bundler/definition.rb, line 170 def requested_specs @requested_specs ||= begin groups = requested_groups groups.map! { |g| g.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 lib/bundler/definition.rb, line 193 def resolve @resolve ||= begin last_resolve = converge_locked_specs if Bundler.settings[:frozen] || (!@unlocking && nothing_changed?) last_resolve else # Run a resolve against the locally available gems last_resolve.merge Resolver.resolve(expanded_dependencies, index, source_requirements, last_resolve) end end end
# File lib/bundler/definition.rb, line 125 def resolve_remotely! raise "Specs already loaded" if @specs @remote = true sources.remote! specs end
# File lib/bundler/definition.rb, line 119 def resolve_with_cache! raise "Specs already loaded" if @specs sources.cached! specs end
used when frozen is enabled so we can find the bundler spec, even if (say) a git gem is not checked out.
# File lib/bundler/definition.rb, line 220 def rubygems_index @rubygems_index ||= Index.build do |idx| sources.rubygems_sources.each do |rubygems| idx.add_source rubygems.specs end end end
# File lib/bundler/definition.rb, line 236 def spec_git_paths sources.git_sources.map {|s| s.path.to_s } 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 lib/bundler/definition.rb, line 138 def specs @specs ||= begin specs = resolve.materialize(Bundler.settings[:cache_all_platforms] ? dependencies : requested_dependencies) unless specs["bundler"].any? local = Bundler.settings[:frozen] ? rubygems_index : index bundler = local.search(Gem::Dependency.new('bundler', VERSION)).last specs["bundler"] = bundler if bundler end specs end end
# File lib/bundler/definition.rb, line 182 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 lib/bundler/definition.rb, line 287 def to_lock out = "" sources.lock_sources.each do |source| # Add the source header out << source.to_lock # Find all specs for this source resolve. select { |s| source.can_lock?(s) }. # This needs to be sorted by full name so that # gems with the same name, but different platform # are ordered consistently sort_by { |s| s.full_name }. each do |spec| next if spec.name == 'bundler' out << spec.to_lock end out << "\n" end out << "PLATFORMS\n" platforms.map { |p| p.to_s }.sort.each do |p| out << " #{p}\n" end out << "\n" out << "DEPENDENCIES\n" handled = [] dependencies. sort_by { |d| d.to_s }. each do |dep| next if handled.include?(dep.name) out << dep.to_lock handled << dep.name end # Record the version of Bundler that was used to create the lockfile out << "\nBUNDLED WITH\n" out << " #{lock_version}\n" out end
# File lib/bundler/definition.rb, line 391 def validate_ruby! return unless ruby_version if diff = ruby_version.diff(Bundler.ruby_version) 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.ruby_version.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