diff options
author | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-16 18:49:26 +0900 |
---|---|---|
committer | Jari Vetoniemi <jari.vetoniemi@indooratlas.com> | 2020-03-30 00:39:06 +0900 |
commit | fcbf63e62c627deae76c1b8cb8c0876c536ed811 (patch) | |
tree | 64cb17de3f41a2b6fef2368028fbd00349946994 /jni/ruby/lib/rubygems/request_set |
Fresh start
Diffstat (limited to 'jni/ruby/lib/rubygems/request_set')
-rw-r--r-- | jni/ruby/lib/rubygems/request_set/gem_dependency_api.rb | 801 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/request_set/lockfile.rb | 650 |
2 files changed, 1451 insertions, 0 deletions
diff --git a/jni/ruby/lib/rubygems/request_set/gem_dependency_api.rb b/jni/ruby/lib/rubygems/request_set/gem_dependency_api.rb new file mode 100644 index 0000000..9aad5ab --- /dev/null +++ b/jni/ruby/lib/rubygems/request_set/gem_dependency_api.rb @@ -0,0 +1,801 @@ +## +# A semi-compatible DSL for the Bundler Gemfile and Isolate gem dependencies +# files. +# +# To work with both the Bundler Gemfile and Isolate formats this +# implementation takes some liberties to allow compatibility with each, most +# notably in #source. +# +# A basic gem dependencies file will look like the following: +# +# source 'https://rubygems.org' +# +# gem 'rails', '3.2.14a +# gem 'devise', '~> 2.1', '>= 2.1.3' +# gem 'cancan' +# gem 'airbrake' +# gem 'pg' +# +# RubyGems recommends saving this as gem.deps.rb over Gemfile or Isolate. +# +# To install the gems in this Gemfile use `gem install -g` to install it and +# create a lockfile. The lockfile will ensure that when you make changes to +# your gem dependencies file a minimum amount of change is made to the +# dependencies of your gems. +# +# RubyGems can activate all the gems in your dependencies file at startup +# using the RUBYGEMS_GEMDEPS environment variable or through Gem.use_gemdeps. +# See Gem.use_gemdeps for details and warnings. +# +# See `gem help install` and `gem help gem_dependencies` for further details. + +class Gem::RequestSet::GemDependencyAPI + + ENGINE_MAP = { # :nodoc: + :jruby => %w[jruby], + :jruby_18 => %w[jruby], + :jruby_19 => %w[jruby], + :maglev => %w[maglev], + :mri => %w[ruby], + :mri_18 => %w[ruby], + :mri_19 => %w[ruby], + :mri_20 => %w[ruby], + :mri_21 => %w[ruby], + :rbx => %w[rbx], + :ruby => %w[ruby rbx maglev], + :ruby_18 => %w[ruby rbx maglev], + :ruby_19 => %w[ruby rbx maglev], + :ruby_20 => %w[ruby rbx maglev], + :ruby_21 => %w[ruby rbx maglev], + } + + mswin = Gem::Platform.new 'x86-mswin32' + mswin64 = Gem::Platform.new 'x64-mswin64' + x86_mingw = Gem::Platform.new 'x86-mingw32' + x64_mingw = Gem::Platform.new 'x64-mingw32' + + PLATFORM_MAP = { # :nodoc: + :jruby => Gem::Platform::RUBY, + :jruby_18 => Gem::Platform::RUBY, + :jruby_19 => Gem::Platform::RUBY, + :maglev => Gem::Platform::RUBY, + :mingw => x86_mingw, + :mingw_18 => x86_mingw, + :mingw_19 => x86_mingw, + :mingw_20 => x86_mingw, + :mingw_21 => x86_mingw, + :mri => Gem::Platform::RUBY, + :mri_18 => Gem::Platform::RUBY, + :mri_19 => Gem::Platform::RUBY, + :mri_20 => Gem::Platform::RUBY, + :mri_21 => Gem::Platform::RUBY, + :mswin => mswin, + :mswin_18 => mswin, + :mswin_19 => mswin, + :mswin_20 => mswin, + :mswin_21 => mswin, + :mswin64 => mswin64, + :mswin64_19 => mswin64, + :mswin64_20 => mswin64, + :mswin64_21 => mswin64, + :rbx => Gem::Platform::RUBY, + :ruby => Gem::Platform::RUBY, + :ruby_18 => Gem::Platform::RUBY, + :ruby_19 => Gem::Platform::RUBY, + :ruby_20 => Gem::Platform::RUBY, + :ruby_21 => Gem::Platform::RUBY, + :x64_mingw => x64_mingw, + :x64_mingw_20 => x64_mingw, + :x64_mingw_21 => x64_mingw + } + + gt_eq_0 = Gem::Requirement.new '>= 0' + tilde_gt_1_8_0 = Gem::Requirement.new '~> 1.8.0' + tilde_gt_1_9_0 = Gem::Requirement.new '~> 1.9.0' + tilde_gt_2_0_0 = Gem::Requirement.new '~> 2.0.0' + tilde_gt_2_1_0 = Gem::Requirement.new '~> 2.1.0' + + VERSION_MAP = { # :nodoc: + :jruby => gt_eq_0, + :jruby_18 => tilde_gt_1_8_0, + :jruby_19 => tilde_gt_1_9_0, + :maglev => gt_eq_0, + :mingw => gt_eq_0, + :mingw_18 => tilde_gt_1_8_0, + :mingw_19 => tilde_gt_1_9_0, + :mingw_20 => tilde_gt_2_0_0, + :mingw_21 => tilde_gt_2_1_0, + :mri => gt_eq_0, + :mri_18 => tilde_gt_1_8_0, + :mri_19 => tilde_gt_1_9_0, + :mri_20 => tilde_gt_2_0_0, + :mri_21 => tilde_gt_2_1_0, + :mswin => gt_eq_0, + :mswin_18 => tilde_gt_1_8_0, + :mswin_19 => tilde_gt_1_9_0, + :mswin_20 => tilde_gt_2_0_0, + :mswin_21 => tilde_gt_2_1_0, + :mswin64 => gt_eq_0, + :mswin64_19 => tilde_gt_1_9_0, + :mswin64_20 => tilde_gt_2_0_0, + :mswin64_21 => tilde_gt_2_1_0, + :rbx => gt_eq_0, + :ruby => gt_eq_0, + :ruby_18 => tilde_gt_1_8_0, + :ruby_19 => tilde_gt_1_9_0, + :ruby_20 => tilde_gt_2_0_0, + :ruby_21 => tilde_gt_2_1_0, + :x64_mingw => gt_eq_0, + :x64_mingw_20 => tilde_gt_2_0_0, + :x64_mingw_21 => tilde_gt_2_1_0, + } + + WINDOWS = { # :nodoc: + :mingw => :only, + :mingw_18 => :only, + :mingw_19 => :only, + :mingw_20 => :only, + :mingw_21 => :only, + :mri => :never, + :mri_18 => :never, + :mri_19 => :never, + :mri_20 => :never, + :mri_21 => :never, + :mswin => :only, + :mswin_18 => :only, + :mswin_19 => :only, + :mswin_20 => :only, + :mswin_21 => :only, + :mswin64 => :only, + :mswin64_19 => :only, + :mswin64_20 => :only, + :mswin64_21 => :only, + :rbx => :never, + :ruby => :never, + :ruby_18 => :never, + :ruby_19 => :never, + :ruby_20 => :never, + :ruby_21 => :never, + :x64_mingw => :only, + :x64_mingw_20 => :only, + :x64_mingw_21 => :only, + } + + ## + # The gems required by #gem statements in the gem.deps.rb file + + attr_reader :dependencies + + ## + # A set of gems that are loaded via the +:git+ option to #gem + + attr_reader :git_set # :nodoc: + + ## + # A Hash containing gem names and files to require from those gems. + + attr_reader :requires # :nodoc: + + ## + # A set of gems that are loaded via the +:path+ option to #gem + + attr_reader :vendor_set # :nodoc: + + ## + # The groups of gems to exclude from installation + + attr_accessor :without_groups # :nodoc: + + ## + # Creates a new GemDependencyAPI that will add dependencies to the + # Gem::RequestSet +set+ based on the dependency API description in +path+. + + def initialize set, path + @set = set + @path = path + + @current_groups = nil + @current_platforms = nil + @current_repository = nil + @dependencies = {} + @default_sources = true + @git_set = @set.git_set + @git_sources = {} + @installing = false + @requires = Hash.new { |h, name| h[name] = [] } + @vendor_set = @set.vendor_set + @gem_sources = {} + @without_groups = [] + + git_source :github do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/" + + "git://github.com/#{repo_name}.git" + end + + git_source :bitbucket do |repo_name| + repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/" + + user, = repo_name.split "/", 2 + + "https://#{user}@bitbucket.org/#{repo_name}.git" + end + end + + ## + # Adds +dependencies+ to the request set if any of the +groups+ are allowed. + # This is used for gemspec dependencies. + + def add_dependencies groups, dependencies # :nodoc: + return unless (groups & @without_groups).empty? + + dependencies.each do |dep| + @set.gem dep.name, *dep.requirement + end + end + + private :add_dependencies + + ## + # Finds a gemspec with the given +name+ that lives at +path+. + + def find_gemspec name, path # :nodoc: + glob = File.join path, "#{name}.gemspec" + + spec_files = Dir[glob] + + case spec_files.length + when 1 then + spec_file = spec_files.first + + spec = Gem::Specification.load spec_file + + return spec if spec + + raise ArgumentError, "invalid gemspec #{spec_file}" + when 0 then + raise ArgumentError, "no gemspecs found at #{Dir.pwd}" + else + raise ArgumentError, + "found multiple gemspecs at #{Dir.pwd}, " + + "use the name: option to specify the one you want" + end + end + + ## + # Changes the behavior of gem dependency file loading to installing mode. + # In installing mode certain restrictions are ignored such as ruby version + # mismatch checks. + + def installing= installing # :nodoc: + @installing = installing + end + + ## + # Loads the gem dependency file and returns self. + + def load + instance_eval File.read(@path).untaint, @path, 1 + + self + end + + ## + # :category: Gem Dependencies DSL + # + # :call-seq: + # gem(name) + # gem(name, *requirements) + # gem(name, *requirements, options) + # + # Specifies a gem dependency with the given +name+ and +requirements+. You + # may also supply +options+ following the +requirements+ + # + # +options+ include: + # + # require: :: + # RubyGems does not provide any autorequire features so requires in a gem + # dependencies file are recorded but ignored. + # + # In bundler the require: option overrides the file to require during + # Bundler.require. By default the name of the dependency is required in + # Bundler. A single file or an Array of files may be given. + # + # To disable requiring any file give +false+: + # + # gem 'rake', require: false + # + # group: :: + # Place the dependencies in the given dependency group. A single group or + # an Array of groups may be given. + # + # See also #group + # + # platform: :: + # Only install the dependency on the given platform. A single platform or + # an Array of platforms may be given. + # + # See #platform for a list of platforms available. + # + # path: :: + # Install this dependency from an unpacked gem in the given directory. + # + # gem 'modified_gem', path: 'vendor/modified_gem' + # + # git: :: + # Install this dependency from a git repository: + # + # gem 'private_gem', git: git@my.company.example:private_gem.git' + # + # gist: :: + # Install this dependency from the gist ID: + # + # gem 'bang', gist: '1232884' + # + # github: :: + # Install this dependency from a github git repository: + # + # gem 'private_gem', github: 'my_company/private_gem' + # + # submodules: :: + # Set to +true+ to include submodules when fetching the git repository for + # git:, gist: and github: dependencies. + # + # ref: :: + # Use the given commit name or SHA for git:, gist: and github: + # dependencies. + # + # branch: :: + # Use the given branch for git:, gist: and github: dependencies. + # + # tag: :: + # Use the given tag for git:, gist: and github: dependencies. + + def gem name, *requirements + options = requirements.pop if requirements.last.kind_of?(Hash) + options ||= {} + + options[:git] = @current_repository if @current_repository + + source_set = false + + source_set ||= gem_path name, options + source_set ||= gem_git name, options + source_set ||= gem_git_source name, options + + duplicate = @dependencies.include? name + + @dependencies[name] = + if requirements.empty? and not source_set then + nil + elsif source_set then + '!' + else + requirements + end + + return unless gem_platforms options + + groups = gem_group name, options + + return unless (groups & @without_groups).empty? + + pin_gem_source name, :default unless source_set + + gem_requires name, options + + if duplicate then + warn <<-WARNING +Gem dependencies file #{@path} requires #{name} more than once. + WARNING + end + + @set.gem name, *requirements + end + + ## + # Handles the git: option from +options+ for gem +name+. + # + # Returns +true+ if the path option was handled. + + def gem_git name, options # :nodoc: + if gist = options.delete(:gist) then + options[:git] = "https://gist.github.com/#{gist}.git" + end + + return unless repository = options.delete(:git) + + pin_gem_source name, :git, repository + + reference = nil + reference ||= options.delete :ref + reference ||= options.delete :branch + reference ||= options.delete :tag + reference ||= 'master' + + submodules = options.delete :submodules + + @git_set.add_git_gem name, repository, reference, submodules + + true + end + + private :gem_git + + ## + # Handles a git gem option from +options+ for gem +name+ for a git source + # registered through git_source. + # + # Returns +true+ if the custom source option was handled. + + def gem_git_source name, options # :nodoc: + return unless git_source = (@git_sources.keys & options.keys).last + + source_callback = @git_sources[git_source] + source_param = options.delete git_source + + git_url = source_callback.call source_param + + options[:git] = git_url + + gem_git name, options + + true + end + + private :gem_git_source + + ## + # Handles the :group and :groups +options+ for the gem with the given + # +name+. + + def gem_group name, options # :nodoc: + g = options.delete :group + all_groups = g ? Array(g) : [] + + groups = options.delete :groups + all_groups |= groups if groups + + all_groups |= @current_groups if @current_groups + + all_groups + end + + private :gem_group + + ## + # Handles the path: option from +options+ for gem +name+. + # + # Returns +true+ if the path option was handled. + + def gem_path name, options # :nodoc: + return unless directory = options.delete(:path) + + pin_gem_source name, :path, directory + + @vendor_set.add_vendor_gem name, directory + + true + end + + private :gem_path + + ## + # Handles the platforms: option from +options+. Returns true if the + # platform matches the current platform. + + def gem_platforms options # :nodoc: + platform_names = Array(options.delete :platform) + platform_names.concat Array(options.delete :platforms) + platform_names.concat @current_platforms if @current_platforms + + return true if platform_names.empty? + + platform_names.any? do |platform_name| + raise ArgumentError, "unknown platform #{platform_name.inspect}" unless + platform = PLATFORM_MAP[platform_name] + + next false unless Gem::Platform.match platform + + if engines = ENGINE_MAP[platform_name] then + next false unless engines.include? Gem.ruby_engine + end + + case WINDOWS[platform_name] + when :only then + next false unless Gem.win_platform? + when :never then + next false if Gem.win_platform? + end + + VERSION_MAP[platform_name].satisfied_by? Gem.ruby_version + end + end + + private :gem_platforms + + ## + # Records the require: option from +options+ and adds those files, or the + # default file to the require list for +name+. + + def gem_requires name, options # :nodoc: + if options.include? :require then + if requires = options.delete(:require) then + @requires[name].concat Array requires + end + else + @requires[name] << name + end + end + + private :gem_requires + + ## + # :category: Gem Dependencies DSL + # + # Block form for specifying gems from a git +repository+. + # + # git 'https://github.com/rails/rails.git' do + # gem 'activesupport' + # gem 'activerecord' + # end + + def git repository + @current_repository = repository + + yield + + ensure + @current_repository = nil + end + + ## + # Defines a custom git source that uses +name+ to expand git repositories + # for use in gems built from git repositories. You must provide a block + # that accepts a git repository name for expansion. + + def git_source name, &callback + @git_sources[name] = callback + end + + ## + # Returns the basename of the file the dependencies were loaded from + + def gem_deps_file # :nodoc: + File.basename @path + end + + ## + # :category: Gem Dependencies DSL + # + # Loads dependencies from a gemspec file. + # + # +options+ include: + # + # name: :: + # The name portion of the gemspec file. Defaults to searching for any + # gemspec file in the current directory. + # + # gemspec name: 'my_gem' + # + # path: :: + # The path the gemspec lives in. Defaults to the current directory: + # + # gemspec 'my_gem', path: 'gemspecs', name: 'my_gem' + # + # development_group: :: + # The group to add development dependencies to. By default this is + # :development. Only one group may be specified. + + def gemspec options = {} + name = options.delete(:name) || '{,*}' + path = options.delete(:path) || '.' + development_group = options.delete(:development_group) || :development + + spec = find_gemspec name, path + + groups = gem_group spec.name, {} + + self_dep = Gem::Dependency.new spec.name, spec.version + + add_dependencies groups, [self_dep] + add_dependencies groups, spec.runtime_dependencies + + @dependencies[spec.name] = '!' + + spec.dependencies.each do |dep| + @dependencies[dep.name] = dep.requirement + end + + groups << development_group + + add_dependencies groups, spec.development_dependencies + + gem_requires spec.name, options + end + + ## + # :category: Gem Dependencies DSL + # + # Block form for placing a dependency in the given +groups+. + # + # group :development do + # gem 'debugger' + # end + # + # group :development, :test do + # gem 'minitest' + # end + # + # Groups can be excluded at install time using `gem install -g --without + # development`. See `gem help install` and `gem help gem_dependencies` for + # further details. + + def group *groups + @current_groups = groups + + yield + + ensure + @current_groups = nil + end + + ## + # Pins the gem +name+ to the given +source+. Adding a gem with the same + # name from a different +source+ will raise an exception. + + def pin_gem_source name, type = :default, source = nil + source_description = + case type + when :default then '(default)' + when :path then "path: #{source}" + when :git then "git: #{source}" + else '(unknown)' + end + + raise ArgumentError, + "duplicate source #{source_description} for gem #{name}" if + @gem_sources.fetch(name, source) != source + + @gem_sources[name] = source + end + + private :pin_gem_source + + ## + # :category: Gem Dependencies DSL + # + # Block form for restricting gems to a set of platforms. + # + # The gem dependencies platform is different from Gem::Platform. A platform + # gem.deps.rb platform matches on the ruby engine, the ruby version and + # whether or not windows is allowed. + # + # :ruby, :ruby_XY :: + # Matches non-windows, non-jruby implementations where X and Y can be used + # to match releases in the 1.8, 1.9, 2.0 or 2.1 series. + # + # :mri, :mri_XY :: + # Matches non-windows C Ruby (Matz Ruby) or only the 1.8, 1.9, 2.0 or + # 2.1 series. + # + # :mingw, :mingw_XY :: + # Matches 32 bit C Ruby on MinGW or only the 1.8, 1.9, 2.0 or 2.1 series. + # + # :x64_mingw, :x64_mingw_XY :: + # Matches 64 bit C Ruby on MinGW or only the 1.8, 1.9, 2.0 or 2.1 series. + # + # :mswin, :mswin_XY :: + # Matches 32 bit C Ruby on Microsoft Windows or only the 1.8, 1.9, 2.0 or + # 2.1 series. + # + # :mswin64, :mswin64_XY :: + # Matches 64 bit C Ruby on Microsoft Windows or only the 1.8, 1.9, 2.0 or + # 2.1 series. + # + # :jruby, :jruby_XY :: + # Matches JRuby or JRuby in 1.8 or 1.9 mode. + # + # :maglev :: + # Matches Maglev + # + # :rbx :: + # Matches non-windows Rubinius + # + # NOTE: There is inconsistency in what environment a platform matches. You + # may need to read the source to know the exact details. + + def platform *platforms + @current_platforms = platforms + + yield + + ensure + @current_platforms = nil + end + + ## + # :category: Gem Dependencies DSL + # + # Block form for restricting gems to a particular set of platforms. See + # #platform. + + alias :platforms :platform + + ## + # :category: Gem Dependencies DSL + # + # Restricts this gem dependencies file to the given ruby +version+. + # + # You may also provide +engine:+ and +engine_version:+ options to restrict + # this gem dependencies file to a particular ruby engine and its engine + # version. This matching is performed by using the RUBY_ENGINE and + # engine_specific VERSION constants. (For JRuby, JRUBY_VERSION). + + def ruby version, options = {} + engine = options[:engine] + engine_version = options[:engine_version] + + raise ArgumentError, + 'you must specify engine_version along with the ruby engine' if + engine and not engine_version + + return true if @installing + + unless RUBY_VERSION == version then + message = "Your Ruby version is #{RUBY_VERSION}, " + + "but your #{gem_deps_file} requires #{version}" + + raise Gem::RubyVersionMismatch, message + end + + if engine and engine != Gem.ruby_engine then + message = "Your ruby engine is #{Gem.ruby_engine}, " + + "but your #{gem_deps_file} requires #{engine}" + + raise Gem::RubyVersionMismatch, message + end + + if engine_version then + my_engine_version = Object.const_get "#{Gem.ruby_engine.upcase}_VERSION" + + if engine_version != my_engine_version then + message = + "Your ruby engine version is #{Gem.ruby_engine} #{my_engine_version}, " + + "but your #{gem_deps_file} requires #{engine} #{engine_version}" + + raise Gem::RubyVersionMismatch, message + end + end + + return true + end + + ## + # :category: Gem Dependencies DSL + # + # Sets +url+ as a source for gems for this dependency API. RubyGems uses + # the default configured sources if no source was given. If a source is set + # only that source is used. + # + # This method differs in behavior from Bundler: + # + # * The +:gemcutter+, # +:rubygems+ and +:rubyforge+ sources are not + # supported as they are deprecated in bundler. + # * The +prepend:+ option is not supported. If you wish to order sources + # then list them in your preferred order. + + def source url + Gem.sources.clear if @default_sources + + @default_sources = false + + Gem.sources << url + end + + # TODO: remove this typo name at RubyGems 3.0 + + Gem::RequestSet::GemDepedencyAPI = self # :nodoc: + +end + diff --git a/jni/ruby/lib/rubygems/request_set/lockfile.rb b/jni/ruby/lib/rubygems/request_set/lockfile.rb new file mode 100644 index 0000000..4f2fa09 --- /dev/null +++ b/jni/ruby/lib/rubygems/request_set/lockfile.rb @@ -0,0 +1,650 @@ +require 'strscan' + +## +# Parses a gem.deps.rb.lock file and constructs a LockSet containing the +# dependencies found inside. If the lock file is missing no LockSet is +# constructed. + +class Gem::RequestSet::Lockfile + + ## + # Raised when a lockfile cannot be parsed + + class ParseError < Gem::Exception + + ## + # The column where the error was encountered + + attr_reader :column + + ## + # The line where the error was encountered + + attr_reader :line + + ## + # The location of the lock file + + attr_reader :path + + ## + # Raises a ParseError with the given +message+ which was encountered at a + # +line+ and +column+ while parsing. + + def initialize message, column, line, path + @line = line + @column = column + @path = path + super "#{message} (at line #{line} column #{column})" + end + + end + + ## + # The platforms for this Lockfile + + attr_reader :platforms + + ## + # Creates a new Lockfile for the given +request_set+ and +gem_deps_file+ + # location. + + def initialize request_set, gem_deps_file, dependencies = nil + @set = request_set + @dependencies = dependencies + @gem_deps_file = File.expand_path(gem_deps_file) + @gem_deps_dir = File.dirname(@gem_deps_file) + + @gem_deps_file.untaint unless gem_deps_file.tainted? + + @current_token = nil + @line = 0 + @line_pos = 0 + @platforms = [] + @tokens = [] + end + + def add_DEPENDENCIES out # :nodoc: + out << "DEPENDENCIES" + + dependencies = + if @dependencies then + @dependencies.sort_by { |name,| name }.map do |name, requirement| + requirement_string = + if '!' == requirement then + requirement + else + Gem::Requirement.new(requirement).for_lockfile + end + + [name, requirement_string] + end + else + @requests.sort_by { |r| r.name }.map do |request| + spec = request.spec + name = request.name + requirement = request.request.dependency.requirement + + requirement_string = + if [Gem::Resolver::VendorSpecification, + Gem::Resolver::GitSpecification].include? spec.class then + "!" + else + requirement.for_lockfile + end + + [name, requirement_string] + end + end + + dependencies = dependencies.map do |name, requirement_string| + " #{name}#{requirement_string}" + end + + out.concat dependencies + + out << nil + end + + def add_GEM out # :nodoc: + return if @spec_groups.empty? + + source_groups = @spec_groups.values.flatten.group_by do |request| + request.spec.source.uri + end + + source_groups.sort_by { |group,| group.to_s }.map do |group, requests| + out << "GEM" + out << " remote: #{group}" + out << " specs:" + + requests.sort_by { |request| request.name }.each do |request| + next if request.spec.name == 'bundler' + platform = "-#{request.spec.platform}" unless + Gem::Platform::RUBY == request.spec.platform + + out << " #{request.name} (#{request.version}#{platform})" + + request.full_spec.dependencies.sort.each do |dependency| + next if dependency.type == :development + + requirement = dependency.requirement + out << " #{dependency.name}#{requirement.for_lockfile}" + end + end + out << nil + end + end + + def add_GIT out + return unless git_requests = + @spec_groups.delete(Gem::Resolver::GitSpecification) + + by_repository_revision = git_requests.group_by do |request| + source = request.spec.source + [source.repository, source.rev_parse] + end + + out << "GIT" + by_repository_revision.each do |(repository, revision), requests| + out << " remote: #{repository}" + out << " revision: #{revision}" + out << " specs:" + + requests.sort_by { |request| request.name }.each do |request| + out << " #{request.name} (#{request.version})" + + dependencies = request.spec.dependencies.sort_by { |dep| dep.name } + dependencies.each do |dep| + out << " #{dep.name}#{dep.requirement.for_lockfile}" + end + end + end + + out << nil + end + + def relative_path_from dest, base # :nodoc: + dest = File.expand_path(dest) + base = File.expand_path(base) + + if dest.index(base) == 0 then + offset = dest[base.size+1..-1] + + return '.' unless offset + + offset + else + dest + end + end + + def add_PATH out # :nodoc: + return unless path_requests = + @spec_groups.delete(Gem::Resolver::VendorSpecification) + + out << "PATH" + path_requests.each do |request| + directory = File.expand_path(request.spec.source.uri) + + out << " remote: #{relative_path_from directory, @gem_deps_dir}" + out << " specs:" + out << " #{request.name} (#{request.version})" + end + + out << nil + end + + def add_PLATFORMS out # :nodoc: + out << "PLATFORMS" + + platforms = @requests.map { |request| request.spec.platform }.uniq + + platforms = platforms.sort_by { |platform| platform.to_s } + + platforms.sort.each do |platform| + out << " #{platform}" + end + + out << nil + end + + ## + # Gets the next token for a Lockfile + + def get expected_types = nil, expected_value = nil # :nodoc: + @current_token = @tokens.shift + + type, value, column, line = @current_token + + if expected_types and not Array(expected_types).include? type then + unget + + message = "unexpected token [#{type.inspect}, #{value.inspect}], " + + "expected #{expected_types.inspect}" + + raise ParseError.new message, column, line, "#{@gem_deps_file}.lock" + end + + if expected_value and expected_value != value then + unget + + message = "unexpected token [#{type.inspect}, #{value.inspect}], " + + "expected [#{expected_types.inspect}, " + + "#{expected_value.inspect}]" + + raise ParseError.new message, column, line, "#{@gem_deps_file}.lock" + end + + @current_token + end + + def parse # :nodoc: + tokenize + + until @tokens.empty? do + type, data, column, line = get + + case type + when :section then + skip :newline + + case data + when 'DEPENDENCIES' then + parse_DEPENDENCIES + when 'GIT' then + parse_GIT + when 'GEM' then + parse_GEM + when 'PATH' then + parse_PATH + when 'PLATFORMS' then + parse_PLATFORMS + else + type, = get until @tokens.empty? or peek.first == :section + end + else + raise "BUG: unhandled token #{type} (#{data.inspect}) at line #{line} column #{column}" + end + end + end + + def parse_DEPENDENCIES # :nodoc: + while not @tokens.empty? and :text == peek.first do + _, name, = get :text + + requirements = [] + + case peek[0] + when :bang then + get :bang + + requirements << pinned_requirement(name) + when :l_paren then + get :l_paren + + loop do + _, op, = get :requirement + _, version, = get :text + + requirements << "#{op} #{version}" + + break unless peek[0] == :comma + + get :comma + end + + get :r_paren + + if peek[0] == :bang then + requirements.clear + requirements << pinned_requirement(name) + + get :bang + end + end + + @set.gem name, *requirements + + skip :newline + end + end + + def parse_GEM # :nodoc: + sources = [] + + while [:entry, 'remote'] == peek.first(2) do + get :entry, 'remote' + _, data, = get :text + skip :newline + + sources << Gem::Source.new(data) + end + + sources << Gem::Source.new(Gem::DEFAULT_HOST) if sources.empty? + + get :entry, 'specs' + + skip :newline + + set = Gem::Resolver::LockSet.new sources + last_specs = nil + + while not @tokens.empty? and :text == peek.first do + _, name, column, = get :text + + case peek[0] + when :newline then + last_specs.each do |spec| + spec.add_dependency Gem::Dependency.new name if column == 6 + end + when :l_paren then + get :l_paren + + type, data, = get [:text, :requirement] + + if type == :text and column == 4 then + version, platform = data.split '-', 2 + + platform = + platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY + + last_specs = set.add name, version, platform + else + dependency = parse_dependency name, data + + last_specs.each do |spec| + spec.add_dependency dependency + end + end + + get :r_paren + else + raise "BUG: unknown token #{peek}" + end + + skip :newline + end + + @set.sets << set + end + + def parse_GIT # :nodoc: + get :entry, 'remote' + _, repository, = get :text + + skip :newline + + get :entry, 'revision' + _, revision, = get :text + + skip :newline + + type, value = peek.first 2 + if type == :entry and %w[branch ref tag].include? value then + get + get :text + + skip :newline + end + + get :entry, 'specs' + + skip :newline + + set = Gem::Resolver::GitSet.new + set.root_dir = @set.install_dir + + last_spec = nil + + while not @tokens.empty? and :text == peek.first do + _, name, column, = get :text + + case peek[0] + when :newline then + last_spec.add_dependency Gem::Dependency.new name if column == 6 + when :l_paren then + get :l_paren + + type, data, = get [:text, :requirement] + + if type == :text and column == 4 then + last_spec = set.add_git_spec name, data, repository, revision, true + else + dependency = parse_dependency name, data + + last_spec.add_dependency dependency + end + + get :r_paren + else + raise "BUG: unknown token #{peek}" + end + + skip :newline + end + + @set.sets << set + end + + def parse_PATH # :nodoc: + get :entry, 'remote' + _, directory, = get :text + + skip :newline + + get :entry, 'specs' + + skip :newline + + set = Gem::Resolver::VendorSet.new + last_spec = nil + + while not @tokens.empty? and :text == peek.first do + _, name, column, = get :text + + case peek[0] + when :newline then + last_spec.add_dependency Gem::Dependency.new name if column == 6 + when :l_paren then + get :l_paren + + type, data, = get [:text, :requirement] + + if type == :text and column == 4 then + last_spec = set.add_vendor_gem name, directory + else + dependency = parse_dependency name, data + + last_spec.dependencies << dependency + end + + get :r_paren + else + raise "BUG: unknown token #{peek}" + end + + skip :newline + end + + @set.sets << set + end + + def parse_PLATFORMS # :nodoc: + while not @tokens.empty? and :text == peek.first do + _, name, = get :text + + @platforms << name + + skip :newline + end + end + + ## + # Parses the requirements following the dependency +name+ and the +op+ for + # the first token of the requirements and returns a Gem::Dependency object. + + def parse_dependency name, op # :nodoc: + return Gem::Dependency.new name, op unless peek[0] == :text + + _, version, = get :text + + requirements = ["#{op} #{version}"] + + while peek[0] == :comma do + get :comma + _, op, = get :requirement + _, version, = get :text + + requirements << "#{op} #{version}" + end + + Gem::Dependency.new name, requirements + end + + ## + # Peeks at the next token for Lockfile + + def peek # :nodoc: + @tokens.first || [:EOF] + end + + def pinned_requirement name # :nodoc: + spec = @set.sets.select { |set| + Gem::Resolver::GitSet === set or + Gem::Resolver::VendorSet === set + }.map { |set| + set.specs[name] + }.compact.first + + spec.version + end + + def skip type # :nodoc: + get while not @tokens.empty? and peek.first == type + end + + ## + # The contents of the lock file. + + def to_s + @set.resolve + + out = [] + + @requests = @set.sorted_requests + + @spec_groups = @requests.group_by do |request| + request.spec.class + end + + add_PATH out + + add_GIT out + + add_GEM out + + add_PLATFORMS out + + add_DEPENDENCIES out + + out.join "\n" + end + + ## + # Calculates the column (by byte) and the line of the current token based on + # +byte_offset+. + + def token_pos byte_offset # :nodoc: + [byte_offset - @line_pos, @line] + end + + ## + # Converts a lock file into an Array of tokens. If the lock file is missing + # an empty Array is returned. + + def tokenize # :nodoc: + @line = 0 + @line_pos = 0 + + @platforms = [] + @tokens = [] + @current_token = nil + + lock_file = "#{@gem_deps_file}.lock" + + @input = File.read lock_file + s = StringScanner.new @input + + until s.eos? do + pos = s.pos + + pos = s.pos if leading_whitespace = s.scan(/ +/) + + if s.scan(/[<|=>]{7}/) then + message = "your #{lock_file} contains merge conflict markers" + column, line = token_pos pos + + raise ParseError.new message, column, line, lock_file + end + + @tokens << + case + when s.scan(/\r?\n/) then + token = [:newline, nil, *token_pos(pos)] + @line_pos = s.pos + @line += 1 + token + when s.scan(/[A-Z]+/) then + if leading_whitespace then + text = s.matched + text += s.scan(/[^\s)]*/).to_s # in case of no match + [:text, text, *token_pos(pos)] + else + [:section, s.matched, *token_pos(pos)] + end + when s.scan(/([a-z]+):\s/) then + s.pos -= 1 # rewind for possible newline + [:entry, s[1], *token_pos(pos)] + when s.scan(/\(/) then + [:l_paren, nil, *token_pos(pos)] + when s.scan(/\)/) then + [:r_paren, nil, *token_pos(pos)] + when s.scan(/<=|>=|=|~>|<|>|!=/) then + [:requirement, s.matched, *token_pos(pos)] + when s.scan(/,/) then + [:comma, nil, *token_pos(pos)] + when s.scan(/!/) then + [:bang, nil, *token_pos(pos)] + when s.scan(/[^\s),!]*/) then + [:text, s.matched, *token_pos(pos)] + else + raise "BUG: can't create token for: #{s.string[s.pos..-1].inspect}" + end + end + + @tokens + rescue Errno::ENOENT + @tokens + end + + ## + # Ungets the last token retrieved by #get + + def unget # :nodoc: + @tokens.unshift @current_token + end + + ## + # Writes the lock file alongside the gem dependencies file + + def write + content = to_s + + open "#{@gem_deps_file}.lock", 'w' do |io| + io.write content + end + end + +end + |