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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
|
# frozen_string_literal: true
module Bundler
class LockfileParser
include GemHelpers
class Position
attr_reader :line, :column
def initialize(line, column)
@line = line
@column = column
end
def advance!(string)
lines = string.count("\n")
if lines > 0
@line += lines
@column = string.length - string.rindex("\n")
else
@column += string.length
end
end
def to_s
"#{line}:#{column}"
end
end
attr_reader(
:sources,
:dependencies,
:specs,
:platforms,
:most_specific_locked_platform,
:bundler_version,
:ruby_version,
:checksums,
)
BUNDLED = "BUNDLED WITH"
DEPENDENCIES = "DEPENDENCIES"
CHECKSUMS = "CHECKSUMS"
PLATFORMS = "PLATFORMS"
RUBY = "RUBY VERSION"
GIT = "GIT"
GEM = "GEM"
PATH = "PATH"
PLUGIN = "PLUGIN SOURCE"
SPECS = " specs:"
OPTIONS = /^ ([a-z]+): (.*)$/i
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,
Gem::Version.create("2.5.0") => [CHECKSUMS].freeze,
}.freeze
KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten!.freeze
ENVIRONMENT_VERSION_SECTIONS = [BUNDLED, RUBY].freeze
deprecate_constant(:ENVIRONMENT_VERSION_SECTIONS)
def self.sections_in_lockfile(lockfile_contents)
sections = lockfile_contents.scan(/^\w[\w ]*$/)
sections.uniq!
sections
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")
attributes = []
SECTIONS_BY_VERSION_INTRODUCED.each do |version, introduced|
next if version <= base_version
attributes += introduced
end
attributes
end
def self.bundled_with
lockfile = Bundler.default_lockfile
return unless lockfile.file?
lockfile_contents = Bundler.read_file(lockfile)
return unless lockfile_contents.include?(BUNDLED)
lockfile_contents.split(BUNDLED).last.strip
end
def initialize(lockfile)
@platforms = []
@sources = []
@dependencies = {}
@parse_method = nil
@specs = {}
@lockfile_path = begin
SharedHelpers.relative_lockfile_path
rescue GemfileNotFound
"Gemfile.lock"
end
@pos = Position.new(1, 1)
if lockfile.match?(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/)
raise LockfileError, "Your #{@lockfile_path} contains merge conflicts.\n" \
"Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock."
end
lockfile.split(/((?:\r?\n)+)/) do |line|
# split alternates between the line and the following whitespace
next @pos.advance!(line) if line.match?(/^\s*$/)
if SOURCE.include?(line)
@parse_method = :parse_source
parse_source(line)
elsif line == DEPENDENCIES
@parse_method = :parse_dependency
elsif line == CHECKSUMS
# This is a temporary solution to make this feature disabled by default
# for all gemfiles that don't already explicitly include the feature.
@checksums = true
@parse_method = :parse_checksum
elsif line == PLATFORMS
@parse_method = :parse_platform
elsif line == RUBY
@parse_method = :parse_ruby
elsif line == BUNDLED
@parse_method = :parse_bundled_with
elsif /^[^\s]/.match?(line)
@parse_method = nil
elsif @parse_method
send(@parse_method, line)
end
@pos.advance!(line)
end
@most_specific_locked_platform = @platforms.min_by do |bundle_platform|
platform_specificity_match(bundle_platform, local_platform)
end
@specs = @specs.values.sort_by!(&:full_name).each do |spec|
spec.most_specific_locked_platform = @most_specific_locked_platform
end
rescue ArgumentError => e
Bundler.ui.debug(e)
raise LockfileError, "Your lockfile is unreadable. Run `rm #{@lockfile_path}` " \
"and then `bundle install` to generate a new lockfile. The error occurred while " \
"evaluating #{@lockfile_path}:#{@pos}"
end
def may_include_redundant_platform_specific_gems?
bundler_version.nil? || bundler_version < Gem::Version.new("1.16.2")
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
return unless TYPES.key?(@type)
@current_source = TYPES[@type].from_lock(@opts)
@sources << @current_source
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
(?:#{space}([^ ]+))? # Optional checksum
$ # Line end
/xo
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(",").each(&: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_checksum(line)
return unless line =~ NAME_VERSION
spaces = $1
return unless spaces.size == 2
checksums = $6
name = $2
version = $3
platform = $4
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
full_name = Gem::NameTuple.new(name, version, platform).full_name
return unless spec = @specs[full_name]
if checksums
checksums.split(",") do |lock_checksum|
column = line.index(lock_checksum) + 1
checksum = Checksum.from_lock(lock_checksum, "#{@lockfile_path}:#{@pos.line}:#{column}")
spec.source.checksum_store.register(spec, checksum)
end
else
spec.source.checksum_store.register(spec, nil)
end
end
def parse_spec(line)
return unless line =~ NAME_VERSION
spaces = $1
name = -$2
version = $3
if spaces.size == 4
# only load platform for non-dependency (spec) line
platform = $4
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
@current_spec = LazySpecification.new(name, version, platform, @current_source)
@current_source.add_dependency_names(name)
@specs[@current_spec.full_name] = @current_spec
elsif spaces.size == 6
version = version.split(",").each(&:strip!) if version
dep = Gem::Dependency.new(name, version)
@current_spec.dependencies << dep
end
end
def parse_platform(line)
@platforms << Gem::Platform.new($1.strip) if line =~ /^ (.*)$/
end
def parse_bundled_with(line)
line.strip!
return unless Gem::Version.correct?(line)
@bundler_version = Gem::Version.create(line)
end
def parse_ruby(line)
line.strip!
@ruby_version = line
end
end
end
|