scint 0.6.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/README.md +90 -41
- data/VERSION +1 -1
- data/bin/scint +9 -0
- data/lib/bundler.rb +106 -0
- data/lib/scint/cache/layout.rb +16 -14
- data/lib/scint/cache/metadata_store.rb +4 -11
- data/lib/scint/cli/cache.rb +2 -1
- data/lib/scint/cli/exec.rb +12 -24
- data/lib/scint/cli/install.rb +1034 -124
- data/lib/scint/credentials.rb +78 -15
- data/lib/scint/debug/io_trace.rb +26 -7
- data/lib/scint/downloader/fetcher.rb +25 -1
- data/lib/scint/downloader/pool.rb +67 -15
- data/lib/scint/errors.rb +10 -0
- data/lib/scint/fs.rb +44 -2
- data/lib/scint/gemfile/parser.rb +31 -4
- data/lib/scint/installer/extension_builder.rb +60 -30
- data/lib/scint/installer/linker.rb +8 -24
- data/lib/scint/installer/planner.rb +30 -7
- data/lib/scint/installer/preparer.rb +2 -9
- data/lib/scint/lockfile/parser.rb +2 -1
- data/lib/scint/lockfile/writer.rb +85 -36
- data/lib/scint/platform.rb +8 -0
- data/lib/scint/resolver/provider.rb +15 -2
- data/lib/scint/runtime/exec.rb +52 -26
- data/lib/scint/runtime/setup.rb +29 -1
- data/lib/scint/scheduler.rb +6 -1
- data/lib/scint/spec_utils.rb +58 -0
- data/lib/scint.rb +1 -0
- metadata +2 -1
data/lib/scint/cli/install.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../errors"
|
|
4
4
|
require_relative "../fs"
|
|
5
5
|
require_relative "../platform"
|
|
6
|
+
require_relative "../spec_utils"
|
|
6
7
|
require_relative "../progress"
|
|
7
8
|
require_relative "../worker_pool"
|
|
8
9
|
require_relative "../scheduler"
|
|
@@ -32,6 +33,8 @@ require_relative "../resolver/provider"
|
|
|
32
33
|
require_relative "../resolver/resolver"
|
|
33
34
|
require_relative "../credentials"
|
|
34
35
|
require "open3"
|
|
36
|
+
require "set"
|
|
37
|
+
require "pathname"
|
|
35
38
|
|
|
36
39
|
module Scint
|
|
37
40
|
module CLI
|
|
@@ -44,6 +47,10 @@ module Scint
|
|
|
44
47
|
@path = nil
|
|
45
48
|
@verbose = false
|
|
46
49
|
@force = false
|
|
50
|
+
@download_pool = nil
|
|
51
|
+
@download_pool_lock = Thread::Mutex.new
|
|
52
|
+
@gemspec_cache = {}
|
|
53
|
+
@gemspec_cache_lock = Thread::Mutex.new
|
|
47
54
|
parse_options
|
|
48
55
|
end
|
|
49
56
|
|
|
@@ -117,7 +124,14 @@ module Scint
|
|
|
117
124
|
# Scale up for download/install phase based on actual work count
|
|
118
125
|
scheduler.scale_workers(to_install.size)
|
|
119
126
|
|
|
127
|
+
# Warm-cache accelerator: pre-materialize cache-backed gem trees in
|
|
128
|
+
# batches so install workers avoid one cp process per gem.
|
|
129
|
+
bulk_prelink_gem_files(to_install, cache, bundle_path)
|
|
130
|
+
|
|
120
131
|
if to_install.empty?
|
|
132
|
+
# Keep lock artifacts aligned even when everything is already installed.
|
|
133
|
+
write_lockfile(resolved, gemfile, lockfile)
|
|
134
|
+
write_runtime_config(resolved, bundle_path)
|
|
121
135
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
122
136
|
worker_count = scheduler.stats[:workers]
|
|
123
137
|
warn_missing_bundle_gitignore_entry
|
|
@@ -148,7 +162,9 @@ module Scint
|
|
|
148
162
|
if errors.any?
|
|
149
163
|
$stderr.puts "#{RED}Some gems failed to install:#{RESET}"
|
|
150
164
|
errors.each do |err|
|
|
151
|
-
|
|
165
|
+
error = err[:error]
|
|
166
|
+
$stderr.puts " #{BOLD}#{err[:name]}#{RESET}: #{error.message}"
|
|
167
|
+
emit_network_error_details(error)
|
|
152
168
|
end
|
|
153
169
|
elsif stats[:failed] > 0
|
|
154
170
|
$stderr.puts "#{YELLOW}Warning: #{stats[:failed]} jobs failed but no error details captured#{RESET}"
|
|
@@ -168,14 +184,18 @@ module Scint
|
|
|
168
184
|
1
|
|
169
185
|
else
|
|
170
186
|
# 10. Write lockfile + runtime config only for successful installs
|
|
171
|
-
write_lockfile(resolved, gemfile)
|
|
187
|
+
write_lockfile(resolved, gemfile, lockfile)
|
|
172
188
|
write_runtime_config(resolved, bundle_path)
|
|
173
189
|
warn_missing_bundle_gitignore_entry
|
|
174
190
|
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems)}. #{DIM}(#{format_run_footer(elapsed_ms, worker_count)})#{RESET}"
|
|
175
191
|
0
|
|
176
192
|
end
|
|
177
193
|
ensure
|
|
178
|
-
|
|
194
|
+
begin
|
|
195
|
+
scheduler.shutdown
|
|
196
|
+
ensure
|
|
197
|
+
close_download_pool
|
|
198
|
+
end
|
|
179
199
|
end
|
|
180
200
|
end
|
|
181
201
|
|
|
@@ -207,7 +227,7 @@ module Scint
|
|
|
207
227
|
def dedupe_resolved_specs(resolved)
|
|
208
228
|
seen = {}
|
|
209
229
|
resolved.each do |spec|
|
|
210
|
-
key =
|
|
230
|
+
key = SpecUtils.full_key(spec)
|
|
211
231
|
seen[key] ||= spec
|
|
212
232
|
end
|
|
213
233
|
seen.values
|
|
@@ -217,8 +237,8 @@ module Scint
|
|
|
217
237
|
# No download needed — we know exactly where we are.
|
|
218
238
|
def install_builtin_gem(entry, bundle_path)
|
|
219
239
|
spec = entry.spec
|
|
220
|
-
ruby_dir =
|
|
221
|
-
full_name =
|
|
240
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
241
|
+
full_name = SpecUtils.full_name(spec)
|
|
222
242
|
scint_root = File.expand_path("../../..", __FILE__)
|
|
223
243
|
|
|
224
244
|
# Copy gem files into gems/scint-x.y.z/lib/
|
|
@@ -270,7 +290,10 @@ module Scint
|
|
|
270
290
|
|
|
271
291
|
def resolve(gemfile, lockfile, cache)
|
|
272
292
|
# If lockfile is up-to-date, use its specs directly
|
|
273
|
-
if lockfile &&
|
|
293
|
+
if lockfile &&
|
|
294
|
+
lockfile_current?(gemfile, lockfile) &&
|
|
295
|
+
lockfile_dependency_graph_valid?(lockfile) &&
|
|
296
|
+
lockfile_git_source_mapping_valid?(lockfile, cache)
|
|
274
297
|
return lockfile_to_resolved(lockfile)
|
|
275
298
|
end
|
|
276
299
|
|
|
@@ -305,6 +328,7 @@ module Scint
|
|
|
305
328
|
# Build path_gems: gem_name => { version:, dependencies:, source: }
|
|
306
329
|
# for gems with path: or git: sources (skip compact index for these)
|
|
307
330
|
path_gems = {}
|
|
331
|
+
git_source_metadata_cache = {}
|
|
308
332
|
gemfile.dependencies.each do |dep|
|
|
309
333
|
opts = dep.source_options
|
|
310
334
|
next unless opts[:path] || opts[:git]
|
|
@@ -314,19 +338,57 @@ module Scint
|
|
|
314
338
|
|
|
315
339
|
# Try to read version and deps from gemspec if it's a path gem
|
|
316
340
|
if opts[:path]
|
|
317
|
-
gemspec = find_gemspec(opts[:path], dep.name)
|
|
341
|
+
gemspec = find_gemspec(opts[:path], dep.name, glob: opts[:glob])
|
|
318
342
|
if gemspec
|
|
319
343
|
version = gemspec.version.to_s
|
|
320
344
|
deps = gemspec.dependencies
|
|
321
345
|
.select { |d| d.type == :runtime }
|
|
322
|
-
.map
|
|
346
|
+
.map do |d|
|
|
347
|
+
requirement_parts = d.requirement.requirements.map { |op, req_version| "#{op} #{req_version}" }
|
|
348
|
+
[d.name, requirement_parts]
|
|
349
|
+
end
|
|
323
350
|
end
|
|
324
351
|
end
|
|
325
352
|
|
|
326
|
-
# For git gems,
|
|
327
|
-
if opts[:git]
|
|
328
|
-
|
|
329
|
-
|
|
353
|
+
# For git gems, read version and dependencies from the available revision when possible.
|
|
354
|
+
if opts[:git]
|
|
355
|
+
git_source = find_matching_git_source(Array(lockfile&.sources), opts) || find_matching_git_source(gemfile.sources, opts)
|
|
356
|
+
revision_hint = git_source&.revision || git_source&.ref || opts[:ref] || opts[:branch] || opts[:tag] || "HEAD"
|
|
357
|
+
bare_repo = cache&.git_path(opts[:git])
|
|
358
|
+
if bare_repo && !Dir.exist?(bare_repo)
|
|
359
|
+
clone_git_repo(opts[:git], bare_repo)
|
|
360
|
+
elsif bare_repo && Dir.exist?(bare_repo)
|
|
361
|
+
fetch_git_repo(bare_repo)
|
|
362
|
+
end
|
|
363
|
+
if bare_repo && Dir.exist?(bare_repo)
|
|
364
|
+
begin
|
|
365
|
+
resolved_revision = resolve_git_revision(bare_repo, revision_hint)
|
|
366
|
+
cache_key = "#{opts[:git]}@#{resolved_revision}"
|
|
367
|
+
git_metadata = git_source_metadata_cache[cache_key]
|
|
368
|
+
unless git_metadata
|
|
369
|
+
git_metadata = build_git_path_gems_for_revision(
|
|
370
|
+
bare_repo,
|
|
371
|
+
resolved_revision,
|
|
372
|
+
glob: opts[:glob],
|
|
373
|
+
source_desc: opts[:git],
|
|
374
|
+
)
|
|
375
|
+
git_source_metadata_cache[cache_key] = git_metadata
|
|
376
|
+
end
|
|
377
|
+
path_gems.merge!(git_metadata)
|
|
378
|
+
current = git_metadata[dep.name]
|
|
379
|
+
if current
|
|
380
|
+
version = current[:version]
|
|
381
|
+
deps = current[:dependencies]
|
|
382
|
+
end
|
|
383
|
+
rescue StandardError
|
|
384
|
+
# Fall back to lockfile version below.
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if lockfile && version == "0"
|
|
389
|
+
locked_spec = lockfile.specs.find { |s| s[:name] == dep.name }
|
|
390
|
+
version = locked_spec[:version] if locked_spec
|
|
391
|
+
end
|
|
330
392
|
end
|
|
331
393
|
|
|
332
394
|
source_desc = opts[:path] || opts[:git] || "local"
|
|
@@ -353,14 +415,16 @@ module Scint
|
|
|
353
415
|
resolver.resolve
|
|
354
416
|
end
|
|
355
417
|
|
|
356
|
-
def find_gemspec(path, gem_name)
|
|
418
|
+
def find_gemspec(path, gem_name, glob: nil)
|
|
357
419
|
return nil unless Dir.exist?(path)
|
|
358
420
|
|
|
421
|
+
glob_pattern = glob || Source::Path::DEFAULT_GLOB
|
|
359
422
|
# Look for exact match first, then any gemspec
|
|
360
423
|
candidates = [
|
|
361
424
|
File.join(path, "#{gem_name}.gemspec"),
|
|
425
|
+
*Dir.glob(File.join(path, glob_pattern)),
|
|
362
426
|
*Dir.glob(File.join(path, "*.gemspec")),
|
|
363
|
-
]
|
|
427
|
+
].uniq
|
|
364
428
|
|
|
365
429
|
candidates.each do |gs|
|
|
366
430
|
next unless File.exist?(gs)
|
|
@@ -374,11 +438,135 @@ module Scint
|
|
|
374
438
|
nil
|
|
375
439
|
end
|
|
376
440
|
|
|
441
|
+
def find_git_gemspec(bare_repo, revision, gem_name, glob: nil)
|
|
442
|
+
gemspec_paths = gemspec_paths_in_git_revision(bare_repo, revision)
|
|
443
|
+
return nil if gemspec_paths.empty?
|
|
444
|
+
|
|
445
|
+
path = gemspec_paths[gem_name.to_s]
|
|
446
|
+
if path.nil? && glob
|
|
447
|
+
glob_regex = git_glob_to_regex(glob)
|
|
448
|
+
path = gemspec_paths.values.find { |candidate| candidate.match?(glob_regex) }
|
|
449
|
+
end
|
|
450
|
+
path ||= gemspec_paths.values.first
|
|
451
|
+
return nil if path.nil?
|
|
452
|
+
|
|
453
|
+
load_git_gemspec(bare_repo, revision, path)
|
|
454
|
+
rescue StandardError
|
|
455
|
+
nil
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def build_git_path_gems_for_revision(bare_repo, revision, glob: nil, source_desc: nil)
|
|
459
|
+
gemspec_paths = gemspec_paths_in_git_revision(bare_repo, revision)
|
|
460
|
+
return {} if gemspec_paths.empty?
|
|
461
|
+
|
|
462
|
+
glob_regex = glob ? git_glob_to_regex(glob) : nil
|
|
463
|
+
data = {}
|
|
464
|
+
|
|
465
|
+
with_git_worktree(bare_repo, revision) do |worktree|
|
|
466
|
+
gemspec_paths.each_value do |path|
|
|
467
|
+
next if glob_regex && !path.match?(glob_regex)
|
|
468
|
+
|
|
469
|
+
gemspec = load_gemspec_from_worktree(worktree, path)
|
|
470
|
+
next unless gemspec
|
|
471
|
+
|
|
472
|
+
deps = gemspec.dependencies
|
|
473
|
+
.select { |d| d.type == :runtime }
|
|
474
|
+
.map do |d|
|
|
475
|
+
requirement_parts = d.requirement.requirements.map { |op, req_version| "#{op} #{req_version}" }
|
|
476
|
+
[d.name, requirement_parts]
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
data[gemspec.name] = {
|
|
480
|
+
version: gemspec.version.to_s,
|
|
481
|
+
dependencies: deps,
|
|
482
|
+
source: source_desc || "local",
|
|
483
|
+
}
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
data
|
|
488
|
+
rescue StandardError
|
|
489
|
+
{}
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def git_glob_to_regex(glob)
|
|
493
|
+
pattern = glob.to_s
|
|
494
|
+
escaped = Regexp.escape(pattern)
|
|
495
|
+
escaped = escaped.gsub("\\*\\*", ".*")
|
|
496
|
+
escaped = escaped.gsub("\\*", "[^/]*")
|
|
497
|
+
escaped = escaped.gsub("\\?", ".")
|
|
498
|
+
/\A#{escaped}\z/
|
|
499
|
+
end
|
|
500
|
+
|
|
377
501
|
def lockfile_current?(gemfile, lockfile)
|
|
378
502
|
return false unless lockfile
|
|
379
503
|
|
|
380
504
|
locked_names = Set.new(lockfile.specs.map { |s| s[:name] })
|
|
381
|
-
gemfile.dependencies.all?
|
|
505
|
+
gemfile.dependencies.all? do |dep|
|
|
506
|
+
next true unless dependency_relevant_for_local_platform?(dep)
|
|
507
|
+
|
|
508
|
+
locked_names.include?(dep.name)
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def lockfile_dependency_graph_valid?(lockfile)
|
|
513
|
+
return false unless lockfile
|
|
514
|
+
|
|
515
|
+
specs = Array(lockfile.specs)
|
|
516
|
+
return false if specs.empty?
|
|
517
|
+
|
|
518
|
+
by_name = Hash.new { |h, k| h[k] = [] }
|
|
519
|
+
specs.each { |spec| by_name[spec[:name].to_s] << spec }
|
|
520
|
+
|
|
521
|
+
specs.all? do |spec|
|
|
522
|
+
Array(spec[:dependencies]).all? do |dep|
|
|
523
|
+
dep_name = dep[:name].to_s
|
|
524
|
+
# Lockfiles generally do not include a concrete "bundler" spec
|
|
525
|
+
# entry even though many gemspecs declare a runtime dependency
|
|
526
|
+
# on bundler. Treat it as externally satisfied.
|
|
527
|
+
next true if dep_name == "bundler"
|
|
528
|
+
|
|
529
|
+
dep_reqs = Array(dep[:version_reqs])
|
|
530
|
+
req = Gem::Requirement.new(dep_reqs.empty? ? [">= 0"] : dep_reqs)
|
|
531
|
+
by_name[dep_name].any? { |candidate| req.satisfied_by?(Gem::Version.new(candidate[:version].to_s)) }
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
rescue StandardError
|
|
535
|
+
false
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def dependency_relevant_for_local_platform?(dependency)
|
|
539
|
+
platforms = Array(dependency.platforms).map(&:to_sym)
|
|
540
|
+
return true if platforms.empty?
|
|
541
|
+
|
|
542
|
+
platforms.any? { |platform| gemfile_platform_matches_local?(platform) }
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def gemfile_platform_matches_local?(platform)
|
|
546
|
+
case platform
|
|
547
|
+
when :ruby
|
|
548
|
+
true
|
|
549
|
+
when :mri
|
|
550
|
+
RUBY_ENGINE == "ruby"
|
|
551
|
+
when :jruby
|
|
552
|
+
RUBY_ENGINE == "jruby"
|
|
553
|
+
when :truffleruby
|
|
554
|
+
RUBY_ENGINE == "truffleruby"
|
|
555
|
+
when :rbx
|
|
556
|
+
RUBY_ENGINE == "rbx"
|
|
557
|
+
when :windows, :mswin, :mswin64, :mingw, :x64_mingw, :x86_mingw, :x64_mingw_ucrt
|
|
558
|
+
Platform.windows?
|
|
559
|
+
when :linux
|
|
560
|
+
Platform.linux?
|
|
561
|
+
when :darwin, :macos
|
|
562
|
+
Platform.macos?
|
|
563
|
+
else
|
|
564
|
+
platform_name = platform.to_s.tr("_", "-")
|
|
565
|
+
spec_platform = Gem::Platform.new(platform_name)
|
|
566
|
+
spec_platform === Platform.local_platform
|
|
567
|
+
end
|
|
568
|
+
rescue StandardError
|
|
569
|
+
false
|
|
382
570
|
end
|
|
383
571
|
|
|
384
572
|
def lockfile_to_resolved(lockfile)
|
|
@@ -414,6 +602,108 @@ module Scint
|
|
|
414
602
|
apply_locked_platform_preferences(resolved)
|
|
415
603
|
end
|
|
416
604
|
|
|
605
|
+
def lockfile_git_source_mapping_valid?(lockfile, cache)
|
|
606
|
+
return true unless lockfile && cache
|
|
607
|
+
|
|
608
|
+
git_specs = Array(lockfile.specs).select { |s| s[:source].is_a?(Source::Git) }
|
|
609
|
+
return true if git_specs.empty?
|
|
610
|
+
|
|
611
|
+
by_source = git_specs.group_by { |s| s[:source] }
|
|
612
|
+
by_source.each do |source, specs|
|
|
613
|
+
uri, revision = git_source_ref(source)
|
|
614
|
+
bare_repo = cache.git_path(uri)
|
|
615
|
+
# Do not invalidate an otherwise-usable lockfile just because this
|
|
616
|
+
# git source has not been cached yet in the current machine/session.
|
|
617
|
+
next unless Dir.exist?(bare_repo)
|
|
618
|
+
|
|
619
|
+
resolved_revision = begin
|
|
620
|
+
resolve_git_revision(bare_repo, revision)
|
|
621
|
+
rescue InstallError
|
|
622
|
+
nil
|
|
623
|
+
end
|
|
624
|
+
return false unless resolved_revision
|
|
625
|
+
|
|
626
|
+
gemspec_paths = gemspec_paths_in_git_revision(bare_repo, resolved_revision)
|
|
627
|
+
gemspec_names = gemspec_paths.keys.to_set
|
|
628
|
+
return false if gemspec_names.empty?
|
|
629
|
+
|
|
630
|
+
specs.each do |spec|
|
|
631
|
+
return false unless gemspec_names.include?(spec[:name].to_s)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
true
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def gemspec_paths_in_git_revision(bare_repo, revision)
|
|
639
|
+
out, _err, status = git_capture3(
|
|
640
|
+
"--git-dir", bare_repo,
|
|
641
|
+
"ls-tree",
|
|
642
|
+
"-r",
|
|
643
|
+
"--name-only",
|
|
644
|
+
revision,
|
|
645
|
+
)
|
|
646
|
+
return {} unless status.success?
|
|
647
|
+
|
|
648
|
+
paths = {}
|
|
649
|
+
out.each_line do |line|
|
|
650
|
+
path = line.strip
|
|
651
|
+
next unless path.end_with?(".gemspec")
|
|
652
|
+
name = File.basename(path, ".gemspec")
|
|
653
|
+
paths[name] ||= path
|
|
654
|
+
end
|
|
655
|
+
paths
|
|
656
|
+
rescue StandardError
|
|
657
|
+
{}
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
def runtime_dependencies_for_git_gemspec(bare_repo, revision, gemspec_path)
|
|
661
|
+
spec = load_git_gemspec(bare_repo, revision, gemspec_path)
|
|
662
|
+
return nil unless spec
|
|
663
|
+
|
|
664
|
+
spec.dependencies.select { |dep| dep.type == :runtime }
|
|
665
|
+
rescue StandardError
|
|
666
|
+
nil
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def load_git_gemspec(bare_repo, revision, gemspec_path)
|
|
670
|
+
return nil if gemspec_path.to_s.empty?
|
|
671
|
+
|
|
672
|
+
with_git_worktree(bare_repo, revision) do |worktree|
|
|
673
|
+
load_gemspec_from_worktree(worktree, gemspec_path)
|
|
674
|
+
end
|
|
675
|
+
rescue StandardError
|
|
676
|
+
nil
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def with_git_worktree(bare_repo, revision)
|
|
680
|
+
worktree = Dir.mktmpdir("scint-gemspec")
|
|
681
|
+
_out, _err, status = git_capture3(
|
|
682
|
+
"--git-dir", bare_repo,
|
|
683
|
+
"--work-tree", worktree,
|
|
684
|
+
"checkout",
|
|
685
|
+
"--force",
|
|
686
|
+
revision,
|
|
687
|
+
)
|
|
688
|
+
return nil unless status.success?
|
|
689
|
+
|
|
690
|
+
File.write(File.join(worktree, ".git"), "gitdir: #{bare_repo}\n")
|
|
691
|
+
yield worktree if block_given?
|
|
692
|
+
ensure
|
|
693
|
+
FileUtils.rm_rf(worktree) if worktree && !worktree.empty?
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def load_gemspec_from_worktree(worktree, gemspec_path)
|
|
697
|
+
absolute_gemspec = File.join(worktree, gemspec_path)
|
|
698
|
+
return nil unless File.exist?(absolute_gemspec)
|
|
699
|
+
|
|
700
|
+
Dir.chdir(File.dirname(absolute_gemspec)) do
|
|
701
|
+
Gem::Specification.load(absolute_gemspec)
|
|
702
|
+
end
|
|
703
|
+
rescue StandardError
|
|
704
|
+
nil
|
|
705
|
+
end
|
|
706
|
+
|
|
417
707
|
# Preference: exact platform match > compatible match > ruby > first.
|
|
418
708
|
def pick_best_platform_spec(specs, local_plat)
|
|
419
709
|
return specs.first if specs.size == 1
|
|
@@ -451,7 +741,7 @@ module Scint
|
|
|
451
741
|
return resolved_specs if preferred.empty?
|
|
452
742
|
|
|
453
743
|
resolved_specs.each do |spec|
|
|
454
|
-
key =
|
|
744
|
+
key = SpecUtils.full_name_for(spec.name, spec.version)
|
|
455
745
|
platform = preferred[key]
|
|
456
746
|
next if platform.nil? || platform.empty?
|
|
457
747
|
|
|
@@ -478,7 +768,7 @@ module Scint
|
|
|
478
768
|
preferred = preferred.to_s
|
|
479
769
|
next if preferred.empty? || preferred == spec.platform.to_s
|
|
480
770
|
|
|
481
|
-
out[
|
|
771
|
+
out[SpecUtils.full_name_for(spec.name, spec.version)] = preferred
|
|
482
772
|
end
|
|
483
773
|
rescue StandardError
|
|
484
774
|
next
|
|
@@ -496,7 +786,7 @@ module Scint
|
|
|
496
786
|
spec = entry.spec
|
|
497
787
|
source = spec.source
|
|
498
788
|
if git_source?(source)
|
|
499
|
-
|
|
789
|
+
prepare_git_checkout(spec, cache, fetch: false)
|
|
500
790
|
return
|
|
501
791
|
end
|
|
502
792
|
source_uri = source.to_s
|
|
@@ -504,7 +794,7 @@ module Scint
|
|
|
504
794
|
# Path gems are not downloaded from a remote
|
|
505
795
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
506
796
|
|
|
507
|
-
full_name =
|
|
797
|
+
full_name = SpecUtils.full_name(spec)
|
|
508
798
|
gem_filename = "#{full_name}.gem"
|
|
509
799
|
source_uri = source_uri.chomp("/")
|
|
510
800
|
download_uri = "#{source_uri}/gems/#{gem_filename}"
|
|
@@ -513,9 +803,20 @@ module Scint
|
|
|
513
803
|
FS.mkdir_p(File.dirname(dest_path))
|
|
514
804
|
|
|
515
805
|
unless File.exist?(dest_path)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
806
|
+
downloader_pool.download(download_uri, dest_path)
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def downloader_pool
|
|
811
|
+
@download_pool_lock.synchronize do
|
|
812
|
+
@download_pool ||= Downloader::Pool.new(credentials: @credentials)
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def close_download_pool
|
|
817
|
+
@download_pool_lock.synchronize do
|
|
818
|
+
@download_pool&.close
|
|
819
|
+
@download_pool = nil
|
|
519
820
|
end
|
|
520
821
|
end
|
|
521
822
|
|
|
@@ -523,8 +824,12 @@ module Scint
|
|
|
523
824
|
spec = entry.spec
|
|
524
825
|
source_uri = spec.source.to_s
|
|
525
826
|
|
|
526
|
-
# Git
|
|
527
|
-
|
|
827
|
+
# Git gems are extracted from the cached checkout; path gems are
|
|
828
|
+
# linked directly from local source.
|
|
829
|
+
if git_source?(spec.source)
|
|
830
|
+
materialize_git_spec(entry, cache)
|
|
831
|
+
return
|
|
832
|
+
end
|
|
528
833
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
529
834
|
|
|
530
835
|
extracted = cache.extracted_path(spec)
|
|
@@ -546,51 +851,132 @@ module Scint
|
|
|
546
851
|
source_str.end_with?(".git") || source_str.include?(".git/")
|
|
547
852
|
end
|
|
548
853
|
|
|
854
|
+
def path_source?(source)
|
|
855
|
+
return true if source.is_a?(Source::Path)
|
|
856
|
+
|
|
857
|
+
source_str =
|
|
858
|
+
if source.respond_to?(:path)
|
|
859
|
+
source.path.to_s
|
|
860
|
+
else
|
|
861
|
+
source.to_s
|
|
862
|
+
end
|
|
863
|
+
return false if source_str.empty?
|
|
864
|
+
return false if source_str.start_with?("http://", "https://")
|
|
865
|
+
return false if git_source?(source)
|
|
866
|
+
|
|
867
|
+
source_str.start_with?("/", ".", "~")
|
|
868
|
+
end
|
|
869
|
+
|
|
549
870
|
def prepare_git_source(entry, cache)
|
|
871
|
+
# Legacy helper used by tests/callers that expect git download+extract
|
|
872
|
+
# in a single step.
|
|
550
873
|
spec = entry.spec
|
|
874
|
+
checkout_dir, resolved_revision, _uri = prepare_git_checkout(spec, cache, fetch: true)
|
|
875
|
+
materialize_git_spec(entry, cache, checkout_dir: checkout_dir, resolved_revision: resolved_revision)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def prepare_git_checkout(spec, cache, fetch: false)
|
|
551
879
|
source = spec.source
|
|
552
880
|
uri, revision = git_source_ref(source)
|
|
881
|
+
submodules = git_source_submodules?(source)
|
|
553
882
|
|
|
554
883
|
bare_repo = cache.git_path(uri)
|
|
555
884
|
|
|
556
885
|
# Serialize all git operations per bare repo — git uses index.lock
|
|
557
886
|
# and can't handle concurrent checkouts from the same repo.
|
|
558
887
|
git_mutex_for(bare_repo).synchronize do
|
|
559
|
-
|
|
560
|
-
|
|
888
|
+
if Dir.exist?(bare_repo)
|
|
889
|
+
fetch_git_repo(bare_repo) if fetch
|
|
890
|
+
else
|
|
891
|
+
clone_git_repo(uri, bare_repo)
|
|
892
|
+
fetch_git_repo(bare_repo)
|
|
893
|
+
end
|
|
561
894
|
|
|
562
895
|
resolved_revision = resolve_git_revision(bare_repo, revision)
|
|
896
|
+
incoming_checkout = cache.git_checkout_path(uri, resolved_revision)
|
|
897
|
+
materialize_git_checkout(
|
|
898
|
+
bare_repo,
|
|
899
|
+
incoming_checkout,
|
|
900
|
+
resolved_revision,
|
|
901
|
+
spec,
|
|
902
|
+
uri,
|
|
903
|
+
submodules: submodules,
|
|
904
|
+
)
|
|
905
|
+
[incoming_checkout, resolved_revision, uri]
|
|
906
|
+
end
|
|
907
|
+
end
|
|
563
908
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
909
|
+
def materialize_git_spec(entry, cache, checkout_dir: nil, resolved_revision: nil)
|
|
910
|
+
spec = entry.spec
|
|
911
|
+
checkout_dir, resolved_revision, _uri = prepare_git_checkout(spec, cache, fetch: false) unless checkout_dir && resolved_revision
|
|
912
|
+
gem_root = resolve_git_gem_subdir(checkout_dir, spec)
|
|
913
|
+
extracted = cache.extracted_path(spec)
|
|
914
|
+
marker = git_checkout_marker_path(extracted)
|
|
915
|
+
if Dir.exist?(extracted) &&
|
|
916
|
+
File.exist?(marker) &&
|
|
917
|
+
File.read(marker).strip == resolved_revision &&
|
|
918
|
+
git_spec_layout_current?(extracted, spec)
|
|
919
|
+
return
|
|
920
|
+
end
|
|
569
921
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
922
|
+
tmp = "#{extracted}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
923
|
+
begin
|
|
924
|
+
FileUtils.rm_rf(tmp)
|
|
925
|
+
FS.clone_tree(gem_root, tmp)
|
|
926
|
+
|
|
927
|
+
FileUtils.rm_rf(extracted)
|
|
928
|
+
FS.atomic_move(tmp, extracted)
|
|
929
|
+
FS.atomic_write(marker, "#{resolved_revision}\n")
|
|
930
|
+
ensure
|
|
931
|
+
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def git_spec_layout_current?(extracted_path, spec)
|
|
936
|
+
File.exist?(File.join(extracted_path, "#{spec.name}.gemspec"))
|
|
937
|
+
rescue StandardError
|
|
938
|
+
false
|
|
939
|
+
end
|
|
940
|
+
|
|
941
|
+
def materialize_git_checkout(bare_repo, checkout_dir, resolved_revision, spec, uri, submodules: false)
|
|
942
|
+
marker = git_checkout_marker_path(checkout_dir)
|
|
943
|
+
if Dir.exist?(checkout_dir) && File.exist?(marker)
|
|
944
|
+
checkout_marker = parse_git_checkout_marker(marker)
|
|
945
|
+
marker_revision = checkout_marker[:revision]
|
|
946
|
+
marker_submodules = checkout_marker[:submodules]
|
|
947
|
+
|
|
948
|
+
# Legacy markers only recorded revision. If this source now requires
|
|
949
|
+
# submodules, force a refresh so previously incomplete checkouts are
|
|
950
|
+
# not reused.
|
|
951
|
+
return if marker_revision == resolved_revision && (!submodules || marker_submodules == true)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
tmp = "#{checkout_dir}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
955
|
+
begin
|
|
956
|
+
FileUtils.rm_rf(tmp)
|
|
957
|
+
if submodules
|
|
958
|
+
checkout_git_tree_with_submodules(
|
|
959
|
+
bare_repo,
|
|
960
|
+
tmp,
|
|
580
961
|
resolved_revision,
|
|
581
|
-
|
|
582
|
-
|
|
962
|
+
spec,
|
|
963
|
+
uri,
|
|
964
|
+
)
|
|
965
|
+
else
|
|
966
|
+
checkout_git_tree(
|
|
967
|
+
bare_repo,
|
|
968
|
+
tmp,
|
|
969
|
+
resolved_revision,
|
|
970
|
+
spec,
|
|
971
|
+
uri,
|
|
583
972
|
)
|
|
584
|
-
unless status.success?
|
|
585
|
-
raise InstallError, "Git checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
FileUtils.rm_rf(extracted)
|
|
589
|
-
FS.atomic_move(tmp, extracted)
|
|
590
|
-
FS.atomic_write(marker, "#{resolved_revision}\n")
|
|
591
|
-
ensure
|
|
592
|
-
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
593
973
|
end
|
|
974
|
+
|
|
975
|
+
FileUtils.rm_rf(checkout_dir)
|
|
976
|
+
FS.atomic_move(tmp, checkout_dir)
|
|
977
|
+
FS.atomic_write(marker, format_git_checkout_marker(resolved_revision, submodules: submodules))
|
|
978
|
+
ensure
|
|
979
|
+
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
594
980
|
end
|
|
595
981
|
end
|
|
596
982
|
|
|
@@ -603,6 +989,68 @@ module Scint
|
|
|
603
989
|
[source.to_s, "HEAD"]
|
|
604
990
|
end
|
|
605
991
|
|
|
992
|
+
def git_source_submodules?(source)
|
|
993
|
+
source.respond_to?(:submodules) && !!source.submodules
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
def checkout_git_tree(bare_repo, destination, resolved_revision, spec, uri)
|
|
997
|
+
FileUtils.mkdir_p(destination)
|
|
998
|
+
_out, err, status = git_capture3(
|
|
999
|
+
"--git-dir", bare_repo,
|
|
1000
|
+
"--work-tree", destination,
|
|
1001
|
+
"checkout",
|
|
1002
|
+
"-f",
|
|
1003
|
+
resolved_revision,
|
|
1004
|
+
"--",
|
|
1005
|
+
".",
|
|
1006
|
+
)
|
|
1007
|
+
unless status.success?
|
|
1008
|
+
raise InstallError, "Git checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
1009
|
+
end
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def checkout_git_tree_with_submodules(bare_repo, destination, resolved_revision, spec, uri)
|
|
1013
|
+
worktree = "#{destination}.worktree"
|
|
1014
|
+
FileUtils.rm_rf(worktree)
|
|
1015
|
+
|
|
1016
|
+
_out, err, status = git_capture3(
|
|
1017
|
+
"--git-dir", bare_repo,
|
|
1018
|
+
"worktree",
|
|
1019
|
+
"add",
|
|
1020
|
+
"--detach",
|
|
1021
|
+
"--force",
|
|
1022
|
+
worktree,
|
|
1023
|
+
resolved_revision,
|
|
1024
|
+
)
|
|
1025
|
+
unless status.success?
|
|
1026
|
+
raise InstallError, "Git worktree checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
begin
|
|
1030
|
+
_sub_out, sub_err, sub_status = git_capture3(
|
|
1031
|
+
"-C", worktree,
|
|
1032
|
+
"-c", "protocol.file.allow=always",
|
|
1033
|
+
"submodule",
|
|
1034
|
+
"update",
|
|
1035
|
+
"--init",
|
|
1036
|
+
"--recursive",
|
|
1037
|
+
)
|
|
1038
|
+
unless sub_status.success?
|
|
1039
|
+
raise InstallError, "Git submodule update failed for #{spec.name} (#{uri}@#{resolved_revision}): #{sub_err.to_s.strip}"
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
FS.clone_tree(worktree, destination)
|
|
1043
|
+
|
|
1044
|
+
# Keep cache/extracted trees deterministic and detached from git internals.
|
|
1045
|
+
Dir.glob(File.join(destination, "**", ".git"), File::FNM_DOTMATCH).each do |path|
|
|
1046
|
+
FileUtils.rm_rf(path)
|
|
1047
|
+
end
|
|
1048
|
+
ensure
|
|
1049
|
+
git_capture3("--git-dir", bare_repo, "worktree", "remove", "--force", worktree)
|
|
1050
|
+
FileUtils.rm_rf(worktree)
|
|
1051
|
+
end
|
|
1052
|
+
end
|
|
1053
|
+
|
|
606
1054
|
def git_mutex_for(repo_path)
|
|
607
1055
|
@git_mutexes_lock ||= Thread::Mutex.new
|
|
608
1056
|
@git_mutexes_lock.synchronize do
|
|
@@ -649,19 +1097,53 @@ module Scint
|
|
|
649
1097
|
"#{dir}.scint_git_revision"
|
|
650
1098
|
end
|
|
651
1099
|
|
|
1100
|
+
def parse_git_checkout_marker(path)
|
|
1101
|
+
content = File.read(path)
|
|
1102
|
+
revision = nil
|
|
1103
|
+
submodules = nil
|
|
1104
|
+
|
|
1105
|
+
content.each_line do |line|
|
|
1106
|
+
key, value = line.strip.split("=", 2)
|
|
1107
|
+
case key
|
|
1108
|
+
when "revision"
|
|
1109
|
+
revision = value
|
|
1110
|
+
when "submodules"
|
|
1111
|
+
submodules = (value == "1" || value == "true")
|
|
1112
|
+
end
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
if revision.nil?
|
|
1116
|
+
# Legacy format: raw revision only.
|
|
1117
|
+
revision = content.strip
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
{ revision: revision, submodules: submodules }
|
|
1121
|
+
rescue StandardError
|
|
1122
|
+
{ revision: nil, submodules: nil }
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def format_git_checkout_marker(revision, submodules:)
|
|
1126
|
+
"revision=#{revision}\nsubmodules=#{submodules ? 1 : 0}\n"
|
|
1127
|
+
end
|
|
1128
|
+
|
|
652
1129
|
def compile_slots_for(worker_count)
|
|
653
|
-
# Keep
|
|
654
|
-
#
|
|
655
|
-
|
|
656
|
-
1
|
|
1130
|
+
# Keep compile parallelism conservative: at most 2 native builds.
|
|
1131
|
+
# Small pools stay single-lane; larger pools can run two builds.
|
|
1132
|
+
workers = worker_count.to_i
|
|
1133
|
+
return 1 if workers <= 6
|
|
1134
|
+
|
|
1135
|
+
2
|
|
657
1136
|
end
|
|
658
1137
|
|
|
659
1138
|
def install_task_limits(worker_count, compile_slots)
|
|
660
1139
|
# Leave headroom for compile and binstub lanes so link/download
|
|
661
1140
|
# throughput cannot fully starve them.
|
|
662
1141
|
io_cpu_limit = [worker_count - compile_slots - 1, 1].max
|
|
1142
|
+
# Keep download in-flight set bounded so fail-fast exits quickly on
|
|
1143
|
+
# auth/source errors instead of queueing a large burst.
|
|
1144
|
+
download_limit = [io_cpu_limit, 8].min
|
|
663
1145
|
{
|
|
664
|
-
download:
|
|
1146
|
+
download: download_limit,
|
|
665
1147
|
extract: io_cpu_limit,
|
|
666
1148
|
link: io_cpu_limit,
|
|
667
1149
|
build_ext: compile_slots,
|
|
@@ -770,7 +1252,7 @@ module Scint
|
|
|
770
1252
|
end
|
|
771
1253
|
|
|
772
1254
|
def spec_key(spec)
|
|
773
|
-
|
|
1255
|
+
SpecUtils.full_key(spec)
|
|
774
1256
|
end
|
|
775
1257
|
|
|
776
1258
|
def dependency_link_job_ids(spec, link_job_by_name)
|
|
@@ -805,11 +1287,25 @@ module Scint
|
|
|
805
1287
|
def extracted_path_for_entry(entry, cache)
|
|
806
1288
|
source_str = entry.spec.source.to_s
|
|
807
1289
|
if source_str.start_with?("/") && Dir.exist?(source_str)
|
|
808
|
-
|
|
1290
|
+
begin
|
|
1291
|
+
if path_source?(entry.spec.source)
|
|
1292
|
+
resolve_path_gem_subdir(source_str, entry.spec)
|
|
1293
|
+
else
|
|
1294
|
+
resolve_git_gem_subdir(source_str, entry.spec)
|
|
1295
|
+
end
|
|
1296
|
+
rescue InstallError
|
|
1297
|
+
source_str
|
|
1298
|
+
end
|
|
809
1299
|
else
|
|
810
1300
|
base = entry.cached_path || cache.extracted_path(entry.spec)
|
|
811
1301
|
if git_source?(entry.spec.source) && Dir.exist?(base)
|
|
812
1302
|
resolve_git_gem_subdir(base, entry.spec)
|
|
1303
|
+
elsif path_source?(entry.spec.source) && Dir.exist?(base)
|
|
1304
|
+
begin
|
|
1305
|
+
resolve_path_gem_subdir(base, entry.spec)
|
|
1306
|
+
rescue InstallError
|
|
1307
|
+
base
|
|
1308
|
+
end
|
|
813
1309
|
else
|
|
814
1310
|
base
|
|
815
1311
|
end
|
|
@@ -826,8 +1322,40 @@ module Scint
|
|
|
826
1322
|
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
827
1323
|
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
828
1324
|
end
|
|
1325
|
+
# Compatibility fallback for monorepos whose gem layout does not match
|
|
1326
|
+
# the lockfile glob exactly.
|
|
1327
|
+
Dir.glob(File.join(repo_root, "**", "*.gemspec")).each do |path|
|
|
1328
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
1329
|
+
end
|
|
829
1330
|
|
|
830
|
-
|
|
1331
|
+
source_uri = source.respond_to?(:uri) ? source.uri : source.to_s
|
|
1332
|
+
raise InstallError,
|
|
1333
|
+
"Git source #{source_uri} does not contain #{name}.gemspec (glob: #{glob.inspect}); lockfile source mapping may be stale"
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
# For path monorepo sources, map gem name to its gemspec subdirectory.
|
|
1337
|
+
def resolve_path_gem_subdir(repo_root, spec)
|
|
1338
|
+
name = spec.name
|
|
1339
|
+
return repo_root if File.exist?(File.join(repo_root, "#{name}.gemspec"))
|
|
1340
|
+
|
|
1341
|
+
source = spec.source
|
|
1342
|
+
glob = source.respond_to?(:glob) ? source.glob : Source::Path::DEFAULT_GLOB
|
|
1343
|
+
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
1344
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
1345
|
+
end
|
|
1346
|
+
Dir.glob(File.join(repo_root, "**", "*.gemspec")).each do |path|
|
|
1347
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
source_uri =
|
|
1351
|
+
if source.respond_to?(:path)
|
|
1352
|
+
source.path
|
|
1353
|
+
elsif source.respond_to?(:uri)
|
|
1354
|
+
source.uri
|
|
1355
|
+
else
|
|
1356
|
+
source.to_s
|
|
1357
|
+
end
|
|
1358
|
+
raise InstallError, "Path source #{source_uri} does not contain #{name}.gemspec (glob: #{glob.inspect})"
|
|
831
1359
|
end
|
|
832
1360
|
|
|
833
1361
|
def link_gem_files(entry, cache, bundle_path)
|
|
@@ -882,7 +1410,7 @@ module Scint
|
|
|
882
1410
|
dep_names.uniq!
|
|
883
1411
|
return if dep_names.empty?
|
|
884
1412
|
|
|
885
|
-
source_ruby_dir =
|
|
1413
|
+
source_ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
886
1414
|
target_ruby_dir = cache.install_ruby_dir
|
|
887
1415
|
|
|
888
1416
|
dep_names.each do |name|
|
|
@@ -922,8 +1450,15 @@ module Scint
|
|
|
922
1450
|
end
|
|
923
1451
|
|
|
924
1452
|
def load_gemspec(extracted_path, spec, cache)
|
|
1453
|
+
cache_key = "#{cache.full_name(spec)}@#{extracted_path}"
|
|
1454
|
+
cached_value = @gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] }
|
|
1455
|
+
return cached_value if cached_value
|
|
1456
|
+
|
|
925
1457
|
cached = load_cached_gemspec(spec, cache, extracted_path)
|
|
926
|
-
|
|
1458
|
+
if cached
|
|
1459
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = cached }
|
|
1460
|
+
return cached
|
|
1461
|
+
end
|
|
927
1462
|
|
|
928
1463
|
inbound = cache.inbound_path(spec)
|
|
929
1464
|
return nil unless File.exist?(inbound)
|
|
@@ -931,12 +1466,38 @@ module Scint
|
|
|
931
1466
|
begin
|
|
932
1467
|
metadata = GemPkg::Package.new.read_metadata(inbound)
|
|
933
1468
|
cache_gemspec(spec, metadata, cache)
|
|
1469
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = metadata }
|
|
934
1470
|
metadata
|
|
935
1471
|
rescue StandardError
|
|
936
1472
|
nil
|
|
937
1473
|
end
|
|
938
1474
|
end
|
|
939
1475
|
|
|
1476
|
+
def bulk_prelink_gem_files(entries, cache, bundle_path)
|
|
1477
|
+
# Keep small installs simple; batching is for large warm-path runs.
|
|
1478
|
+
return if entries.length < 32
|
|
1479
|
+
|
|
1480
|
+
ruby_dir = File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
1481
|
+
gems_dir = File.join(ruby_dir, "gems")
|
|
1482
|
+
|
|
1483
|
+
sources = entries.filter_map do |entry|
|
|
1484
|
+
next unless entry.action == :link || entry.action == :build_ext
|
|
1485
|
+
|
|
1486
|
+
extracted = cache.extracted_path(entry.spec)
|
|
1487
|
+
full_name = cache.full_name(entry.spec)
|
|
1488
|
+
next unless File.basename(extracted) == full_name
|
|
1489
|
+
next unless Dir.exist?(extracted)
|
|
1490
|
+
next if Dir.exist?(File.join(gems_dir, full_name))
|
|
1491
|
+
|
|
1492
|
+
extracted
|
|
1493
|
+
end
|
|
1494
|
+
return if sources.empty?
|
|
1495
|
+
|
|
1496
|
+
FS.clone_many_trees(sources, gems_dir)
|
|
1497
|
+
rescue StandardError => e
|
|
1498
|
+
$stderr.puts("bulk prelink warning: #{e.message}") if ENV["SCINT_DEBUG"]
|
|
1499
|
+
end
|
|
1500
|
+
|
|
940
1501
|
def load_cached_gemspec(spec, cache, extracted_path)
|
|
941
1502
|
path = cache.spec_cache_path(spec)
|
|
942
1503
|
return nil unless File.exist?(path)
|
|
@@ -996,79 +1557,386 @@ module Scint
|
|
|
996
1557
|
|
|
997
1558
|
# --- Lockfile + runtime config ---
|
|
998
1559
|
|
|
999
|
-
def write_lockfile(resolved, gemfile)
|
|
1000
|
-
sources =
|
|
1560
|
+
def write_lockfile(resolved, gemfile, lockfile = nil)
|
|
1561
|
+
specs, sources, preserved_layout = build_lockfile_specs_and_sources(resolved, gemfile, lockfile)
|
|
1562
|
+
|
|
1563
|
+
lockfile_data = Lockfile::LockfileData.new(
|
|
1564
|
+
specs: specs,
|
|
1565
|
+
dependencies: build_lockfile_dependencies(gemfile, lockfile),
|
|
1566
|
+
platforms: preserved_layout && lockfile ? Array(lockfile.platforms) : build_lockfile_platforms(specs, lockfile),
|
|
1567
|
+
sources: sources,
|
|
1568
|
+
bundler_version: lockfile&.bundler_version || Scint::VERSION,
|
|
1569
|
+
ruby_version: lockfile&.ruby_version || gemfile.ruby_version,
|
|
1570
|
+
checksums: preserved_layout && lockfile ? lockfile.checksums : build_lockfile_checksums(specs, lockfile),
|
|
1571
|
+
)
|
|
1572
|
+
|
|
1573
|
+
content = Lockfile::Writer.write(lockfile_data)
|
|
1574
|
+
FS.atomic_write("Gemfile.lock", content)
|
|
1575
|
+
end
|
|
1576
|
+
|
|
1577
|
+
def build_lockfile_specs_and_sources(resolved, gemfile, lockfile)
|
|
1578
|
+
resolved_for_lockfile = filter_lockfile_specs(resolved)
|
|
1579
|
+
|
|
1580
|
+
if preserve_existing_lockfile_specs?(resolved_for_lockfile, lockfile)
|
|
1581
|
+
specs = Array(lockfile.specs).map { |spec| normalize_lockfile_spec(spec) }
|
|
1582
|
+
sources = uniq_sources(Array(lockfile.sources))
|
|
1583
|
+
return [specs, sources, true]
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
dependency_sources = dependency_sources_from_gemfile(gemfile, lockfile)
|
|
1587
|
+
existing_sources = Array(lockfile&.sources)
|
|
1588
|
+
candidate_sources = uniq_sources(existing_sources + dependency_sources.values)
|
|
1589
|
+
|
|
1590
|
+
rubygems_uris = collect_lockfile_rubygems_uris(gemfile)
|
|
1591
|
+
if rubygems_uris.empty? && candidate_sources.none? { |src| src.is_a?(Source::Rubygems) }
|
|
1592
|
+
rubygems_uris << "https://rubygems.org"
|
|
1593
|
+
end
|
|
1594
|
+
rubygems_uris.each do |uri|
|
|
1595
|
+
source = find_matching_rubygems_source(candidate_sources, uri)
|
|
1596
|
+
candidate_sources << Source::Rubygems.new(remotes: [uri]) unless source
|
|
1597
|
+
end
|
|
1598
|
+
candidate_sources = uniq_sources(candidate_sources)
|
|
1599
|
+
|
|
1600
|
+
lock_source_by_full, lock_source_by_name_version = lockfile_sources_by_spec_key(lockfile)
|
|
1601
|
+
default_rubygems_source = candidate_sources.find { |src| src.is_a?(Source::Rubygems) }
|
|
1602
|
+
|
|
1603
|
+
specs = resolved_for_lockfile.map do |spec|
|
|
1604
|
+
normalized = normalize_resolved_spec(spec)
|
|
1605
|
+
source = source_for_spec(
|
|
1606
|
+
normalized,
|
|
1607
|
+
dependency_sources: dependency_sources,
|
|
1608
|
+
candidate_sources: candidate_sources,
|
|
1609
|
+
lock_source_by_full: lock_source_by_full,
|
|
1610
|
+
lock_source_by_name_version: lock_source_by_name_version,
|
|
1611
|
+
fallback: default_rubygems_source,
|
|
1612
|
+
)
|
|
1613
|
+
normalized.merge(source: source)
|
|
1614
|
+
end
|
|
1615
|
+
|
|
1616
|
+
sources = uniq_sources(specs.map { |spec| spec[:source] }.compact)
|
|
1617
|
+
if sources.empty?
|
|
1618
|
+
fallback = default_rubygems_source || Source::Rubygems.new(remotes: ["https://rubygems.org"])
|
|
1619
|
+
sources = [fallback]
|
|
1620
|
+
specs.each { |spec| spec[:source] = fallback }
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
[specs, sources, false]
|
|
1624
|
+
end
|
|
1625
|
+
|
|
1626
|
+
def filter_lockfile_specs(specs)
|
|
1627
|
+
specs.reject do |spec|
|
|
1628
|
+
name = spec.is_a?(Hash) ? spec[:name].to_s : spec.name.to_s
|
|
1629
|
+
name == "scint"
|
|
1630
|
+
end
|
|
1631
|
+
end
|
|
1632
|
+
|
|
1633
|
+
def preserve_existing_lockfile_specs?(resolved, lockfile)
|
|
1634
|
+
return false unless lockfile && lockfile.respond_to?(:specs)
|
|
1635
|
+
|
|
1636
|
+
wanted = resolved.map { |spec| [spec.name.to_s, spec.version.to_s] }.uniq
|
|
1637
|
+
return false if wanted.empty?
|
|
1638
|
+
|
|
1639
|
+
available = Set.new
|
|
1640
|
+
Array(lockfile.specs).each do |spec|
|
|
1641
|
+
available << [spec[:name].to_s, spec[:version].to_s]
|
|
1642
|
+
end
|
|
1643
|
+
|
|
1644
|
+
wanted.all? { |tuple| available.include?(tuple) }
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1647
|
+
def normalize_lockfile_spec(spec)
|
|
1648
|
+
if spec.is_a?(Hash)
|
|
1649
|
+
{
|
|
1650
|
+
name: spec[:name],
|
|
1651
|
+
version: spec[:version],
|
|
1652
|
+
platform: spec[:platform] || "ruby",
|
|
1653
|
+
dependencies: spec[:dependencies] || [],
|
|
1654
|
+
source: spec[:source],
|
|
1655
|
+
checksum: spec[:checksum],
|
|
1656
|
+
}
|
|
1657
|
+
else
|
|
1658
|
+
{
|
|
1659
|
+
name: spec.name,
|
|
1660
|
+
version: spec.version,
|
|
1661
|
+
platform: spec.platform || "ruby",
|
|
1662
|
+
dependencies: spec.dependencies || [],
|
|
1663
|
+
source: spec.source,
|
|
1664
|
+
checksum: spec.respond_to?(:checksum) ? spec.checksum : nil,
|
|
1665
|
+
}
|
|
1666
|
+
end
|
|
1667
|
+
end
|
|
1668
|
+
|
|
1669
|
+
def normalize_resolved_spec(spec)
|
|
1670
|
+
if spec.is_a?(Hash)
|
|
1671
|
+
{
|
|
1672
|
+
name: spec[:name],
|
|
1673
|
+
version: spec[:version],
|
|
1674
|
+
platform: spec[:platform] || "ruby",
|
|
1675
|
+
dependencies: spec[:dependencies] || [],
|
|
1676
|
+
source: spec[:source],
|
|
1677
|
+
checksum: spec[:checksum],
|
|
1678
|
+
}
|
|
1679
|
+
else
|
|
1680
|
+
{
|
|
1681
|
+
name: spec.name,
|
|
1682
|
+
version: spec.version,
|
|
1683
|
+
platform: spec.platform || "ruby",
|
|
1684
|
+
dependencies: spec.dependencies || [],
|
|
1685
|
+
source: spec.source,
|
|
1686
|
+
checksum: spec.respond_to?(:checksum) ? spec.checksum : nil,
|
|
1687
|
+
}
|
|
1688
|
+
end
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
def collect_lockfile_rubygems_uris(gemfile)
|
|
1692
|
+
uris = gemfile.sources
|
|
1693
|
+
.select { |src| src[:type] == :rubygems && src[:uri] }
|
|
1694
|
+
.map { |src| src[:uri].to_s }
|
|
1695
|
+
|
|
1696
|
+
gemfile.dependencies.each do |dep|
|
|
1697
|
+
inline = dep.source_options[:source]
|
|
1698
|
+
uris << inline.to_s if inline
|
|
1699
|
+
end
|
|
1700
|
+
|
|
1701
|
+
uris.uniq
|
|
1702
|
+
end
|
|
1703
|
+
|
|
1704
|
+
def dependency_sources_from_gemfile(gemfile, lockfile)
|
|
1705
|
+
existing_sources = Array(lockfile&.sources)
|
|
1706
|
+
out = {}
|
|
1001
1707
|
|
|
1002
|
-
# Build source objects for path and git gems
|
|
1003
1708
|
gemfile.dependencies.each do |dep|
|
|
1004
1709
|
opts = dep.source_options
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1710
|
+
|
|
1711
|
+
source =
|
|
1712
|
+
if opts[:path]
|
|
1713
|
+
find_matching_path_source(existing_sources, opts[:path]) ||
|
|
1714
|
+
Source::Path.new(path: opts[:path], name: dep.name)
|
|
1715
|
+
elsif opts[:git]
|
|
1716
|
+
matched = find_matching_git_source(existing_sources, opts)
|
|
1717
|
+
Source::Git.new(
|
|
1718
|
+
uri: opts[:git],
|
|
1719
|
+
revision: matched&.revision,
|
|
1720
|
+
ref: opts[:ref] || matched&.ref,
|
|
1721
|
+
branch: opts[:branch] || matched&.branch,
|
|
1722
|
+
tag: opts[:tag] || matched&.tag,
|
|
1723
|
+
submodules: opts.fetch(:submodules, matched&.submodules),
|
|
1724
|
+
glob: matched&.glob,
|
|
1725
|
+
name: dep.name,
|
|
1726
|
+
)
|
|
1727
|
+
elsif opts[:source]
|
|
1728
|
+
find_matching_rubygems_source(existing_sources, opts[:source]) ||
|
|
1729
|
+
Source::Rubygems.new(remotes: [opts[:source]])
|
|
1730
|
+
end
|
|
1731
|
+
|
|
1732
|
+
out[dep.name] = source if source
|
|
1733
|
+
end
|
|
1734
|
+
|
|
1735
|
+
out
|
|
1736
|
+
end
|
|
1737
|
+
|
|
1738
|
+
def lockfile_sources_by_spec_key(lockfile)
|
|
1739
|
+
by_full = {}
|
|
1740
|
+
by_name_version = {}
|
|
1741
|
+
|
|
1742
|
+
Array(lockfile&.specs).each do |spec|
|
|
1743
|
+
source = spec[:source]
|
|
1744
|
+
next unless source
|
|
1745
|
+
|
|
1746
|
+
name = spec[:name].to_s
|
|
1747
|
+
version = spec[:version].to_s
|
|
1748
|
+
platform = (spec[:platform] || "ruby").to_s
|
|
1749
|
+
|
|
1750
|
+
by_full[[name, version, platform]] = source
|
|
1751
|
+
by_name_version[[name, version]] ||= source
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
[by_full, by_name_version]
|
|
1755
|
+
end
|
|
1756
|
+
|
|
1757
|
+
def source_for_spec(spec, dependency_sources:, candidate_sources:, lock_source_by_full:, lock_source_by_name_version:, fallback:)
|
|
1758
|
+
key_full = [spec[:name].to_s, spec[:version].to_s, spec[:platform].to_s]
|
|
1759
|
+
locked_source = lock_source_by_full[key_full] || lock_source_by_name_version[key_full[0, 2]]
|
|
1760
|
+
return locked_source if locked_source
|
|
1761
|
+
|
|
1762
|
+
dep_source = dependency_sources[spec[:name].to_s]
|
|
1763
|
+
return dep_source if dep_source
|
|
1764
|
+
|
|
1765
|
+
spec_source = spec[:source]
|
|
1766
|
+
source = find_matching_source(candidate_sources, spec_source)
|
|
1767
|
+
return source if source
|
|
1768
|
+
|
|
1769
|
+
spec_source = spec_source.to_s
|
|
1770
|
+
if git_source?(spec_source)
|
|
1771
|
+
source = Source::Git.new(uri: spec_source, name: spec[:name])
|
|
1772
|
+
candidate_sources << source
|
|
1773
|
+
return source
|
|
1774
|
+
elsif spec_source.start_with?("/") || spec_source.start_with?(".")
|
|
1775
|
+
source = Source::Path.new(path: spec_source, name: spec[:name])
|
|
1776
|
+
candidate_sources << source
|
|
1777
|
+
return source
|
|
1778
|
+
end
|
|
1779
|
+
|
|
1780
|
+
if rubygems_source_uri?(spec_source.to_s)
|
|
1781
|
+
source = Source::Rubygems.new(remotes: [spec_source.to_s])
|
|
1782
|
+
candidate_sources << source
|
|
1783
|
+
return source
|
|
1784
|
+
end
|
|
1785
|
+
|
|
1786
|
+
fallback
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1789
|
+
def find_matching_source(sources, source_ref)
|
|
1790
|
+
return nil if source_ref.nil?
|
|
1791
|
+
|
|
1792
|
+
sources.find do |source|
|
|
1793
|
+
source_matches?(source, source_ref)
|
|
1794
|
+
end
|
|
1795
|
+
end
|
|
1796
|
+
|
|
1797
|
+
def source_matches?(source, source_ref)
|
|
1798
|
+
return true if source.equal?(source_ref)
|
|
1799
|
+
return true if source == source_ref
|
|
1800
|
+
|
|
1801
|
+
source_key = normalize_source_key(source_ref)
|
|
1802
|
+
return false unless source_key
|
|
1803
|
+
|
|
1804
|
+
if source.is_a?(Source::Rubygems)
|
|
1805
|
+
source.remotes.any? { |remote| normalize_source_key(remote) == source_key }
|
|
1806
|
+
elsif source.respond_to?(:uri)
|
|
1807
|
+
normalize_source_key(source.uri) == source_key
|
|
1808
|
+
else
|
|
1809
|
+
normalize_source_key(source) == source_key
|
|
1810
|
+
end
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def normalize_source_key(source_ref)
|
|
1814
|
+
return nil if source_ref.nil?
|
|
1815
|
+
|
|
1816
|
+
raw =
|
|
1817
|
+
if source_ref.respond_to?(:uri)
|
|
1818
|
+
source_ref.uri.to_s
|
|
1819
|
+
elsif source_ref.respond_to?(:path)
|
|
1820
|
+
source_ref.path.to_s
|
|
1821
|
+
else
|
|
1822
|
+
source_ref.to_s
|
|
1014
1823
|
end
|
|
1824
|
+
return nil if raw.empty?
|
|
1825
|
+
|
|
1826
|
+
if raw.match?(%r{\Ahttps?://}i)
|
|
1827
|
+
raw = raw.sub(%r{\Ahttps?://}i, "")
|
|
1828
|
+
raw = raw.sub(%r{\.git/?\z}i, "")
|
|
1829
|
+
raw.chomp("/").downcase
|
|
1830
|
+
elsif raw.start_with?("/") || raw.start_with?(".")
|
|
1831
|
+
File.expand_path(raw)
|
|
1832
|
+
else
|
|
1833
|
+
raw.sub(%r{\.git/?\z}i, "").chomp("/").downcase
|
|
1015
1834
|
end
|
|
1835
|
+
end
|
|
1016
1836
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
.
|
|
1020
|
-
|
|
1021
|
-
|
|
1837
|
+
def find_matching_rubygems_source(sources, uri)
|
|
1838
|
+
sources.find do |source|
|
|
1839
|
+
source.is_a?(Source::Rubygems) && source.remotes.any? { |remote| source_matches?(remote, uri) }
|
|
1840
|
+
end
|
|
1841
|
+
end
|
|
1022
1842
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1843
|
+
def find_matching_path_source(sources, path)
|
|
1844
|
+
sources.find { |source| source.is_a?(Source::Path) && source_matches?(source, path) }
|
|
1845
|
+
end
|
|
1846
|
+
|
|
1847
|
+
def find_matching_git_source(sources, opts)
|
|
1848
|
+
candidates = sources.select { |source| source.is_a?(Source::Git) && source_matches?(source, opts[:git]) }
|
|
1849
|
+
return nil if candidates.empty?
|
|
1850
|
+
|
|
1851
|
+
candidates.find { |source| git_source_options_match?(source, opts) } || candidates.first
|
|
1852
|
+
end
|
|
1853
|
+
|
|
1854
|
+
def git_source_options_match?(source, opts)
|
|
1855
|
+
return false if opts[:branch] && source.branch.to_s != opts[:branch].to_s
|
|
1856
|
+
return false if opts[:tag] && source.tag.to_s != opts[:tag].to_s
|
|
1857
|
+
return false if opts[:ref] && source.ref.to_s != opts[:ref].to_s
|
|
1858
|
+
|
|
1859
|
+
true
|
|
1860
|
+
end
|
|
1861
|
+
|
|
1862
|
+
def uniq_sources(sources)
|
|
1863
|
+
out = []
|
|
1864
|
+
sources.each do |source|
|
|
1865
|
+
next unless source
|
|
1866
|
+
out << source unless out.any? { |existing| existing.eql?(source) }
|
|
1029
1867
|
end
|
|
1868
|
+
out
|
|
1869
|
+
end
|
|
1870
|
+
|
|
1871
|
+
def build_lockfile_dependencies(gemfile, lockfile)
|
|
1872
|
+
locked = lockfile&.dependencies || {}
|
|
1873
|
+
gemfile.dependencies
|
|
1874
|
+
.select { |dep| lockfile_dependency_direct?(dep) }
|
|
1875
|
+
.map do |dep|
|
|
1876
|
+
locked_dep = locked[dep.name]
|
|
1877
|
+
{
|
|
1878
|
+
name: dep.name,
|
|
1879
|
+
version_reqs: dep.version_reqs,
|
|
1880
|
+
pinned: !!(locked_dep && locked_dep[:pinned]),
|
|
1881
|
+
}
|
|
1882
|
+
end
|
|
1883
|
+
end
|
|
1884
|
+
|
|
1885
|
+
def lockfile_dependency_direct?(dep)
|
|
1886
|
+
opts = dep.source_options || {}
|
|
1887
|
+
return true unless opts[:gemspec_generated]
|
|
1888
|
+
|
|
1889
|
+
opts[:gemspec_primary] != false
|
|
1890
|
+
end
|
|
1030
1891
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1892
|
+
def build_lockfile_platforms(specs, lockfile)
|
|
1893
|
+
platforms = Set.new(Array(lockfile&.platforms))
|
|
1894
|
+
specs.each do |spec|
|
|
1895
|
+
platform = spec[:platform] || "ruby"
|
|
1896
|
+
platforms << platform
|
|
1034
1897
|
end
|
|
1898
|
+
platforms << "ruby"
|
|
1899
|
+
platforms.to_a
|
|
1900
|
+
end
|
|
1035
1901
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1902
|
+
def build_lockfile_checksums(specs, lockfile)
|
|
1903
|
+
existing = lockfile&.checksums
|
|
1904
|
+
checksums = {}
|
|
1905
|
+
|
|
1906
|
+
specs.each do |spec|
|
|
1907
|
+
key = lockfile_spec_checksum_key(spec)
|
|
1908
|
+
checksum = spec[:checksum]
|
|
1909
|
+
if checksum && !Array(checksum).empty?
|
|
1910
|
+
checksums[key] = Array(checksum)
|
|
1911
|
+
elsif existing&.key?(key)
|
|
1912
|
+
checksums[key] = Array(existing[key])
|
|
1913
|
+
end
|
|
1914
|
+
end
|
|
1040
1915
|
|
|
1041
|
-
|
|
1042
|
-
specs: resolved,
|
|
1043
|
-
dependencies: gemfile.dependencies.map { |d| { name: d.name, version_reqs: d.version_reqs } },
|
|
1044
|
-
platforms: [Platform.local_platform.to_s, "ruby"].uniq,
|
|
1045
|
-
sources: sources,
|
|
1046
|
-
bundler_version: Scint::VERSION,
|
|
1047
|
-
ruby_version: nil,
|
|
1048
|
-
checksums: nil,
|
|
1049
|
-
)
|
|
1916
|
+
return nil if checksums.empty?
|
|
1050
1917
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1918
|
+
checksums
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
def lockfile_spec_checksum_key(spec)
|
|
1922
|
+
SpecUtils.full_name_for(spec[:name], spec[:version], spec[:platform] || "ruby")
|
|
1053
1923
|
end
|
|
1054
1924
|
|
|
1055
1925
|
def write_runtime_config(resolved, bundle_path)
|
|
1056
|
-
ruby_dir =
|
|
1057
|
-
RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
1926
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
1058
1927
|
|
|
1059
1928
|
data = {}
|
|
1060
1929
|
resolved.each do |spec|
|
|
1061
|
-
full =
|
|
1930
|
+
full = SpecUtils.full_name(spec)
|
|
1062
1931
|
gem_dir = File.join(ruby_dir, "gems", full)
|
|
1063
1932
|
spec_file = File.join(ruby_dir, "specifications", "#{full}.gemspec")
|
|
1064
1933
|
require_paths = read_require_paths(spec_file)
|
|
1065
1934
|
load_paths = require_paths
|
|
1066
|
-
.map { |rp|
|
|
1935
|
+
.map { |rp| expand_require_path(gem_dir, rp) }
|
|
1067
1936
|
.select { |path| Dir.exist?(path) }
|
|
1068
1937
|
|
|
1069
1938
|
default_lib = File.join(gem_dir, "lib")
|
|
1070
1939
|
load_paths << default_lib if load_paths.empty? && Dir.exist?(default_lib)
|
|
1071
|
-
load_paths.concat(detect_nested_lib_paths(gem_dir))
|
|
1072
1940
|
load_paths.uniq!
|
|
1073
1941
|
|
|
1074
1942
|
# Add ext load path if extensions exist
|
|
@@ -1076,6 +1944,12 @@ module Scint
|
|
|
1076
1944
|
Platform.gem_arch, Platform.extension_api_version, full)
|
|
1077
1945
|
load_paths << ext_dir if Dir.exist?(ext_dir)
|
|
1078
1946
|
|
|
1947
|
+
if load_paths.empty?
|
|
1948
|
+
source_paths = runtime_source_load_paths(spec)
|
|
1949
|
+
load_paths.concat(source_paths)
|
|
1950
|
+
load_paths.uniq!
|
|
1951
|
+
end
|
|
1952
|
+
|
|
1079
1953
|
data[spec.name] = {
|
|
1080
1954
|
version: spec.version.to_s,
|
|
1081
1955
|
load_paths: load_paths,
|
|
@@ -1096,26 +1970,40 @@ module Scint
|
|
|
1096
1970
|
["lib"]
|
|
1097
1971
|
end
|
|
1098
1972
|
|
|
1099
|
-
def
|
|
1100
|
-
|
|
1101
|
-
return
|
|
1973
|
+
def expand_require_path(gem_dir, require_path)
|
|
1974
|
+
value = require_path.to_s
|
|
1975
|
+
return value if Pathname.new(value).absolute?
|
|
1102
1976
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1977
|
+
File.join(gem_dir, value)
|
|
1978
|
+
rescue StandardError
|
|
1979
|
+
File.join(gem_dir, require_path.to_s)
|
|
1980
|
+
end
|
|
1981
|
+
|
|
1982
|
+
def runtime_source_load_paths(spec)
|
|
1983
|
+
source_root = spec.source.to_s
|
|
1984
|
+
return [] unless source_root.start_with?("/") && Dir.exist?(source_root)
|
|
1985
|
+
|
|
1986
|
+
source_dir = begin
|
|
1987
|
+
resolve_git_gem_subdir(source_root, spec)
|
|
1988
|
+
rescue InstallError
|
|
1989
|
+
source_root
|
|
1107
1990
|
end
|
|
1108
|
-
return [] if top_level_rb
|
|
1109
1991
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1992
|
+
gemspec_file = File.join(source_dir, "#{spec.name}.gemspec")
|
|
1993
|
+
require_paths = read_require_paths(gemspec_file)
|
|
1994
|
+
paths = require_paths
|
|
1995
|
+
.map { |rp| expand_require_path(source_dir, rp) }
|
|
1996
|
+
.select { |path| Dir.exist?(path) }
|
|
1997
|
+
|
|
1998
|
+
default_lib = File.join(source_dir, "lib")
|
|
1999
|
+
paths << default_lib if paths.empty? && Dir.exist?(default_lib)
|
|
2000
|
+
paths.uniq
|
|
2001
|
+
rescue StandardError
|
|
2002
|
+
[]
|
|
1113
2003
|
end
|
|
1114
2004
|
|
|
1115
2005
|
def spec_full_name(spec)
|
|
1116
|
-
|
|
1117
|
-
plat = spec.respond_to?(:platform) ? spec.platform : nil
|
|
1118
|
-
(plat.nil? || plat.to_s == "ruby" || plat.to_s.empty?) ? base : "#{base}-#{plat}"
|
|
2006
|
+
SpecUtils.full_name(spec)
|
|
1119
2007
|
end
|
|
1120
2008
|
|
|
1121
2009
|
def elapsed_ms_since(start_time)
|
|
@@ -1124,7 +2012,7 @@ module Scint
|
|
|
1124
2012
|
end
|
|
1125
2013
|
|
|
1126
2014
|
def force_purge_artifacts(resolved, bundle_path, cache)
|
|
1127
|
-
ruby_dir =
|
|
2015
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
1128
2016
|
ext_root = File.join(ruby_dir, "extensions", Platform.gem_arch, Platform.extension_api_version)
|
|
1129
2017
|
|
|
1130
2018
|
resolved.each do |spec|
|
|
@@ -1160,6 +2048,28 @@ module Scint
|
|
|
1160
2048
|
"#{format_elapsed(elapsed_ms)}, #{workers} #{noun} used"
|
|
1161
2049
|
end
|
|
1162
2050
|
|
|
2051
|
+
def emit_network_error_details(error)
|
|
2052
|
+
return unless error.is_a?(NetworkError)
|
|
2053
|
+
|
|
2054
|
+
headers = error.response_headers
|
|
2055
|
+
body = error.response_body.to_s
|
|
2056
|
+
return if (headers.nil? || headers.empty?) && body.empty?
|
|
2057
|
+
|
|
2058
|
+
if headers && !headers.empty?
|
|
2059
|
+
$stderr.puts " headers:"
|
|
2060
|
+
headers.sort.each do |key, value|
|
|
2061
|
+
$stderr.puts " #{key}: #{value}"
|
|
2062
|
+
end
|
|
2063
|
+
end
|
|
2064
|
+
|
|
2065
|
+
return if body.empty?
|
|
2066
|
+
|
|
2067
|
+
$stderr.puts " body:"
|
|
2068
|
+
body.each_line do |line|
|
|
2069
|
+
$stderr.puts " #{line.rstrip}"
|
|
2070
|
+
end
|
|
2071
|
+
end
|
|
2072
|
+
|
|
1163
2073
|
def warn_missing_bundle_gitignore_entry
|
|
1164
2074
|
path = ".gitignore"
|
|
1165
2075
|
return unless File.file?(path)
|