mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
d35e7b5be2
with a 1.8.7 compatibility fix. * test/rubygems/test_gem_request_set_lockfile.rb: ditto. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@44159 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
579 lines
13 KiB
Ruby
579 lines
13 KiB
Ruby
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
|
|
@set = request_set
|
|
@gem_deps_file = File.expand_path(gem_deps_file)
|
|
@gem_deps_dir = File.dirname(@gem_deps_file)
|
|
|
|
@current_token = nil
|
|
@line = 0
|
|
@line_pos = 0
|
|
@platforms = []
|
|
@tokens = []
|
|
end
|
|
|
|
def add_DEPENDENCIES out # :nodoc:
|
|
out << "DEPENDENCIES"
|
|
|
|
@requests.sort_by { |r| r.name }.each do |request|
|
|
spec = request.spec
|
|
|
|
if [Gem::Resolver::VendorSpecification,
|
|
Gem::Resolver::GitSpecification].include? spec.class then
|
|
out << " #{request.name}!"
|
|
else
|
|
requirement = request.request.dependency.requirement
|
|
|
|
out << " #{request.name}#{requirement.for_lockfile}"
|
|
end
|
|
end
|
|
|
|
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|
|
|
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|
|
|
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
|
|
return dest[base.size+1..-1]
|
|
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.delete Gem::Platform::RUBY if platforms.length > 1
|
|
|
|
platforms.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
|
|
|
|
spec = @set.sets.select { |set|
|
|
Gem::Resolver::GitSet === set or
|
|
Gem::Resolver::VendorSet === set
|
|
}.map { |set|
|
|
set.specs[name]
|
|
}.first
|
|
|
|
requirements << spec.version
|
|
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
|
|
end
|
|
|
|
@set.gem name, *requirements
|
|
|
|
skip :newline
|
|
end
|
|
end
|
|
|
|
def parse_GEM # :nodoc:
|
|
get :entry, 'remote'
|
|
_, data, = get :text
|
|
|
|
source = Gem::Source.new data
|
|
|
|
skip :newline
|
|
|
|
get :entry, 'specs'
|
|
|
|
skip :newline
|
|
|
|
set = Gem::Resolver::LockSet.new source
|
|
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 name, data, Gem::Platform::RUBY
|
|
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_GIT # :nodoc:
|
|
get :entry, 'remote'
|
|
_, repository, = get :text
|
|
|
|
skip :newline
|
|
|
|
get :entry, 'revision'
|
|
_, revision, = get :text
|
|
|
|
skip :newline
|
|
|
|
get :entry, 'specs'
|
|
|
|
skip :newline
|
|
|
|
set = Gem::Resolver::GitSet.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_git_spec name, data, repository, revision, true
|
|
else
|
|
dependency = parse_dependency name, data
|
|
|
|
last_spec.spec.dependencies << 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.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 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 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
|
|
open "#{@gem_deps_file}.lock", 'w' do |io|
|
|
io.write to_s
|
|
end
|
|
end
|
|
|
|
end
|
|
|