rubylens 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,33 +2,17 @@
2
2
 
3
3
  require "bundler"
4
4
  require "bundler/lockfile_parser"
5
- require "find"
6
5
  require "pathname"
7
6
  require "set"
8
7
  require_relative "../dependency_warning"
8
+ require_relative "git_package_source"
9
9
 
10
10
  module RubyLens
11
11
  module Index
12
12
  class Manifest
13
13
  Package = Data.define(:name, :version, :role, :location, :root, :files)
14
14
  DependencySystem = Data.define(:id, :package_indexes, :label_package_index)
15
- GitPackagePaths = Data.define(:logical_root, :canonical_root, :logical_canonical_root)
16
15
 
17
- class NixStoreProvider
18
- STORE_ROOT = Pathname("/nix/store").freeze
19
- STORE_OBJECT_PATTERN = /\A[0123456789abcdfghijklmnpqrsvwxyz]{32}-.+\z/
20
-
21
- def trusted?(path)
22
- path = Pathname(path).expand_path
23
- return false unless Paths.inside?(path, STORE_ROOT)
24
-
25
- first_component = path.relative_path_from(STORE_ROOT).each_filename.first
26
- first_component && STORE_OBJECT_PATTERN.match?(first_component)
27
- end
28
- end
29
-
30
- UnsafeGitRequirePath = Class.new(StandardError)
31
- UnsafeGitPackageFile = Class.new(StandardError)
32
16
  GIT_SKIP_REASONS = DependencyWarning::REASONS
33
17
 
34
18
  attr_reader :root, :files, :workspace_files, :packages, :dependency_systems, :warnings, :dependency_warnings
@@ -40,7 +24,7 @@ module RubyLens
40
24
  def initialize(root:, lockfile: nil)
41
25
  @root = Pathname(root).expand_path.realpath
42
26
  @lockfile = Pathname(lockfile || @root.join("Gemfile.lock")).expand_path
43
- @nix_store_provider = NixStoreProvider.new
27
+ @git_package_source = GitPackageSource.new(lockfile: @lockfile)
44
28
  @warnings = []
45
29
  @dependency_warnings = []
46
30
  @packages = []
@@ -49,7 +33,7 @@ module RubyLens
49
33
  @package_index_by_file = {}
50
34
  @package_index_cache = {}
51
35
  @relative_workspace_path_cache = {}
52
- @git_spec_indexes = {}.compare_by_identity
36
+ @workspace_path_cache = {}
53
37
  end
54
38
 
55
39
  def build
@@ -80,7 +64,10 @@ module RubyLens
80
64
  end
81
65
 
82
66
  def workspace_path?(path)
83
- Paths.inside?(path, @root)
67
+ path = path.to_s
68
+ @workspace_path_cache.fetch(path) do
69
+ @workspace_path_cache[path] = Paths.inside?(path, @root)
70
+ end
84
71
  end
85
72
 
86
73
  def relative_workspace_path(path)
@@ -189,134 +176,17 @@ module RubyLens
189
176
  end
190
177
 
191
178
  def git_package(locked, role)
192
- source = locked.source
193
- return skip_git_dependency(locked, :local_only_required) if source.allow_git_ops?
194
-
195
- checkout = git_checkout_path(source)
196
- return skip_git_dependency(locked, :checkout_unavailable) unless checkout.directory?
197
-
198
- specification = (@git_spec_indexes[source] ||= git_specifications(source, checkout)).search(locked).first
199
- return skip_git_dependency(locked, :specification_unavailable) unless specification
200
-
201
- paths = git_package_paths(specification, checkout)
202
- return skip_git_dependency(locked, :unsafe_specification_root) unless paths
203
-
204
- files = git_indexable_files(paths, specification.require_paths)
179
+ resolution = @git_package_source.resolve(locked)
180
+ return skip_git_dependency(locked, resolution) if resolution.is_a?(Symbol)
205
181
 
206
182
  Package.new(
207
183
  name: locked.name,
208
184
  version: locked.version.to_s,
209
185
  role: role,
210
186
  location: "external",
211
- root: paths.canonical_root,
212
- files: files.freeze,
187
+ root: resolution.root,
188
+ files: resolution.files,
213
189
  )
214
- rescue UnsafeGitRequirePath
215
- skip_git_dependency(locked, :unsafe_require_paths)
216
- rescue UnsafeGitPackageFile
217
- skip_git_dependency(locked, :unsafe_package_files)
218
- rescue Bundler::BundlerError, Gem::Exception
219
- skip_git_dependency(locked, :specification_unreadable)
220
- rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
221
- skip_git_dependency(locked, :checkout_unavailable)
222
- end
223
-
224
- def git_checkout_path(source)
225
- bundle_root = @lockfile.dirname
226
- app_config = ENV["BUNDLE_APP_CONFIG"]
227
- app_config_path = if app_config
228
- Pathname(app_config).expand_path(bundle_root)
229
- else
230
- bundle_root.join(".bundle")
231
- end
232
- bundle_path = Pathname(Bundler::Settings.new(app_config_path).path.path).expand_path(bundle_root)
233
- bundle_path.join("bundler/gems", source.extension_dir_name)
234
- end
235
-
236
- def git_specifications(source, checkout)
237
- source.__send__(:set_install_path!, checkout)
238
- source.specs
239
- end
240
-
241
- def git_package_paths(specification, checkout)
242
- logical_root = Pathname(specification.full_gem_path).expand_path
243
- logical_gemspec = Pathname(specification.loaded_from).expand_path
244
- return unless Paths.inside?(logical_root, checkout)
245
- return unless logical_gemspec.dirname == logical_root
246
- return unless logical_gemspec.file? && logical_root.directory?
247
-
248
- canonical_checkout = checkout.realpath
249
- resolved_gemspec_root = logical_gemspec.realpath.dirname
250
- logical_canonical_root = logical_root.realpath
251
- return unless resolved_gemspec_root.directory?
252
- normal_checkout = !checkout.symlink? &&
253
- Paths.inside?(resolved_gemspec_root, canonical_checkout) &&
254
- Paths.inside?(logical_canonical_root, canonical_checkout)
255
- return unless normal_checkout || @nix_store_provider.trusted?(resolved_gemspec_root)
256
-
257
- canonical_root = normal_checkout ? logical_canonical_root : resolved_gemspec_root
258
-
259
- GitPackagePaths.new(
260
- logical_root: logical_root,
261
- canonical_root: canonical_root,
262
- logical_canonical_root: logical_canonical_root,
263
- )
264
- rescue TypeError, ArgumentError
265
- nil
266
- end
267
-
268
- def git_indexable_files(paths, require_paths)
269
- logical_require_paths = require_paths.map do |relative_path|
270
- raise UnsafeGitRequirePath unless relative_path.is_a?(String)
271
-
272
- relative = Pathname(relative_path)
273
- raise UnsafeGitRequirePath if relative.absolute? || relative.each_filename.include?("..")
274
-
275
- candidate = paths.logical_root.join(relative).cleanpath
276
- raise UnsafeGitRequirePath unless Paths.inside?(candidate, paths.logical_root)
277
-
278
- candidate
279
- rescue ArgumentError, EncodingError
280
- raise UnsafeGitRequirePath
281
- end
282
-
283
- files = []
284
- visited_directories = Set.new
285
- logical_require_paths.each do |path|
286
- next unless path.exist? || path.symlink?
287
-
288
- traverse_git_package_path(path, paths, files, visited_directories, Set.new)
289
- end
290
- files.uniq.sort
291
- end
292
-
293
- def traverse_git_package_path(path, paths, files, visited_directories, active_directories)
294
- resolved = path.realpath
295
- if resolved.file?
296
- raise UnsafeGitPackageFile unless Paths.inside?(resolved, paths.canonical_root)
297
-
298
- files << resolved.to_s if indexable?(path)
299
- return
300
- end
301
- return unless resolved.directory?
302
-
303
- unless Paths.inside?(resolved, paths.logical_canonical_root) || Paths.inside?(resolved, paths.canonical_root)
304
- raise UnsafeGitPackageFile
305
- end
306
- raise UnsafeGitPackageFile if active_directories.include?(resolved)
307
- return if visited_directories.include?(resolved)
308
-
309
- active_directories.add(resolved)
310
- begin
311
- path.children.sort_by(&:to_s).each do |child|
312
- traverse_git_package_path(child, paths, files, visited_directories, active_directories)
313
- end
314
- visited_directories.add(resolved)
315
- ensure
316
- active_directories.delete(resolved)
317
- end
318
- rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
319
- raise UnsafeGitPackageFile
320
190
  end
321
191
 
322
192
  def skip_git_dependency(locked, reason_code)
@@ -429,20 +299,54 @@ module RubyLens
429
299
  end.flat_map { |path| enumerate(path, root) }.uniq.sort
430
300
  end
431
301
 
302
+ # Walks an already-canonical directory (the caller resolved and containment-checked
303
+ # it) without re-resolving every entry: descending only non-symlink directories
304
+ # keeps each joined path canonical, so regular files join the list with a cheap
305
+ # prefix containment check and only symlinked entries pay for a realpath. Symlinked
306
+ # directories are never descended, matching Find.find's lstat-based traversal.
432
307
  def enumerate(path, root)
433
308
  return [path.to_s] if path.file? && indexable?(path) && Paths.inside?(path.realpath, root)
434
309
  return [] unless path.directory?
310
+ return [] if path.symlink?
435
311
 
436
312
  files = []
437
- Find.find(path) do |candidate|
438
- candidate = Pathname(candidate)
439
- next if candidate.directory?
440
- next unless indexable?(candidate)
441
-
442
- resolved = candidate.realpath
443
- files << resolved.to_s if Paths.inside?(resolved, root)
444
- rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
445
- next
313
+ root_prefix = "#{root}#{File::SEPARATOR}"
314
+ start = path.to_s
315
+ # Child names must come back in the starting path's encoding. A package
316
+ # root that is not valid UTF-8 otherwise yields UTF-8 entry names that
317
+ # cannot be joined to it, and the whole build aborts on a tree the
318
+ # Find-based walk indexed fine.
319
+ encoding = start.encoding == Encoding::US_ASCII ? Encoding.find("filesystem") : start.encoding
320
+ pending = [start]
321
+ while (directory = pending.pop)
322
+ entries = begin
323
+ Dir.children(directory, encoding: encoding)
324
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL
325
+ next
326
+ end
327
+ entries.each do |entry|
328
+ candidate = File.join(directory, entry)
329
+ stat = begin
330
+ File.lstat(candidate)
331
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG, Errno::EINVAL
332
+ next
333
+ end
334
+ if stat.symlink?
335
+ next if File.directory?(candidate)
336
+ next unless INDEXABLE_EXTENSIONS.include?(File.extname(entry))
337
+
338
+ begin
339
+ resolved = File.realpath(candidate)
340
+ files << resolved if Paths.inside?(resolved, root)
341
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
342
+ next
343
+ end
344
+ elsif stat.directory?
345
+ pending << candidate
346
+ elsif INDEXABLE_EXTENSIONS.include?(File.extname(entry))
347
+ files << candidate if candidate.start_with?(root_prefix)
348
+ end
349
+ end
446
350
  end
447
351
  files
448
352
  end