1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
|
# frozen_string_literal: true
# Some versions of the Bundler 1.1 RC series introduced corrupted
# lockfiles. There were two major problems:
#
# * multiple copies of the same GIT section appeared in the lockfile
# * when this happened, those sections got multiple copies of gems
# in those sections.
#
# As a result, Bundler 1.1 contains code that fixes the earlier
# corruption. We will remove this fix-up code in Bundler 1.2.
module Bundler
class LockfileParser
attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version, :ruby_version
BUNDLED = "BUNDLED WITH".freeze
DEPENDENCIES = "DEPENDENCIES".freeze
PLATFORMS = "PLATFORMS".freeze
RUBY = "RUBY VERSION".freeze
GIT = "GIT".freeze
GEM = "GEM".freeze
PATH = "PATH".freeze
PLUGIN = "PLUGIN SOURCE".freeze
SPECS = " specs:".freeze
OPTIONS = /^ ([a-z]+): (.*)$/i.freeze
SOURCE = [GIT, GEM, PATH, PLUGIN].freeze
SECTIONS_BY_VERSION_INTRODUCED = {
Gem::Version.create("1.0") => [DEPENDENCIES, PLATFORMS, GIT, GEM, PATH].freeze,
Gem::Version.create("1.10") => [BUNDLED].freeze,
Gem::Version.create("1.12") => [RUBY].freeze,
Gem::Version.create("1.13") => [PLUGIN].freeze,
}.freeze
KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten.freeze
ENVIRONMENT_VERSION_SECTIONS = [BUNDLED, RUBY].freeze
def self.sections_in_lockfile(lockfile_contents)
lockfile_contents.scan(/^\w[\w ]*$/).uniq
end
def self.unknown_sections_in_lockfile(lockfile_contents)
sections_in_lockfile(lockfile_contents) - KNOWN_SECTIONS
end
def self.sections_to_ignore(base_version = nil)
base_version &&= base_version.release
base_version ||= Gem::Version.create("1.0".dup)
attributes = []
SECTIONS_BY_VERSION_INTRODUCED.each do |version, introduced|
next if version <= base_version
attributes += introduced
end
attributes
end
def initialize(lockfile)
@platforms = []
@sources = []
@dependencies = {}
@state = nil
@specs = {}
@rubygems_aggregate = Source::Rubygems.new
if lockfile.match(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/)
raise LockfileError, "Your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} contains merge conflicts.\n" \
"Run `git checkout HEAD -- #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}` first to get a clean lock."
end
lockfile.split(/(?:\r?\n)+/).each do |line|
if SOURCE.include?(line)
@state = :source
parse_source(line)
elsif line == DEPENDENCIES
@state = :dependency
elsif line == PLATFORMS
@state = :platform
elsif line == RUBY
@state = :ruby
elsif line == BUNDLED
@state = :bundled_with
elsif line =~ /^[^\s]/
@state = nil
elsif @state
send("parse_#{@state}", line)
end
end
@sources << @rubygems_aggregate unless Bundler.feature_flag.disable_multisource?
@specs = @specs.values.sort_by(&:identifier)
warn_for_outdated_bundler_version
rescue ArgumentError => e
Bundler.ui.debug(e)
raise LockfileError, "Your lockfile is unreadable. Run `rm #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}` " \
"and then `bundle install` to generate a new lockfile."
end
def warn_for_outdated_bundler_version
return unless bundler_version
prerelease_text = bundler_version.prerelease? ? " --pre" : ""
current_version = Gem::Version.create(Bundler::VERSION)
return unless current_version < bundler_version
Bundler.ui.warn "Warning: the running version of Bundler (#{current_version}) is older " \
"than the version that created the lockfile (#{bundler_version}). We suggest you to " \
"upgrade to the version that created the lockfile by running `gem install " \
"bundler:#{bundler_version}#{prerelease_text}`.\n"
end
private
TYPES = {
GIT => Bundler::Source::Git,
GEM => Bundler::Source::Rubygems,
PATH => Bundler::Source::Path,
PLUGIN => Bundler::Plugin,
}.freeze
def parse_source(line)
case line
when SPECS
case @type
when PATH
@current_source = TYPES[@type].from_lock(@opts)
@sources << @current_source
when GIT
@current_source = TYPES[@type].from_lock(@opts)
# Strip out duplicate GIT sections
if @sources.include?(@current_source)
@current_source = @sources.find {|s| s == @current_source }
else
@sources << @current_source
end
when GEM
if Bundler.feature_flag.disable_multisource?
@opts["remotes"] = @opts.delete("remote")
@current_source = TYPES[@type].from_lock(@opts)
@sources << @current_source
else
Array(@opts["remote"]).each do |url|
@rubygems_aggregate.add_remote(url)
end
@current_source = @rubygems_aggregate
end
when PLUGIN
@current_source = Plugin.source_from_lock(@opts)
@sources << @current_source
end
when OPTIONS
value = $2
value = true if value == "true"
value = false if value == "false"
key = $1
if @opts[key]
@opts[key] = Array(@opts[key])
@opts[key] << value
else
@opts[key] = value
end
when *SOURCE
@current_source = nil
@opts = {}
@type = line
else
parse_spec(line)
end
end
space = / /
NAME_VERSION = /
^(#{space}{2}|#{space}{4}|#{space}{6})(?!#{space}) # Exactly 2, 4, or 6 spaces at the start of the line
(.*?) # Name
(?:#{space}\(([^-]*) # Space, followed by version
(?:-(.*))?\))? # Optional platform
(!)? # Optional pinned marker
$ # Line end
/xo.freeze
def parse_dependency(line)
return unless line =~ NAME_VERSION
spaces = $1
return unless spaces.size == 2
name = $2
version = $3
pinned = $5
version = version.split(",").map(&:strip) if version
dep = Bundler::Dependency.new(name, version)
if pinned && dep.name != "bundler"
spec = @specs.find {|_, v| v.name == dep.name }
dep.source = spec.last.source if spec
# Path sources need to know what the default name / version
# to use in the case that there are no gemspecs present. A fake
# gemspec is created based on the version set on the dependency
# TODO: Use the version from the spec instead of from the dependency
if version && version.size == 1 && version.first =~ /^\s*= (.+)\s*$/ && dep.source.is_a?(Bundler::Source::Path)
dep.source.name = name
dep.source.version = $1
end
end
@dependencies[dep.name] = dep
end
def parse_spec(line)
return unless line =~ NAME_VERSION
spaces = $1
name = $2
version = $3
platform = $4
if spaces.size == 4
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
@current_spec = LazySpecification.new(name, version, platform)
@current_spec.source = @current_source
# Avoid introducing multiple copies of the same spec (caused by
# duplicate GIT sections)
@specs[@current_spec.identifier] ||= @current_spec
elsif spaces.size == 6
version = version.split(",").map(&:strip) if version
dep = Gem::Dependency.new(name, version)
@current_spec.dependencies << dep
end
end
def parse_platform(line)
@platforms << Gem::Platform.new($1) if line =~ /^ (.*)$/
end
def parse_bundled_with(line)
line = line.strip
return unless Gem::Version.correct?(line)
@bundler_version = Gem::Version.create(line)
end
def parse_ruby(line)
@ruby_version = line.strip
end
end
end
|