scint 0.1.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 -0
- 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 +1214 -134
- 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 +90 -3
- data/lib/scint/gemfile/parser.rb +31 -4
- data/lib/scint/index/client.rb +1 -1
- data/lib/scint/installer/extension_builder.rb +95 -30
- data/lib/scint/installer/linker.rb +9 -25
- data/lib/scint/installer/planner.rb +37 -13
- data/lib/scint/installer/preparer.rb +2 -9
- data/lib/scint/lockfile/parser.rb +2 -1
- data/lib/scint/lockfile/writer.rb +78 -35
- data/lib/scint/platform.rb +8 -0
- data/lib/scint/progress.rb +128 -73
- data/lib/scint/resolver/provider.rb +71 -7
- 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/vendor/pub_grub/version.rb +5 -1
- data/lib/scint/version.rb +5 -0
- data/lib/scint.rb +3 -2
- metadata +5 -7
- data/bin/bundler-vs-scint +0 -233
- data/bin/scint-io-summary +0 -46
- data/bin/scint-syscall-trace +0 -41
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
|
|
|
@@ -52,10 +59,13 @@ module Scint
|
|
|
52
59
|
|
|
53
60
|
cache = Scint::Cache::Layout.new
|
|
54
61
|
bundle_path = @path || ENV["BUNDLER_PATH"] || ".bundle"
|
|
62
|
+
bundle_display = display_bundle_path(bundle_path)
|
|
55
63
|
bundle_path = File.expand_path(bundle_path)
|
|
56
64
|
worker_count = @jobs || [Platform.cpu_count * 2, 50].min
|
|
57
65
|
compile_slots = compile_slots_for(worker_count)
|
|
58
66
|
per_type_limits = install_task_limits(worker_count, compile_slots)
|
|
67
|
+
$stdout.puts "#{GREEN}💎#{RESET} Scintellating Gemfile into #{BOLD}#{bundle_display}#{RESET} #{DIM}(scint #{VERSION}, ruby #{RUBY_VERSION})#{RESET}"
|
|
68
|
+
$stdout.puts
|
|
59
69
|
|
|
60
70
|
# 0. Build credential store from config files (~/.bundle/config, XDG scint/credentials)
|
|
61
71
|
@credentials = Credentials.new
|
|
@@ -114,10 +124,18 @@ module Scint
|
|
|
114
124
|
# Scale up for download/install phase based on actual work count
|
|
115
125
|
scheduler.scale_workers(to_install.size)
|
|
116
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
|
+
|
|
117
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)
|
|
118
135
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
136
|
+
worker_count = scheduler.stats[:workers]
|
|
119
137
|
warn_missing_bundle_gitignore_entry
|
|
120
|
-
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems)}. #{DIM}(#{
|
|
138
|
+
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems)}. #{DIM}(#{format_run_footer(elapsed_ms, worker_count)})#{RESET}"
|
|
121
139
|
return 0
|
|
122
140
|
end
|
|
123
141
|
|
|
@@ -144,13 +162,16 @@ module Scint
|
|
|
144
162
|
if errors.any?
|
|
145
163
|
$stderr.puts "#{RED}Some gems failed to install:#{RESET}"
|
|
146
164
|
errors.each do |err|
|
|
147
|
-
|
|
165
|
+
error = err[:error]
|
|
166
|
+
$stderr.puts " #{BOLD}#{err[:name]}#{RESET}: #{error.message}"
|
|
167
|
+
emit_network_error_details(error)
|
|
148
168
|
end
|
|
149
169
|
elsif stats[:failed] > 0
|
|
150
170
|
$stderr.puts "#{YELLOW}Warning: #{stats[:failed]} jobs failed but no error details captured#{RESET}"
|
|
151
171
|
end
|
|
152
172
|
|
|
153
173
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
174
|
+
worker_count = stats[:workers]
|
|
154
175
|
failed = errors.filter_map { |e| e[:name] }.uniq
|
|
155
176
|
failed_count = failed.size
|
|
156
177
|
failed_count = 1 if failed_count.zero? && stats[:failed] > 0
|
|
@@ -159,18 +180,22 @@ module Scint
|
|
|
159
180
|
|
|
160
181
|
if has_failures
|
|
161
182
|
warn_missing_bundle_gitignore_entry
|
|
162
|
-
$stdout.puts "\n#{RED}Bundle failed!#{RESET} #{installed_total}/#{total_gems} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems, failed: failed_count)}. #{DIM}(#{
|
|
183
|
+
$stdout.puts "\n#{RED}Bundle failed!#{RESET} #{installed_total}/#{total_gems} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems, failed: failed_count)}. #{DIM}(#{format_run_footer(elapsed_ms, worker_count)})#{RESET}"
|
|
163
184
|
1
|
|
164
185
|
else
|
|
165
186
|
# 10. Write lockfile + runtime config only for successful installs
|
|
166
|
-
write_lockfile(resolved, gemfile)
|
|
187
|
+
write_lockfile(resolved, gemfile, lockfile)
|
|
167
188
|
write_runtime_config(resolved, bundle_path)
|
|
168
189
|
warn_missing_bundle_gitignore_entry
|
|
169
|
-
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems)}. #{DIM}(#{
|
|
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}"
|
|
170
191
|
0
|
|
171
192
|
end
|
|
172
193
|
ensure
|
|
173
|
-
|
|
194
|
+
begin
|
|
195
|
+
scheduler.shutdown
|
|
196
|
+
ensure
|
|
197
|
+
close_download_pool
|
|
198
|
+
end
|
|
174
199
|
end
|
|
175
200
|
end
|
|
176
201
|
|
|
@@ -194,7 +219,7 @@ module Scint
|
|
|
194
219
|
remote_uri: nil,
|
|
195
220
|
checksum: nil,
|
|
196
221
|
)
|
|
197
|
-
resolved
|
|
222
|
+
resolved.unshift(scint_spec)
|
|
198
223
|
|
|
199
224
|
resolved
|
|
200
225
|
end
|
|
@@ -202,7 +227,7 @@ module Scint
|
|
|
202
227
|
def dedupe_resolved_specs(resolved)
|
|
203
228
|
seen = {}
|
|
204
229
|
resolved.each do |spec|
|
|
205
|
-
key =
|
|
230
|
+
key = SpecUtils.full_key(spec)
|
|
206
231
|
seen[key] ||= spec
|
|
207
232
|
end
|
|
208
233
|
seen.values
|
|
@@ -212,8 +237,8 @@ module Scint
|
|
|
212
237
|
# No download needed — we know exactly where we are.
|
|
213
238
|
def install_builtin_gem(entry, bundle_path)
|
|
214
239
|
spec = entry.spec
|
|
215
|
-
ruby_dir =
|
|
216
|
-
full_name =
|
|
240
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
241
|
+
full_name = SpecUtils.full_name(spec)
|
|
217
242
|
scint_root = File.expand_path("../../..", __FILE__)
|
|
218
243
|
|
|
219
244
|
# Copy gem files into gems/scint-x.y.z/lib/
|
|
@@ -221,7 +246,7 @@ module Scint
|
|
|
221
246
|
lib_dest = File.join(gem_dest, "lib")
|
|
222
247
|
unless Dir.exist?(lib_dest)
|
|
223
248
|
FS.mkdir_p(lib_dest)
|
|
224
|
-
FS.
|
|
249
|
+
FS.clone_tree(scint_root, lib_dest)
|
|
225
250
|
end
|
|
226
251
|
|
|
227
252
|
# Write gemspec
|
|
@@ -255,16 +280,20 @@ module Scint
|
|
|
255
280
|
def clone_git_source(source, cache)
|
|
256
281
|
return unless source.respond_to?(:uri)
|
|
257
282
|
git_dir = cache.git_path(source.uri)
|
|
258
|
-
|
|
283
|
+
if Dir.exist?(git_dir)
|
|
284
|
+
fetch_git_repo(git_dir)
|
|
285
|
+
return
|
|
286
|
+
end
|
|
259
287
|
|
|
260
|
-
|
|
261
|
-
system("git", "clone", "--bare", source.uri.to_s, git_dir,
|
|
262
|
-
[:out, :err] => File::NULL)
|
|
288
|
+
clone_git_repo(source.uri, git_dir)
|
|
263
289
|
end
|
|
264
290
|
|
|
265
291
|
def resolve(gemfile, lockfile, cache)
|
|
266
292
|
# If lockfile is up-to-date, use its specs directly
|
|
267
|
-
if lockfile &&
|
|
293
|
+
if lockfile &&
|
|
294
|
+
lockfile_current?(gemfile, lockfile) &&
|
|
295
|
+
lockfile_dependency_graph_valid?(lockfile) &&
|
|
296
|
+
lockfile_git_source_mapping_valid?(lockfile, cache)
|
|
268
297
|
return lockfile_to_resolved(lockfile)
|
|
269
298
|
end
|
|
270
299
|
|
|
@@ -299,6 +328,7 @@ module Scint
|
|
|
299
328
|
# Build path_gems: gem_name => { version:, dependencies:, source: }
|
|
300
329
|
# for gems with path: or git: sources (skip compact index for these)
|
|
301
330
|
path_gems = {}
|
|
331
|
+
git_source_metadata_cache = {}
|
|
302
332
|
gemfile.dependencies.each do |dep|
|
|
303
333
|
opts = dep.source_options
|
|
304
334
|
next unless opts[:path] || opts[:git]
|
|
@@ -308,19 +338,57 @@ module Scint
|
|
|
308
338
|
|
|
309
339
|
# Try to read version and deps from gemspec if it's a path gem
|
|
310
340
|
if opts[:path]
|
|
311
|
-
gemspec = find_gemspec(opts[:path], dep.name)
|
|
341
|
+
gemspec = find_gemspec(opts[:path], dep.name, glob: opts[:glob])
|
|
312
342
|
if gemspec
|
|
313
343
|
version = gemspec.version.to_s
|
|
314
344
|
deps = gemspec.dependencies
|
|
315
345
|
.select { |d| d.type == :runtime }
|
|
316
|
-
.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
|
|
317
350
|
end
|
|
318
351
|
end
|
|
319
352
|
|
|
320
|
-
# For git gems,
|
|
321
|
-
if opts[:git]
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
324
392
|
end
|
|
325
393
|
|
|
326
394
|
source_desc = opts[:path] || opts[:git] || "local"
|
|
@@ -347,14 +415,16 @@ module Scint
|
|
|
347
415
|
resolver.resolve
|
|
348
416
|
end
|
|
349
417
|
|
|
350
|
-
def find_gemspec(path, gem_name)
|
|
418
|
+
def find_gemspec(path, gem_name, glob: nil)
|
|
351
419
|
return nil unless Dir.exist?(path)
|
|
352
420
|
|
|
421
|
+
glob_pattern = glob || Source::Path::DEFAULT_GLOB
|
|
353
422
|
# Look for exact match first, then any gemspec
|
|
354
423
|
candidates = [
|
|
355
424
|
File.join(path, "#{gem_name}.gemspec"),
|
|
425
|
+
*Dir.glob(File.join(path, glob_pattern)),
|
|
356
426
|
*Dir.glob(File.join(path, "*.gemspec")),
|
|
357
|
-
]
|
|
427
|
+
].uniq
|
|
358
428
|
|
|
359
429
|
candidates.each do |gs|
|
|
360
430
|
next unless File.exist?(gs)
|
|
@@ -368,16 +438,148 @@ module Scint
|
|
|
368
438
|
nil
|
|
369
439
|
end
|
|
370
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
|
+
|
|
371
501
|
def lockfile_current?(gemfile, lockfile)
|
|
372
502
|
return false unless lockfile
|
|
373
503
|
|
|
374
504
|
locked_names = Set.new(lockfile.specs.map { |s| s[:name] })
|
|
375
|
-
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
|
|
376
570
|
end
|
|
377
571
|
|
|
378
572
|
def lockfile_to_resolved(lockfile)
|
|
379
|
-
|
|
380
|
-
|
|
573
|
+
local_plat = Platform.local_platform
|
|
574
|
+
|
|
575
|
+
# Pick one best platform variant per gem+version from lockfile specs.
|
|
576
|
+
by_gem = Hash.new { |h, k| h[k] = [] }
|
|
577
|
+
lockfile.specs.each { |ls| by_gem[[ls[:name], ls[:version]]] << ls }
|
|
578
|
+
|
|
579
|
+
resolved = by_gem.map do |(_name, _version), specs|
|
|
580
|
+
best = pick_best_platform_spec(specs, local_plat)
|
|
581
|
+
|
|
582
|
+
source = best[:source]
|
|
381
583
|
source_value =
|
|
382
584
|
if source.is_a?(Source::Rubygems)
|
|
383
585
|
source.uri.to_s
|
|
@@ -386,23 +588,205 @@ module Scint
|
|
|
386
588
|
end
|
|
387
589
|
|
|
388
590
|
ResolvedSpec.new(
|
|
389
|
-
name:
|
|
390
|
-
version:
|
|
391
|
-
platform:
|
|
392
|
-
dependencies:
|
|
591
|
+
name: best[:name],
|
|
592
|
+
version: best[:version],
|
|
593
|
+
platform: best[:platform],
|
|
594
|
+
dependencies: best[:dependencies],
|
|
393
595
|
source: source_value,
|
|
394
596
|
has_extensions: false,
|
|
395
597
|
remote_uri: nil,
|
|
396
|
-
checksum:
|
|
598
|
+
checksum: best[:checksum],
|
|
397
599
|
)
|
|
398
600
|
end
|
|
601
|
+
|
|
602
|
+
apply_locked_platform_preferences(resolved)
|
|
603
|
+
end
|
|
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
|
+
|
|
707
|
+
# Preference: exact platform match > compatible match > ruby > first.
|
|
708
|
+
def pick_best_platform_spec(specs, local_plat)
|
|
709
|
+
return specs.first if specs.size == 1
|
|
710
|
+
|
|
711
|
+
best = nil
|
|
712
|
+
best_score = -2
|
|
713
|
+
|
|
714
|
+
specs.each do |ls|
|
|
715
|
+
platform = ls[:platform] || "ruby"
|
|
716
|
+
if platform == "ruby"
|
|
717
|
+
score = 0
|
|
718
|
+
else
|
|
719
|
+
spec_plat = Gem::Platform.new(platform)
|
|
720
|
+
if spec_plat === local_plat
|
|
721
|
+
score = spec_plat.to_s == local_plat.to_s ? 2 : 1
|
|
722
|
+
else
|
|
723
|
+
score = -1
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
if score > best_score
|
|
728
|
+
best = ls
|
|
729
|
+
best_score = score
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
best
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Lockfiles can carry only the ruby variant for a gem version.
|
|
737
|
+
# Re-check compact index for the same locked version and upgrade to the
|
|
738
|
+
# best local platform variant when available.
|
|
739
|
+
def apply_locked_platform_preferences(resolved_specs)
|
|
740
|
+
preferred = preferred_platforms_for_locked_specs(resolved_specs)
|
|
741
|
+
return resolved_specs if preferred.empty?
|
|
742
|
+
|
|
743
|
+
resolved_specs.each do |spec|
|
|
744
|
+
key = SpecUtils.full_name_for(spec.name, spec.version)
|
|
745
|
+
platform = preferred[key]
|
|
746
|
+
next if platform.nil? || platform.empty?
|
|
747
|
+
|
|
748
|
+
spec.platform = platform
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
resolved_specs
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def preferred_platforms_for_locked_specs(resolved_specs)
|
|
755
|
+
out = {}
|
|
756
|
+
by_source = resolved_specs
|
|
757
|
+
.select { |spec| rubygems_source_uri?(spec.source) }
|
|
758
|
+
.group_by { |spec| spec.source.to_s.chomp("/") }
|
|
759
|
+
|
|
760
|
+
by_source.each do |source_uri, specs|
|
|
761
|
+
begin
|
|
762
|
+
client = Index::Client.new(source_uri, credentials: @credentials)
|
|
763
|
+
provider = Resolver::Provider.new(client)
|
|
764
|
+
provider.prefetch(specs.map(&:name).uniq)
|
|
765
|
+
|
|
766
|
+
specs.each do |spec|
|
|
767
|
+
preferred = provider.preferred_platform_for(spec.name, Gem::Version.new(spec.version.to_s))
|
|
768
|
+
preferred = preferred.to_s
|
|
769
|
+
next if preferred.empty? || preferred == spec.platform.to_s
|
|
770
|
+
|
|
771
|
+
out[SpecUtils.full_name_for(spec.name, spec.version)] = preferred
|
|
772
|
+
end
|
|
773
|
+
rescue StandardError
|
|
774
|
+
next
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
out
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def rubygems_source_uri?(source)
|
|
782
|
+
source.is_a?(String) && source.match?(%r{\Ahttps?://})
|
|
399
783
|
end
|
|
400
784
|
|
|
401
785
|
def download_gem(entry, cache)
|
|
402
786
|
spec = entry.spec
|
|
403
787
|
source = spec.source
|
|
404
788
|
if git_source?(source)
|
|
405
|
-
|
|
789
|
+
prepare_git_checkout(spec, cache, fetch: false)
|
|
406
790
|
return
|
|
407
791
|
end
|
|
408
792
|
source_uri = source.to_s
|
|
@@ -410,7 +794,7 @@ module Scint
|
|
|
410
794
|
# Path gems are not downloaded from a remote
|
|
411
795
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
412
796
|
|
|
413
|
-
full_name =
|
|
797
|
+
full_name = SpecUtils.full_name(spec)
|
|
414
798
|
gem_filename = "#{full_name}.gem"
|
|
415
799
|
source_uri = source_uri.chomp("/")
|
|
416
800
|
download_uri = "#{source_uri}/gems/#{gem_filename}"
|
|
@@ -419,9 +803,20 @@ module Scint
|
|
|
419
803
|
FS.mkdir_p(File.dirname(dest_path))
|
|
420
804
|
|
|
421
805
|
unless File.exist?(dest_path)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
|
425
820
|
end
|
|
426
821
|
end
|
|
427
822
|
|
|
@@ -429,8 +824,12 @@ module Scint
|
|
|
429
824
|
spec = entry.spec
|
|
430
825
|
source_uri = spec.source.to_s
|
|
431
826
|
|
|
432
|
-
# Git
|
|
433
|
-
|
|
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
|
|
434
833
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
435
834
|
|
|
436
835
|
extracted = cache.extracted_path(spec)
|
|
@@ -452,36 +851,132 @@ module Scint
|
|
|
452
851
|
source_str.end_with?(".git") || source_str.include?(".git/")
|
|
453
852
|
end
|
|
454
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
|
+
|
|
455
870
|
def prepare_git_source(entry, cache)
|
|
871
|
+
# Legacy helper used by tests/callers that expect git download+extract
|
|
872
|
+
# in a single step.
|
|
456
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)
|
|
457
879
|
source = spec.source
|
|
458
880
|
uri, revision = git_source_ref(source)
|
|
881
|
+
submodules = git_source_submodules?(source)
|
|
459
882
|
|
|
460
883
|
bare_repo = cache.git_path(uri)
|
|
461
884
|
|
|
462
885
|
# Serialize all git operations per bare repo — git uses index.lock
|
|
463
886
|
# and can't handle concurrent checkouts from the same repo.
|
|
464
887
|
git_mutex_for(bare_repo).synchronize do
|
|
465
|
-
|
|
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
|
|
894
|
+
|
|
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
|
|
466
908
|
|
|
467
|
-
|
|
468
|
-
|
|
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
|
|
469
921
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
922
|
+
tmp = "#{extracted}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
923
|
+
begin
|
|
924
|
+
FileUtils.rm_rf(tmp)
|
|
925
|
+
FS.clone_tree(gem_root, tmp)
|
|
474
926
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
|
480
934
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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,
|
|
961
|
+
resolved_revision,
|
|
962
|
+
spec,
|
|
963
|
+
uri,
|
|
964
|
+
)
|
|
965
|
+
else
|
|
966
|
+
checkout_git_tree(
|
|
967
|
+
bare_repo,
|
|
968
|
+
tmp,
|
|
969
|
+
resolved_revision,
|
|
970
|
+
spec,
|
|
971
|
+
uri,
|
|
972
|
+
)
|
|
484
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)
|
|
485
980
|
end
|
|
486
981
|
end
|
|
487
982
|
|
|
@@ -494,6 +989,68 @@ module Scint
|
|
|
494
989
|
[source.to_s, "HEAD"]
|
|
495
990
|
end
|
|
496
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
|
+
|
|
497
1054
|
def git_mutex_for(repo_path)
|
|
498
1055
|
@git_mutexes_lock ||= Thread::Mutex.new
|
|
499
1056
|
@git_mutexes_lock.synchronize do
|
|
@@ -504,26 +1061,89 @@ module Scint
|
|
|
504
1061
|
|
|
505
1062
|
def clone_git_repo(uri, bare_repo)
|
|
506
1063
|
FS.mkdir_p(File.dirname(bare_repo))
|
|
507
|
-
_out, err, status =
|
|
1064
|
+
_out, err, status = git_capture3("clone", "--bare", uri.to_s, bare_repo)
|
|
508
1065
|
unless status.success?
|
|
509
1066
|
raise InstallError, "Git clone failed for #{uri}: #{err.to_s.strip}"
|
|
510
1067
|
end
|
|
511
1068
|
end
|
|
512
1069
|
|
|
1070
|
+
def fetch_git_repo(bare_repo)
|
|
1071
|
+
_out, err, status = git_capture3(
|
|
1072
|
+
"--git-dir", bare_repo,
|
|
1073
|
+
"fetch",
|
|
1074
|
+
"--prune",
|
|
1075
|
+
"origin",
|
|
1076
|
+
"+refs/heads/*:refs/heads/*",
|
|
1077
|
+
"+refs/tags/*:refs/tags/*",
|
|
1078
|
+
)
|
|
1079
|
+
unless status.success?
|
|
1080
|
+
raise InstallError, "Git fetch failed for #{bare_repo}: #{err.to_s.strip}"
|
|
1081
|
+
end
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
def resolve_git_revision(bare_repo, revision)
|
|
1085
|
+
out, err, status = git_capture3("--git-dir", bare_repo, "rev-parse", "#{revision}^{commit}")
|
|
1086
|
+
unless status.success?
|
|
1087
|
+
raise InstallError, "Unable to resolve git revision #{revision.inspect} in #{bare_repo}: #{err.to_s.strip}"
|
|
1088
|
+
end
|
|
1089
|
+
out.strip
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def git_capture3(*args)
|
|
1093
|
+
Open3.capture3("git", "-c", "core.fsmonitor=false", *args)
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def git_checkout_marker_path(dir)
|
|
1097
|
+
"#{dir}.scint_git_revision"
|
|
1098
|
+
end
|
|
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
|
+
|
|
513
1129
|
def compile_slots_for(worker_count)
|
|
514
|
-
# Keep
|
|
515
|
-
#
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
519
1136
|
end
|
|
520
1137
|
|
|
521
1138
|
def install_task_limits(worker_count, compile_slots)
|
|
522
1139
|
# Leave headroom for compile and binstub lanes so link/download
|
|
523
1140
|
# throughput cannot fully starve them.
|
|
524
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
|
|
525
1145
|
{
|
|
526
|
-
download:
|
|
1146
|
+
download: download_limit,
|
|
527
1147
|
extract: io_cpu_limit,
|
|
528
1148
|
link: io_cpu_limit,
|
|
529
1149
|
build_ext: compile_slots,
|
|
@@ -531,6 +1151,12 @@ module Scint
|
|
|
531
1151
|
}
|
|
532
1152
|
end
|
|
533
1153
|
|
|
1154
|
+
def display_bundle_path(path)
|
|
1155
|
+
return path if path.start_with?("/", "./", "../")
|
|
1156
|
+
|
|
1157
|
+
"./#{path}"
|
|
1158
|
+
end
|
|
1159
|
+
|
|
534
1160
|
# Enqueue dependency-aware install tasks so compile/binstub can run
|
|
535
1161
|
# concurrently with link/download once prerequisites are satisfied.
|
|
536
1162
|
def enqueue_install_dag(scheduler, plan, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
@@ -545,8 +1171,8 @@ module Scint
|
|
|
545
1171
|
when :skip
|
|
546
1172
|
next
|
|
547
1173
|
when :builtin
|
|
548
|
-
|
|
549
|
-
|
|
1174
|
+
link_id = scheduler.enqueue(:link, entry.spec.name,
|
|
1175
|
+
-> { install_builtin_gem(entry, bundle_path) })
|
|
550
1176
|
when :download
|
|
551
1177
|
key = spec_key(entry.spec)
|
|
552
1178
|
download_id = scheduler.enqueue(:download, entry.spec.name,
|
|
@@ -563,7 +1189,7 @@ module Scint
|
|
|
563
1189
|
build_depends = (depends_on + dep_links).uniq
|
|
564
1190
|
|
|
565
1191
|
extracted = extracted_path_for_entry(entry, cache)
|
|
566
|
-
if Installer::ExtensionBuilder.
|
|
1192
|
+
if Installer::ExtensionBuilder.needs_build?(entry.spec, extracted)
|
|
567
1193
|
build_id = scheduler.enqueue(:build_ext, entry.spec.name,
|
|
568
1194
|
-> { build_extensions(entry, cache, bundle_path, progress, compile_slots: compile_slots) },
|
|
569
1195
|
depends_on: build_depends)
|
|
@@ -626,7 +1252,7 @@ module Scint
|
|
|
626
1252
|
end
|
|
627
1253
|
|
|
628
1254
|
def spec_key(spec)
|
|
629
|
-
|
|
1255
|
+
SpecUtils.full_key(spec)
|
|
630
1256
|
end
|
|
631
1257
|
|
|
632
1258
|
def dependency_link_job_ids(spec, link_job_by_name)
|
|
@@ -649,7 +1275,7 @@ module Scint
|
|
|
649
1275
|
enqueued = 0
|
|
650
1276
|
entries.each do |entry|
|
|
651
1277
|
extracted = extracted_path_for_entry(entry, cache)
|
|
652
|
-
next unless Installer::ExtensionBuilder.
|
|
1278
|
+
next unless Installer::ExtensionBuilder.needs_build?(entry.spec, extracted)
|
|
653
1279
|
|
|
654
1280
|
scheduler.enqueue(:build_ext, entry.spec.name,
|
|
655
1281
|
-> { build_extensions(entry, cache, bundle_path, nil, compile_slots: compile_slots) })
|
|
@@ -661,10 +1287,75 @@ module Scint
|
|
|
661
1287
|
def extracted_path_for_entry(entry, cache)
|
|
662
1288
|
source_str = entry.spec.source.to_s
|
|
663
1289
|
if source_str.start_with?("/") && Dir.exist?(source_str)
|
|
664
|
-
|
|
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
|
|
665
1299
|
else
|
|
666
|
-
entry.cached_path || cache.extracted_path(entry.spec)
|
|
1300
|
+
base = entry.cached_path || cache.extracted_path(entry.spec)
|
|
1301
|
+
if git_source?(entry.spec.source) && Dir.exist?(base)
|
|
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
|
|
1309
|
+
else
|
|
1310
|
+
base
|
|
1311
|
+
end
|
|
1312
|
+
end
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# For git monorepo sources, map gem name to its gemspec subdirectory.
|
|
1316
|
+
def resolve_git_gem_subdir(repo_root, spec)
|
|
1317
|
+
name = spec.name
|
|
1318
|
+
return repo_root if File.exist?(File.join(repo_root, "#{name}.gemspec"))
|
|
1319
|
+
|
|
1320
|
+
source = spec.source
|
|
1321
|
+
glob = source.respond_to?(:glob) ? source.glob : Source::Git::DEFAULT_GLOB
|
|
1322
|
+
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
1323
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
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
|
|
1330
|
+
|
|
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
|
|
667
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})"
|
|
668
1359
|
end
|
|
669
1360
|
|
|
670
1361
|
def link_gem_files(entry, cache, bundle_path)
|
|
@@ -680,14 +1371,13 @@ module Scint
|
|
|
680
1371
|
from_cache: true,
|
|
681
1372
|
)
|
|
682
1373
|
Installer::Linker.link_files(prepared, bundle_path)
|
|
683
|
-
Installer::Linker.link_files_to_ruby_dir(prepared, cache.install_ruby_dir)
|
|
684
1374
|
# If this gem has a cached native build, materialize it during link.
|
|
685
1375
|
# This lets reinstalling into a fresh .bundle skip build_ext entirely.
|
|
686
1376
|
Installer::ExtensionBuilder.link_cached_build(prepared, bundle_path, cache)
|
|
687
1377
|
end
|
|
688
1378
|
|
|
689
1379
|
def build_extensions(entry, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
690
|
-
extracted = entry
|
|
1380
|
+
extracted = extracted_path_for_entry(entry, cache)
|
|
691
1381
|
gemspec = load_gemspec(extracted, entry.spec, cache)
|
|
692
1382
|
|
|
693
1383
|
sync_build_env_dependencies(entry.spec, bundle_path, cache)
|
|
@@ -716,9 +1406,11 @@ module Scint
|
|
|
716
1406
|
dep.name
|
|
717
1407
|
end
|
|
718
1408
|
end
|
|
1409
|
+
dep_names << "rake"
|
|
1410
|
+
dep_names.uniq!
|
|
719
1411
|
return if dep_names.empty?
|
|
720
1412
|
|
|
721
|
-
source_ruby_dir =
|
|
1413
|
+
source_ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
722
1414
|
target_ruby_dir = cache.install_ruby_dir
|
|
723
1415
|
|
|
724
1416
|
dep_names.each do |name|
|
|
@@ -734,7 +1426,7 @@ module Scint
|
|
|
734
1426
|
next unless Dir.exist?(source_gem_dir)
|
|
735
1427
|
|
|
736
1428
|
target_gem_dir = File.join(target_ruby_dir, "gems", full_name)
|
|
737
|
-
FS.
|
|
1429
|
+
FS.clone_tree(source_gem_dir, target_gem_dir) unless Dir.exist?(target_gem_dir)
|
|
738
1430
|
|
|
739
1431
|
target_spec_dir = File.join(target_ruby_dir, "specifications")
|
|
740
1432
|
target_spec_path = File.join(target_spec_dir, "#{full_name}.gemspec")
|
|
@@ -758,8 +1450,15 @@ module Scint
|
|
|
758
1450
|
end
|
|
759
1451
|
|
|
760
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
|
+
|
|
761
1457
|
cached = load_cached_gemspec(spec, cache, extracted_path)
|
|
762
|
-
|
|
1458
|
+
if cached
|
|
1459
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = cached }
|
|
1460
|
+
return cached
|
|
1461
|
+
end
|
|
763
1462
|
|
|
764
1463
|
inbound = cache.inbound_path(spec)
|
|
765
1464
|
return nil unless File.exist?(inbound)
|
|
@@ -767,12 +1466,38 @@ module Scint
|
|
|
767
1466
|
begin
|
|
768
1467
|
metadata = GemPkg::Package.new.read_metadata(inbound)
|
|
769
1468
|
cache_gemspec(spec, metadata, cache)
|
|
1469
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = metadata }
|
|
770
1470
|
metadata
|
|
771
1471
|
rescue StandardError
|
|
772
1472
|
nil
|
|
773
1473
|
end
|
|
774
1474
|
end
|
|
775
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
|
+
|
|
776
1501
|
def load_cached_gemspec(spec, cache, extracted_path)
|
|
777
1502
|
path = cache.spec_cache_path(spec)
|
|
778
1503
|
return nil unless File.exist?(path)
|
|
@@ -832,79 +1557,386 @@ module Scint
|
|
|
832
1557
|
|
|
833
1558
|
# --- Lockfile + runtime config ---
|
|
834
1559
|
|
|
835
|
-
def write_lockfile(resolved, gemfile)
|
|
836
|
-
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 = {}
|
|
837
1707
|
|
|
838
|
-
# Build source objects for path and git gems
|
|
839
1708
|
gemfile.dependencies.each do |dep|
|
|
840
1709
|
opts = dep.source_options
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
|
850
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
|
|
851
1834
|
end
|
|
1835
|
+
end
|
|
852
1836
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
.
|
|
856
|
-
|
|
857
|
-
|
|
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
|
|
858
1842
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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) }
|
|
865
1867
|
end
|
|
1868
|
+
out
|
|
1869
|
+
end
|
|
866
1870
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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
|
|
1891
|
+
|
|
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
|
|
870
1897
|
end
|
|
1898
|
+
platforms << "ruby"
|
|
1899
|
+
platforms.to_a
|
|
1900
|
+
end
|
|
871
1901
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
|
876
1915
|
|
|
877
|
-
|
|
878
|
-
specs: resolved,
|
|
879
|
-
dependencies: gemfile.dependencies.map { |d| { name: d.name, version_reqs: d.version_reqs } },
|
|
880
|
-
platforms: [Platform.local_platform.to_s, "ruby"].uniq,
|
|
881
|
-
sources: sources,
|
|
882
|
-
bundler_version: Scint::VERSION,
|
|
883
|
-
ruby_version: nil,
|
|
884
|
-
checksums: nil,
|
|
885
|
-
)
|
|
1916
|
+
return nil if checksums.empty?
|
|
886
1917
|
|
|
887
|
-
|
|
888
|
-
|
|
1918
|
+
checksums
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
def lockfile_spec_checksum_key(spec)
|
|
1922
|
+
SpecUtils.full_name_for(spec[:name], spec[:version], spec[:platform] || "ruby")
|
|
889
1923
|
end
|
|
890
1924
|
|
|
891
1925
|
def write_runtime_config(resolved, bundle_path)
|
|
892
|
-
ruby_dir =
|
|
893
|
-
RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
1926
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
894
1927
|
|
|
895
1928
|
data = {}
|
|
896
1929
|
resolved.each do |spec|
|
|
897
|
-
full =
|
|
1930
|
+
full = SpecUtils.full_name(spec)
|
|
898
1931
|
gem_dir = File.join(ruby_dir, "gems", full)
|
|
899
1932
|
spec_file = File.join(ruby_dir, "specifications", "#{full}.gemspec")
|
|
900
1933
|
require_paths = read_require_paths(spec_file)
|
|
901
1934
|
load_paths = require_paths
|
|
902
|
-
.map { |rp|
|
|
1935
|
+
.map { |rp| expand_require_path(gem_dir, rp) }
|
|
903
1936
|
.select { |path| Dir.exist?(path) }
|
|
904
1937
|
|
|
905
1938
|
default_lib = File.join(gem_dir, "lib")
|
|
906
1939
|
load_paths << default_lib if load_paths.empty? && Dir.exist?(default_lib)
|
|
907
|
-
load_paths.concat(detect_nested_lib_paths(gem_dir))
|
|
908
1940
|
load_paths.uniq!
|
|
909
1941
|
|
|
910
1942
|
# Add ext load path if extensions exist
|
|
@@ -912,6 +1944,12 @@ module Scint
|
|
|
912
1944
|
Platform.gem_arch, Platform.extension_api_version, full)
|
|
913
1945
|
load_paths << ext_dir if Dir.exist?(ext_dir)
|
|
914
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
|
+
|
|
915
1953
|
data[spec.name] = {
|
|
916
1954
|
version: spec.version.to_s,
|
|
917
1955
|
load_paths: load_paths,
|
|
@@ -932,26 +1970,40 @@ module Scint
|
|
|
932
1970
|
["lib"]
|
|
933
1971
|
end
|
|
934
1972
|
|
|
935
|
-
def
|
|
936
|
-
|
|
937
|
-
return
|
|
1973
|
+
def expand_require_path(gem_dir, require_path)
|
|
1974
|
+
value = require_path.to_s
|
|
1975
|
+
return value if Pathname.new(value).absolute?
|
|
938
1976
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
|
943
1990
|
end
|
|
944
|
-
return [] if top_level_rb
|
|
945
1991
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
+
[]
|
|
949
2003
|
end
|
|
950
2004
|
|
|
951
2005
|
def spec_full_name(spec)
|
|
952
|
-
|
|
953
|
-
plat = spec.respond_to?(:platform) ? spec.platform : nil
|
|
954
|
-
(plat.nil? || plat.to_s == "ruby" || plat.to_s.empty?) ? base : "#{base}-#{plat}"
|
|
2006
|
+
SpecUtils.full_name(spec)
|
|
955
2007
|
end
|
|
956
2008
|
|
|
957
2009
|
def elapsed_ms_since(start_time)
|
|
@@ -960,7 +2012,7 @@ module Scint
|
|
|
960
2012
|
end
|
|
961
2013
|
|
|
962
2014
|
def force_purge_artifacts(resolved, bundle_path, cache)
|
|
963
|
-
ruby_dir =
|
|
2015
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
964
2016
|
ext_root = File.join(ruby_dir, "extensions", Platform.gem_arch, Platform.extension_api_version)
|
|
965
2017
|
|
|
966
2018
|
resolved.each do |spec|
|
|
@@ -990,6 +2042,34 @@ module Scint
|
|
|
990
2042
|
"#{(elapsed_ms / 1000.0).round(2)}s"
|
|
991
2043
|
end
|
|
992
2044
|
|
|
2045
|
+
def format_run_footer(elapsed_ms, worker_count)
|
|
2046
|
+
workers = worker_count.to_i
|
|
2047
|
+
noun = workers == 1 ? "worker" : "workers"
|
|
2048
|
+
"#{format_elapsed(elapsed_ms)}, #{workers} #{noun} used"
|
|
2049
|
+
end
|
|
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
|
+
|
|
993
2073
|
def warn_missing_bundle_gitignore_entry
|
|
994
2074
|
path = ".gitignore"
|
|
995
2075
|
return unless File.file?(path)
|