scint 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/FEATURES.md +4 -0
- data/README.md +161 -41
- data/VERSION +1 -1
- data/bin/scint +9 -0
- data/lib/bundler.rb +106 -0
- data/lib/scint/cache/layout.rb +72 -14
- data/lib/scint/cache/manifest.rb +120 -0
- data/lib/scint/cache/metadata_store.rb +4 -11
- data/lib/scint/cache/prewarm.rb +445 -33
- data/lib/scint/cache/validity.rb +134 -0
- data/lib/scint/cli/cache.rb +36 -7
- data/lib/scint/cli/exec.rb +13 -25
- data/lib/scint/cli/install.rb +1452 -164
- 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 +215 -26
- data/lib/scint/gem/package.rb +6 -2
- data/lib/scint/gemfile/parser.rb +44 -10
- data/lib/scint/installer/extension_builder.rb +80 -55
- data/lib/scint/installer/linker.rb +51 -26
- data/lib/scint/installer/planner.rb +53 -34
- data/lib/scint/installer/preparer.rb +170 -47
- data/lib/scint/installer/promoter.rb +97 -0
- data/lib/scint/linker.sh +137 -0
- 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 +133 -0
- data/lib/scint.rb +1 -0
- metadata +6 -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"
|
|
@@ -22,7 +23,9 @@ require_relative "../downloader/pool"
|
|
|
22
23
|
require_relative "../gem/package"
|
|
23
24
|
require_relative "../gem/extractor"
|
|
24
25
|
require_relative "../cache/layout"
|
|
26
|
+
require_relative "../cache/manifest"
|
|
25
27
|
require_relative "../cache/metadata_store"
|
|
28
|
+
require_relative "../cache/validity"
|
|
26
29
|
require_relative "../installer/planner"
|
|
27
30
|
require_relative "../installer/linker"
|
|
28
31
|
require_relative "../installer/preparer"
|
|
@@ -32,31 +35,51 @@ require_relative "../resolver/provider"
|
|
|
32
35
|
require_relative "../resolver/resolver"
|
|
33
36
|
require_relative "../credentials"
|
|
34
37
|
require "open3"
|
|
38
|
+
require "set"
|
|
39
|
+
require "pathname"
|
|
35
40
|
|
|
36
41
|
module Scint
|
|
37
42
|
module CLI
|
|
38
43
|
class Install
|
|
39
44
|
RUNTIME_LOCK = "scint.lock.marshal"
|
|
40
45
|
|
|
41
|
-
def initialize(argv = [])
|
|
46
|
+
def initialize(argv = [], without: nil, with: nil)
|
|
42
47
|
@argv = argv
|
|
43
48
|
@jobs = nil
|
|
44
49
|
@path = nil
|
|
45
50
|
@verbose = false
|
|
46
51
|
@force = false
|
|
52
|
+
@without_groups = nil
|
|
53
|
+
@with_groups = nil
|
|
54
|
+
@download_pool = nil
|
|
55
|
+
@download_pool_lock = Thread::Mutex.new
|
|
56
|
+
@gemspec_cache = {}
|
|
57
|
+
@gemspec_cache_lock = Thread::Mutex.new
|
|
47
58
|
parse_options
|
|
59
|
+
# Allow programmatic override (for tests)
|
|
60
|
+
@without_groups = Array(without).map(&:to_sym) if without
|
|
61
|
+
@with_groups = Array(with).map(&:to_sym) if with
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def _tmark(label, t0)
|
|
65
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
66
|
+
$stderr.puts " [timing] #{label}: #{((now - t0) * 1000).round}ms" if ENV["SCINT_TIMING"]
|
|
67
|
+
now
|
|
48
68
|
end
|
|
49
69
|
|
|
50
70
|
def run
|
|
51
71
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
72
|
+
_t = start_time
|
|
52
73
|
|
|
53
74
|
cache = Scint::Cache::Layout.new
|
|
75
|
+
cache_telemetry = Scint::Cache::Telemetry.new
|
|
54
76
|
bundle_path = @path || ENV["BUNDLER_PATH"] || ".bundle"
|
|
55
77
|
bundle_display = display_bundle_path(bundle_path)
|
|
56
78
|
bundle_path = File.expand_path(bundle_path)
|
|
57
|
-
worker_count = @jobs || [Platform.cpu_count * 2, 50].min
|
|
79
|
+
worker_count = [(@jobs || [Platform.cpu_count * 2, 50].min).to_i, 1].max
|
|
58
80
|
compile_slots = compile_slots_for(worker_count)
|
|
59
|
-
|
|
81
|
+
git_slots = git_slots_for(worker_count)
|
|
82
|
+
per_type_limits = install_task_limits(worker_count, compile_slots, git_slots)
|
|
60
83
|
$stdout.puts "#{GREEN}💎#{RESET} Scintellating Gemfile into #{BOLD}#{bundle_display}#{RESET} #{DIM}(scint #{VERSION}, ruby #{RUBY_VERSION})#{RESET}"
|
|
61
84
|
$stdout.puts
|
|
62
85
|
|
|
@@ -68,6 +91,7 @@ module Scint
|
|
|
68
91
|
scheduler.start
|
|
69
92
|
|
|
70
93
|
begin
|
|
94
|
+
_t = _tmark("startup", _t)
|
|
71
95
|
# 2. Parse Gemfile
|
|
72
96
|
gemfile = Scint::Gemfile::Parser.parse("Gemfile")
|
|
73
97
|
|
|
@@ -79,6 +103,7 @@ module Scint
|
|
|
79
103
|
dep_count = gemfile.dependencies.size
|
|
80
104
|
scheduler.scale_workers(dep_count)
|
|
81
105
|
|
|
106
|
+
_t = _tmark("parse_gemfile", _t)
|
|
82
107
|
# 3. Enqueue index fetches for all sources immediately
|
|
83
108
|
gemfile.sources.each do |source|
|
|
84
109
|
scheduler.enqueue(:fetch_index, source[:uri] || source.to_s,
|
|
@@ -99,25 +124,39 @@ module Scint
|
|
|
99
124
|
-> { clone_git_source(source, cache) })
|
|
100
125
|
end
|
|
101
126
|
|
|
127
|
+
_t = _tmark("enqueue_fetches", _t)
|
|
102
128
|
# 6. Wait for index fetches, then resolve
|
|
103
129
|
scheduler.wait_for(:fetch_index)
|
|
130
|
+
_t = _tmark("wait_index", _t)
|
|
104
131
|
scheduler.wait_for(:git_clone)
|
|
132
|
+
_t = _tmark("wait_git", _t)
|
|
105
133
|
|
|
106
134
|
resolved = resolve(gemfile, lockfile, cache)
|
|
107
135
|
resolved = dedupe_resolved_specs(adjust_meta_gems(resolved))
|
|
136
|
+
resolved = filter_excluded_gems(resolved, gemfile)
|
|
108
137
|
force_purge_artifacts(resolved, bundle_path, cache) if @force
|
|
109
138
|
|
|
139
|
+
_t = _tmark("resolve", _t)
|
|
110
140
|
# 7. Plan: diff resolved vs installed
|
|
111
|
-
plan = Installer::Planner.plan(resolved, bundle_path, cache)
|
|
141
|
+
plan = Installer::Planner.plan(resolved, bundle_path, cache, telemetry: cache_telemetry)
|
|
112
142
|
total_gems = resolved.size
|
|
113
143
|
updated_gems = plan.count { |e| e.action != :skip }
|
|
114
144
|
cached_gems = total_gems - updated_gems
|
|
115
145
|
to_install = plan.reject { |e| e.action == :skip }
|
|
146
|
+
_t = _tmark("plan", _t)
|
|
116
147
|
|
|
117
148
|
# Scale up for download/install phase based on actual work count
|
|
118
149
|
scheduler.scale_workers(to_install.size)
|
|
119
150
|
|
|
151
|
+
# Warm-cache accelerator: pre-materialize cache-backed gem trees in
|
|
152
|
+
# batches so install workers avoid one cp process per gem.
|
|
153
|
+
bulk_prelink_gem_files(to_install, cache, bundle_path)
|
|
154
|
+
_t = _tmark("prelink", _t)
|
|
155
|
+
|
|
120
156
|
if to_install.empty?
|
|
157
|
+
# Keep lock artifacts aligned even when everything is already installed.
|
|
158
|
+
write_lockfile(resolved, gemfile, lockfile)
|
|
159
|
+
write_runtime_config(resolved, bundle_path)
|
|
121
160
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
122
161
|
worker_count = scheduler.stats[:workers]
|
|
123
162
|
warn_missing_bundle_gitignore_entry
|
|
@@ -148,7 +187,9 @@ module Scint
|
|
|
148
187
|
if errors.any?
|
|
149
188
|
$stderr.puts "#{RED}Some gems failed to install:#{RESET}"
|
|
150
189
|
errors.each do |err|
|
|
151
|
-
|
|
190
|
+
error = err[:error]
|
|
191
|
+
$stderr.puts " #{BOLD}#{err[:name]}#{RESET}: #{error.message}"
|
|
192
|
+
emit_network_error_details(error)
|
|
152
193
|
end
|
|
153
194
|
elsif stats[:failed] > 0
|
|
154
195
|
$stderr.puts "#{YELLOW}Warning: #{stats[:failed]} jobs failed but no error details captured#{RESET}"
|
|
@@ -168,14 +209,22 @@ module Scint
|
|
|
168
209
|
1
|
|
169
210
|
else
|
|
170
211
|
# 10. Write lockfile + runtime config only for successful installs
|
|
171
|
-
write_lockfile(resolved, gemfile)
|
|
212
|
+
write_lockfile(resolved, gemfile, lockfile)
|
|
172
213
|
write_runtime_config(resolved, bundle_path)
|
|
173
214
|
warn_missing_bundle_gitignore_entry
|
|
174
215
|
$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
216
|
0
|
|
176
217
|
end
|
|
177
218
|
ensure
|
|
178
|
-
|
|
219
|
+
begin
|
|
220
|
+
cache_telemetry.warn_if_needed(cache_root: cache.root)
|
|
221
|
+
ensure
|
|
222
|
+
begin
|
|
223
|
+
scheduler.shutdown
|
|
224
|
+
ensure
|
|
225
|
+
close_download_pool
|
|
226
|
+
end
|
|
227
|
+
end
|
|
179
228
|
end
|
|
180
229
|
end
|
|
181
230
|
|
|
@@ -207,18 +256,103 @@ module Scint
|
|
|
207
256
|
def dedupe_resolved_specs(resolved)
|
|
208
257
|
seen = {}
|
|
209
258
|
resolved.each do |spec|
|
|
210
|
-
key =
|
|
259
|
+
key = SpecUtils.full_key(spec)
|
|
211
260
|
seen[key] ||= spec
|
|
212
261
|
end
|
|
213
262
|
seen.values
|
|
214
263
|
end
|
|
215
264
|
|
|
265
|
+
# Determine which gem names should be excluded based on group settings.
|
|
266
|
+
# A gem is excluded if ALL of its group memberships are in excluded groups.
|
|
267
|
+
# Gems appearing in any non-excluded group are kept.
|
|
268
|
+
def excluded_gem_names(gemfile, resolved: nil)
|
|
269
|
+
excluded_groups = compute_excluded_groups(gemfile)
|
|
270
|
+
return Set.new if excluded_groups.empty?
|
|
271
|
+
|
|
272
|
+
# Build map: gem_name => set of all groups it appears in (across all declarations)
|
|
273
|
+
gem_groups = Hash.new { |h, k| h[k] = Set.new }
|
|
274
|
+
gemfile.dependencies.each do |dep|
|
|
275
|
+
dep.groups.each { |g| gem_groups[dep.name] << g }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# A gem is directly excluded if ALL its groups are excluded
|
|
279
|
+
directly_excluded = Set.new
|
|
280
|
+
gem_groups.each do |name, groups|
|
|
281
|
+
directly_excluded << name if groups.subset?(excluded_groups)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# If we have resolved specs, also exclude transitive-only deps
|
|
285
|
+
if resolved && directly_excluded.any?
|
|
286
|
+
exclude_transitive_deps(directly_excluded, resolved, gem_groups)
|
|
287
|
+
else
|
|
288
|
+
directly_excluded
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# Filter resolved specs, removing gems that belong only to excluded groups.
|
|
293
|
+
def filter_excluded_gems(resolved, gemfile)
|
|
294
|
+
excluded = excluded_gem_names(gemfile, resolved: resolved)
|
|
295
|
+
return resolved if excluded.empty?
|
|
296
|
+
|
|
297
|
+
resolved.reject { |spec| excluded.include?(spec.name) }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
private
|
|
301
|
+
|
|
302
|
+
def compute_excluded_groups(gemfile)
|
|
303
|
+
optional = Set.new(Array(gemfile.optional_groups))
|
|
304
|
+
without = Set.new(Array(@without_groups))
|
|
305
|
+
with = Set.new(Array(@with_groups))
|
|
306
|
+
|
|
307
|
+
# Optional groups are excluded by default unless explicitly included via --with
|
|
308
|
+
excluded = optional - with
|
|
309
|
+
# --without adds more groups to exclude
|
|
310
|
+
excluded.merge(without)
|
|
311
|
+
excluded
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Walk the dependency graph to find transitive deps that are ONLY
|
|
315
|
+
# reachable through excluded gems. Shared deps are kept.
|
|
316
|
+
def exclude_transitive_deps(directly_excluded, resolved, gem_groups)
|
|
317
|
+
# Build dependency graph: name => [dep_names]
|
|
318
|
+
dep_graph = {}
|
|
319
|
+
resolved.each do |spec|
|
|
320
|
+
dep_names = Array(spec.dependencies).filter_map do |dep|
|
|
321
|
+
if dep.is_a?(Hash)
|
|
322
|
+
dep[:name] || dep["name"]
|
|
323
|
+
elsif dep.respond_to?(:name)
|
|
324
|
+
dep.name
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
dep_graph[spec.name] = dep_names
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
all_names = Set.new(resolved.map(&:name))
|
|
331
|
+
|
|
332
|
+
# Start from Gemfile deps that are NOT excluded, then walk transitive deps
|
|
333
|
+
included_roots = gem_groups.keys.reject { |n| directly_excluded.include?(n) }
|
|
334
|
+
|
|
335
|
+
# BFS from included roots to find all reachable gems
|
|
336
|
+
reachable = Set.new
|
|
337
|
+
queue = included_roots.dup
|
|
338
|
+
while (name = queue.shift)
|
|
339
|
+
next if reachable.include?(name)
|
|
340
|
+
reachable << name
|
|
341
|
+
(dep_graph[name] || []).each { |dep| queue << dep }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Everything not reachable from included roots is excluded
|
|
345
|
+
all_names - reachable
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
public
|
|
349
|
+
|
|
216
350
|
# Install scint into the bundle by copying our own lib tree.
|
|
217
351
|
# No download needed — we know exactly where we are.
|
|
218
352
|
def install_builtin_gem(entry, bundle_path)
|
|
219
353
|
spec = entry.spec
|
|
220
|
-
ruby_dir =
|
|
221
|
-
full_name =
|
|
354
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
355
|
+
full_name = SpecUtils.full_name(spec)
|
|
222
356
|
scint_root = File.expand_path("../../..", __FILE__)
|
|
223
357
|
|
|
224
358
|
# Copy gem files into gems/scint-x.y.z/lib/
|
|
@@ -270,7 +404,10 @@ module Scint
|
|
|
270
404
|
|
|
271
405
|
def resolve(gemfile, lockfile, cache)
|
|
272
406
|
# If lockfile is up-to-date, use its specs directly
|
|
273
|
-
if lockfile &&
|
|
407
|
+
if lockfile &&
|
|
408
|
+
lockfile_current?(gemfile, lockfile) &&
|
|
409
|
+
lockfile_dependency_graph_valid?(lockfile) &&
|
|
410
|
+
lockfile_git_source_mapping_valid?(lockfile, cache)
|
|
274
411
|
return lockfile_to_resolved(lockfile)
|
|
275
412
|
end
|
|
276
413
|
|
|
@@ -305,6 +442,7 @@ module Scint
|
|
|
305
442
|
# Build path_gems: gem_name => { version:, dependencies:, source: }
|
|
306
443
|
# for gems with path: or git: sources (skip compact index for these)
|
|
307
444
|
path_gems = {}
|
|
445
|
+
git_source_metadata_cache = {}
|
|
308
446
|
gemfile.dependencies.each do |dep|
|
|
309
447
|
opts = dep.source_options
|
|
310
448
|
next unless opts[:path] || opts[:git]
|
|
@@ -314,19 +452,57 @@ module Scint
|
|
|
314
452
|
|
|
315
453
|
# Try to read version and deps from gemspec if it's a path gem
|
|
316
454
|
if opts[:path]
|
|
317
|
-
gemspec = find_gemspec(opts[:path], dep.name)
|
|
455
|
+
gemspec = find_gemspec(opts[:path], dep.name, glob: opts[:glob])
|
|
318
456
|
if gemspec
|
|
319
457
|
version = gemspec.version.to_s
|
|
320
458
|
deps = gemspec.dependencies
|
|
321
459
|
.select { |d| d.type == :runtime }
|
|
322
|
-
.map
|
|
460
|
+
.map do |d|
|
|
461
|
+
requirement_parts = d.requirement.requirements.map { |op, req_version| "#{op} #{req_version}" }
|
|
462
|
+
[d.name, requirement_parts]
|
|
463
|
+
end
|
|
323
464
|
end
|
|
324
465
|
end
|
|
325
466
|
|
|
326
|
-
# For git gems,
|
|
327
|
-
if opts[:git]
|
|
328
|
-
|
|
329
|
-
|
|
467
|
+
# For git gems, read version and dependencies from the available revision when possible.
|
|
468
|
+
if opts[:git]
|
|
469
|
+
git_source = find_matching_git_source(Array(lockfile&.sources), opts) || find_matching_git_source(gemfile.sources, opts)
|
|
470
|
+
revision_hint = git_source&.revision || git_source&.ref || opts[:ref] || opts[:branch] || opts[:tag] || "HEAD"
|
|
471
|
+
bare_repo = cache&.git_path(opts[:git])
|
|
472
|
+
if bare_repo && !Dir.exist?(bare_repo)
|
|
473
|
+
clone_git_repo(opts[:git], bare_repo)
|
|
474
|
+
elsif bare_repo && Dir.exist?(bare_repo)
|
|
475
|
+
fetch_git_repo(bare_repo)
|
|
476
|
+
end
|
|
477
|
+
if bare_repo && Dir.exist?(bare_repo)
|
|
478
|
+
begin
|
|
479
|
+
resolved_revision = resolve_git_revision(bare_repo, revision_hint)
|
|
480
|
+
cache_key = "#{opts[:git]}@#{resolved_revision}"
|
|
481
|
+
git_metadata = git_source_metadata_cache[cache_key]
|
|
482
|
+
unless git_metadata
|
|
483
|
+
git_metadata = build_git_path_gems_for_revision(
|
|
484
|
+
bare_repo,
|
|
485
|
+
resolved_revision,
|
|
486
|
+
glob: opts[:glob],
|
|
487
|
+
source_desc: opts[:git],
|
|
488
|
+
)
|
|
489
|
+
git_source_metadata_cache[cache_key] = git_metadata
|
|
490
|
+
end
|
|
491
|
+
path_gems.merge!(git_metadata)
|
|
492
|
+
current = git_metadata[dep.name]
|
|
493
|
+
if current
|
|
494
|
+
version = current[:version]
|
|
495
|
+
deps = current[:dependencies]
|
|
496
|
+
end
|
|
497
|
+
rescue StandardError
|
|
498
|
+
# Fall back to lockfile version below.
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
if lockfile && version == "0"
|
|
503
|
+
locked_spec = lockfile.specs.find { |s| s[:name] == dep.name }
|
|
504
|
+
version = locked_spec[:version] if locked_spec
|
|
505
|
+
end
|
|
330
506
|
end
|
|
331
507
|
|
|
332
508
|
source_desc = opts[:path] || opts[:git] || "local"
|
|
@@ -353,32 +529,158 @@ module Scint
|
|
|
353
529
|
resolver.resolve
|
|
354
530
|
end
|
|
355
531
|
|
|
356
|
-
def find_gemspec(path, gem_name)
|
|
532
|
+
def find_gemspec(path, gem_name, glob: nil)
|
|
357
533
|
return nil unless Dir.exist?(path)
|
|
358
534
|
|
|
535
|
+
glob_pattern = glob || Source::Path::DEFAULT_GLOB
|
|
359
536
|
# Look for exact match first, then any gemspec
|
|
360
537
|
candidates = [
|
|
361
538
|
File.join(path, "#{gem_name}.gemspec"),
|
|
539
|
+
*Dir.glob(File.join(path, glob_pattern)),
|
|
362
540
|
*Dir.glob(File.join(path, "*.gemspec")),
|
|
363
|
-
]
|
|
541
|
+
].uniq
|
|
364
542
|
|
|
365
543
|
candidates.each do |gs|
|
|
366
544
|
next unless File.exist?(gs)
|
|
367
545
|
begin
|
|
368
|
-
spec =
|
|
546
|
+
spec = SpecUtils.load_gemspec(gs, isolate: true)
|
|
369
547
|
return spec if spec
|
|
370
|
-
rescue StandardError
|
|
548
|
+
rescue SystemExit, StandardError
|
|
371
549
|
nil
|
|
372
550
|
end
|
|
373
551
|
end
|
|
374
552
|
nil
|
|
375
553
|
end
|
|
376
554
|
|
|
555
|
+
def find_git_gemspec(bare_repo, revision, gem_name, glob: nil)
|
|
556
|
+
gemspec_paths = gemspec_paths_in_git_revision(bare_repo, revision)
|
|
557
|
+
return nil if gemspec_paths.empty?
|
|
558
|
+
|
|
559
|
+
path = gemspec_paths[gem_name.to_s]
|
|
560
|
+
if path.nil? && glob
|
|
561
|
+
glob_regex = git_glob_to_regex(glob)
|
|
562
|
+
path = gemspec_paths.values.find { |candidate| candidate.match?(glob_regex) }
|
|
563
|
+
end
|
|
564
|
+
path ||= gemspec_paths.values.first
|
|
565
|
+
return nil if path.nil?
|
|
566
|
+
|
|
567
|
+
load_git_gemspec(bare_repo, revision, path)
|
|
568
|
+
rescue StandardError
|
|
569
|
+
nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def build_git_path_gems_for_revision(bare_repo, revision, glob: nil, source_desc: nil)
|
|
573
|
+
gemspec_paths = gemspec_paths_in_git_revision(bare_repo, revision)
|
|
574
|
+
return {} if gemspec_paths.empty?
|
|
575
|
+
|
|
576
|
+
glob_regex = glob ? git_glob_to_regex(glob) : nil
|
|
577
|
+
data = {}
|
|
578
|
+
|
|
579
|
+
with_git_worktree(bare_repo, revision) do |worktree|
|
|
580
|
+
gemspec_paths.each_value do |path|
|
|
581
|
+
next if glob_regex && !path.match?(glob_regex)
|
|
582
|
+
|
|
583
|
+
gemspec = load_gemspec_from_worktree(worktree, path)
|
|
584
|
+
next unless gemspec
|
|
585
|
+
|
|
586
|
+
deps = gemspec.dependencies
|
|
587
|
+
.select { |d| d.type == :runtime }
|
|
588
|
+
.map do |d|
|
|
589
|
+
requirement_parts = d.requirement.requirements.map { |op, req_version| "#{op} #{req_version}" }
|
|
590
|
+
[d.name, requirement_parts]
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
data[gemspec.name] = {
|
|
594
|
+
version: gemspec.version.to_s,
|
|
595
|
+
dependencies: deps,
|
|
596
|
+
source: source_desc || "local",
|
|
597
|
+
}
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
data
|
|
602
|
+
rescue StandardError
|
|
603
|
+
{}
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def git_glob_to_regex(glob)
|
|
607
|
+
pattern = glob.to_s
|
|
608
|
+
escaped = Regexp.escape(pattern)
|
|
609
|
+
escaped = escaped.gsub("\\*\\*", ".*")
|
|
610
|
+
escaped = escaped.gsub("\\*", "[^/]*")
|
|
611
|
+
escaped = escaped.gsub("\\?", ".")
|
|
612
|
+
/\A#{escaped}\z/
|
|
613
|
+
end
|
|
614
|
+
|
|
377
615
|
def lockfile_current?(gemfile, lockfile)
|
|
378
616
|
return false unless lockfile
|
|
379
617
|
|
|
380
618
|
locked_names = Set.new(lockfile.specs.map { |s| s[:name] })
|
|
381
|
-
gemfile.dependencies.all?
|
|
619
|
+
gemfile.dependencies.all? do |dep|
|
|
620
|
+
next true unless dependency_relevant_for_local_platform?(dep)
|
|
621
|
+
|
|
622
|
+
locked_names.include?(dep.name)
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def lockfile_dependency_graph_valid?(lockfile)
|
|
627
|
+
return false unless lockfile
|
|
628
|
+
|
|
629
|
+
specs = Array(lockfile.specs)
|
|
630
|
+
return false if specs.empty?
|
|
631
|
+
|
|
632
|
+
by_name = Hash.new { |h, k| h[k] = [] }
|
|
633
|
+
specs.each { |spec| by_name[spec[:name].to_s] << spec }
|
|
634
|
+
|
|
635
|
+
specs.all? do |spec|
|
|
636
|
+
Array(spec[:dependencies]).all? do |dep|
|
|
637
|
+
dep_name = dep[:name].to_s
|
|
638
|
+
# Lockfiles generally do not include a concrete "bundler" spec
|
|
639
|
+
# entry even though many gemspecs declare a runtime dependency
|
|
640
|
+
# on bundler. Treat it as externally satisfied.
|
|
641
|
+
next true if dep_name == "bundler"
|
|
642
|
+
|
|
643
|
+
dep_reqs = Array(dep[:version_reqs])
|
|
644
|
+
req = Gem::Requirement.new(dep_reqs.empty? ? [">= 0"] : dep_reqs)
|
|
645
|
+
by_name[dep_name].any? { |candidate| req.satisfied_by?(Gem::Version.new(candidate[:version].to_s)) }
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
rescue StandardError
|
|
649
|
+
false
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def dependency_relevant_for_local_platform?(dependency)
|
|
653
|
+
platforms = Array(dependency.platforms).map(&:to_sym)
|
|
654
|
+
return true if platforms.empty?
|
|
655
|
+
|
|
656
|
+
platforms.any? { |platform| gemfile_platform_matches_local?(platform) }
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def gemfile_platform_matches_local?(platform)
|
|
660
|
+
case platform
|
|
661
|
+
when :ruby
|
|
662
|
+
true
|
|
663
|
+
when :mri
|
|
664
|
+
RUBY_ENGINE == "ruby"
|
|
665
|
+
when :jruby
|
|
666
|
+
RUBY_ENGINE == "jruby"
|
|
667
|
+
when :truffleruby
|
|
668
|
+
RUBY_ENGINE == "truffleruby"
|
|
669
|
+
when :rbx
|
|
670
|
+
RUBY_ENGINE == "rbx"
|
|
671
|
+
when :windows, :mswin, :mswin64, :mingw, :x64_mingw, :x86_mingw, :x64_mingw_ucrt
|
|
672
|
+
Platform.windows?
|
|
673
|
+
when :linux
|
|
674
|
+
Platform.linux?
|
|
675
|
+
when :darwin, :macos
|
|
676
|
+
Platform.macos?
|
|
677
|
+
else
|
|
678
|
+
platform_name = platform.to_s.tr("_", "-")
|
|
679
|
+
spec_platform = Gem::Platform.new(platform_name)
|
|
680
|
+
spec_platform === Platform.local_platform
|
|
681
|
+
end
|
|
682
|
+
rescue StandardError
|
|
683
|
+
false
|
|
382
684
|
end
|
|
383
685
|
|
|
384
686
|
def lockfile_to_resolved(lockfile)
|
|
@@ -414,6 +716,106 @@ module Scint
|
|
|
414
716
|
apply_locked_platform_preferences(resolved)
|
|
415
717
|
end
|
|
416
718
|
|
|
719
|
+
def lockfile_git_source_mapping_valid?(lockfile, cache)
|
|
720
|
+
return true unless lockfile && cache
|
|
721
|
+
|
|
722
|
+
git_specs = Array(lockfile.specs).select { |s| s[:source].is_a?(Source::Git) }
|
|
723
|
+
return true if git_specs.empty?
|
|
724
|
+
|
|
725
|
+
by_source = git_specs.group_by { |s| s[:source] }
|
|
726
|
+
by_source.each do |source, specs|
|
|
727
|
+
uri, revision = git_source_ref(source)
|
|
728
|
+
bare_repo = cache.git_path(uri)
|
|
729
|
+
# Do not invalidate an otherwise-usable lockfile just because this
|
|
730
|
+
# git source has not been cached yet in the current machine/session.
|
|
731
|
+
next unless Dir.exist?(bare_repo)
|
|
732
|
+
|
|
733
|
+
resolved_revision = begin
|
|
734
|
+
resolve_git_revision(bare_repo, revision)
|
|
735
|
+
rescue InstallError
|
|
736
|
+
nil
|
|
737
|
+
end
|
|
738
|
+
return false unless resolved_revision
|
|
739
|
+
|
|
740
|
+
gemspec_paths = gemspec_paths_in_git_revision(bare_repo, resolved_revision)
|
|
741
|
+
gemspec_names = gemspec_paths.keys.to_set
|
|
742
|
+
return false if gemspec_names.empty?
|
|
743
|
+
|
|
744
|
+
specs.each do |spec|
|
|
745
|
+
return false unless gemspec_names.include?(spec[:name].to_s)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
true
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def gemspec_paths_in_git_revision(bare_repo, revision)
|
|
753
|
+
out, _err, status = git_capture3(
|
|
754
|
+
"--git-dir", bare_repo,
|
|
755
|
+
"ls-tree",
|
|
756
|
+
"-r",
|
|
757
|
+
"--name-only",
|
|
758
|
+
revision,
|
|
759
|
+
)
|
|
760
|
+
return {} unless status.success?
|
|
761
|
+
|
|
762
|
+
paths = {}
|
|
763
|
+
out.each_line do |line|
|
|
764
|
+
path = line.strip
|
|
765
|
+
next unless path.end_with?(".gemspec")
|
|
766
|
+
name = File.basename(path, ".gemspec")
|
|
767
|
+
paths[name] ||= path
|
|
768
|
+
end
|
|
769
|
+
paths
|
|
770
|
+
rescue StandardError
|
|
771
|
+
{}
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def runtime_dependencies_for_git_gemspec(bare_repo, revision, gemspec_path)
|
|
775
|
+
spec = load_git_gemspec(bare_repo, revision, gemspec_path)
|
|
776
|
+
return nil unless spec
|
|
777
|
+
|
|
778
|
+
spec.dependencies.select { |dep| dep.type == :runtime }
|
|
779
|
+
rescue StandardError
|
|
780
|
+
nil
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def load_git_gemspec(bare_repo, revision, gemspec_path)
|
|
784
|
+
return nil if gemspec_path.to_s.empty?
|
|
785
|
+
|
|
786
|
+
with_git_worktree(bare_repo, revision) do |worktree|
|
|
787
|
+
load_gemspec_from_worktree(worktree, gemspec_path)
|
|
788
|
+
end
|
|
789
|
+
rescue StandardError
|
|
790
|
+
nil
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def with_git_worktree(bare_repo, revision)
|
|
794
|
+
worktree = Dir.mktmpdir("scint-gemspec")
|
|
795
|
+
_out, _err, status = git_capture3(
|
|
796
|
+
"--git-dir", bare_repo,
|
|
797
|
+
"--work-tree", worktree,
|
|
798
|
+
"checkout",
|
|
799
|
+
"--force",
|
|
800
|
+
revision,
|
|
801
|
+
)
|
|
802
|
+
return nil unless status.success?
|
|
803
|
+
|
|
804
|
+
File.write(File.join(worktree, ".git"), "gitdir: #{bare_repo}\n")
|
|
805
|
+
yield worktree if block_given?
|
|
806
|
+
ensure
|
|
807
|
+
FileUtils.rm_rf(worktree) if worktree && !worktree.empty?
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def load_gemspec_from_worktree(worktree, gemspec_path)
|
|
811
|
+
absolute_gemspec = File.join(worktree, gemspec_path)
|
|
812
|
+
return nil unless File.exist?(absolute_gemspec)
|
|
813
|
+
|
|
814
|
+
SpecUtils.load_gemspec(absolute_gemspec, isolate: true)
|
|
815
|
+
rescue SystemExit, StandardError
|
|
816
|
+
nil
|
|
817
|
+
end
|
|
818
|
+
|
|
417
819
|
# Preference: exact platform match > compatible match > ruby > first.
|
|
418
820
|
def pick_best_platform_spec(specs, local_plat)
|
|
419
821
|
return specs.first if specs.size == 1
|
|
@@ -451,7 +853,7 @@ module Scint
|
|
|
451
853
|
return resolved_specs if preferred.empty?
|
|
452
854
|
|
|
453
855
|
resolved_specs.each do |spec|
|
|
454
|
-
key =
|
|
856
|
+
key = SpecUtils.full_name_for(spec.name, spec.version)
|
|
455
857
|
platform = preferred[key]
|
|
456
858
|
next if platform.nil? || platform.empty?
|
|
457
859
|
|
|
@@ -478,7 +880,7 @@ module Scint
|
|
|
478
880
|
preferred = preferred.to_s
|
|
479
881
|
next if preferred.empty? || preferred == spec.platform.to_s
|
|
480
882
|
|
|
481
|
-
out[
|
|
883
|
+
out[SpecUtils.full_name_for(spec.name, spec.version)] = preferred
|
|
482
884
|
end
|
|
483
885
|
rescue StandardError
|
|
484
886
|
next
|
|
@@ -496,7 +898,7 @@ module Scint
|
|
|
496
898
|
spec = entry.spec
|
|
497
899
|
source = spec.source
|
|
498
900
|
if git_source?(source)
|
|
499
|
-
|
|
901
|
+
ensure_git_repo_for_spec(spec, cache, fetch: true)
|
|
500
902
|
return
|
|
501
903
|
end
|
|
502
904
|
source_uri = source.to_s
|
|
@@ -504,7 +906,7 @@ module Scint
|
|
|
504
906
|
# Path gems are not downloaded from a remote
|
|
505
907
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
506
908
|
|
|
507
|
-
full_name =
|
|
909
|
+
full_name = SpecUtils.full_name(spec)
|
|
508
910
|
gem_filename = "#{full_name}.gem"
|
|
509
911
|
source_uri = source_uri.chomp("/")
|
|
510
912
|
download_uri = "#{source_uri}/gems/#{gem_filename}"
|
|
@@ -513,9 +915,20 @@ module Scint
|
|
|
513
915
|
FS.mkdir_p(File.dirname(dest_path))
|
|
514
916
|
|
|
515
917
|
unless File.exist?(dest_path)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
918
|
+
downloader_pool.download(download_uri, dest_path)
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def downloader_pool
|
|
923
|
+
@download_pool_lock.synchronize do
|
|
924
|
+
@download_pool ||= Downloader::Pool.new(credentials: @credentials)
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
def close_download_pool
|
|
929
|
+
@download_pool_lock.synchronize do
|
|
930
|
+
@download_pool&.close
|
|
931
|
+
@download_pool = nil
|
|
519
932
|
end
|
|
520
933
|
end
|
|
521
934
|
|
|
@@ -523,20 +936,37 @@ module Scint
|
|
|
523
936
|
spec = entry.spec
|
|
524
937
|
source_uri = spec.source.to_s
|
|
525
938
|
|
|
526
|
-
# Git
|
|
527
|
-
|
|
939
|
+
# Git gems are extracted from the cached checkout; path gems are
|
|
940
|
+
# linked directly from local source.
|
|
941
|
+
if git_source?(spec.source)
|
|
942
|
+
assemble_git_spec(entry, cache, fetch: false)
|
|
943
|
+
return
|
|
944
|
+
end
|
|
528
945
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
529
946
|
|
|
530
|
-
|
|
531
|
-
return if Dir.exist?(extracted)
|
|
947
|
+
return if Cache::Validity.cached_valid?(spec, cache)
|
|
532
948
|
|
|
533
949
|
dest_path = cache.inbound_path(spec)
|
|
534
950
|
raise InstallError, "Missing cached gem file for #{spec.name}: #{dest_path}" unless File.exist?(dest_path)
|
|
535
951
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
952
|
+
assembling = cache.assembling_path(spec)
|
|
953
|
+
tmp = "#{assembling}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
954
|
+
begin
|
|
955
|
+
FileUtils.rm_rf(assembling)
|
|
956
|
+
FileUtils.rm_rf(tmp)
|
|
957
|
+
FS.mkdir_p(File.dirname(assembling))
|
|
958
|
+
|
|
959
|
+
pkg = GemPkg::Package.new
|
|
960
|
+
result = pkg.extract(dest_path, tmp)
|
|
961
|
+
FS.atomic_move(tmp, assembling)
|
|
962
|
+
cache_gemspec(spec, result[:gemspec], cache)
|
|
963
|
+
|
|
964
|
+
unless Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
965
|
+
promote_assembled_gem(spec, cache, assembling, result[:gemspec], extensions: false)
|
|
966
|
+
end
|
|
967
|
+
ensure
|
|
968
|
+
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
969
|
+
end
|
|
540
970
|
end
|
|
541
971
|
|
|
542
972
|
def git_source?(source)
|
|
@@ -546,50 +976,135 @@ module Scint
|
|
|
546
976
|
source_str.end_with?(".git") || source_str.include?(".git/")
|
|
547
977
|
end
|
|
548
978
|
|
|
979
|
+
def path_source?(source)
|
|
980
|
+
return true if source.is_a?(Source::Path)
|
|
981
|
+
|
|
982
|
+
source_str =
|
|
983
|
+
if source.respond_to?(:path)
|
|
984
|
+
source.path.to_s
|
|
985
|
+
else
|
|
986
|
+
source.to_s
|
|
987
|
+
end
|
|
988
|
+
return false if source_str.empty?
|
|
989
|
+
return false if source_str.start_with?("http://", "https://")
|
|
990
|
+
return false if git_source?(source)
|
|
991
|
+
|
|
992
|
+
source_str.start_with?("/", ".", "~")
|
|
993
|
+
end
|
|
994
|
+
|
|
549
995
|
def prepare_git_source(entry, cache)
|
|
996
|
+
# Legacy helper used by tests/callers that expect git download+extract
|
|
997
|
+
# in a single step.
|
|
998
|
+
assemble_git_spec(entry, cache, fetch: true)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
def ensure_git_repo_for_spec(spec, cache, fetch:)
|
|
1002
|
+
source = spec.source
|
|
1003
|
+
uri, _revision = git_source_ref(source)
|
|
1004
|
+
bare_repo = cache.git_path(uri)
|
|
1005
|
+
|
|
1006
|
+
# Serialize all git operations per bare repo — git uses index.lock
|
|
1007
|
+
# and can't handle concurrent checkouts from the same repo.
|
|
1008
|
+
git_mutex_for(bare_repo).synchronize do
|
|
1009
|
+
if Dir.exist?(bare_repo)
|
|
1010
|
+
fetch_git_repo(bare_repo) if fetch
|
|
1011
|
+
else
|
|
1012
|
+
clone_git_repo(uri, bare_repo)
|
|
1013
|
+
fetch_git_repo(bare_repo)
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
bare_repo
|
|
1018
|
+
end
|
|
1019
|
+
|
|
1020
|
+
def assemble_git_spec(entry, cache, fetch: true)
|
|
550
1021
|
spec = entry.spec
|
|
1022
|
+
return if Cache::Validity.cached_valid?(spec, cache)
|
|
1023
|
+
|
|
551
1024
|
source = spec.source
|
|
552
1025
|
uri, revision = git_source_ref(source)
|
|
1026
|
+
submodules = git_source_submodules?(source)
|
|
553
1027
|
|
|
554
1028
|
bare_repo = cache.git_path(uri)
|
|
555
1029
|
|
|
556
1030
|
# Serialize all git operations per bare repo — git uses index.lock
|
|
557
1031
|
# and can't handle concurrent checkouts from the same repo.
|
|
558
1032
|
git_mutex_for(bare_repo).synchronize do
|
|
559
|
-
|
|
560
|
-
|
|
1033
|
+
tmp_checkout = nil
|
|
1034
|
+
tmp_assembled = nil
|
|
561
1035
|
|
|
562
|
-
|
|
1036
|
+
begin
|
|
1037
|
+
if Dir.exist?(bare_repo)
|
|
1038
|
+
fetch_git_repo(bare_repo) if fetch
|
|
1039
|
+
else
|
|
1040
|
+
clone_git_repo(uri, bare_repo)
|
|
1041
|
+
fetch_git_repo(bare_repo)
|
|
1042
|
+
end
|
|
563
1043
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1044
|
+
resolved_revision = resolve_git_revision(bare_repo, revision)
|
|
1045
|
+
assembling = cache.assembling_path(spec)
|
|
1046
|
+
tmp_checkout = "#{assembling}.checkout.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
1047
|
+
tmp_assembled = "#{assembling}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
1048
|
+
promoter = cache_promoter(cache)
|
|
1049
|
+
|
|
1050
|
+
FileUtils.rm_rf(assembling)
|
|
1051
|
+
FileUtils.rm_rf(tmp_checkout)
|
|
1052
|
+
FileUtils.rm_rf(tmp_assembled)
|
|
1053
|
+
FS.mkdir_p(File.dirname(assembling))
|
|
1054
|
+
|
|
1055
|
+
promoter.validate_within_root!(cache.root, assembling, label: "assembling")
|
|
1056
|
+
promoter.validate_within_root!(cache.root, tmp_checkout, label: "git-checkout")
|
|
1057
|
+
promoter.validate_within_root!(cache.root, tmp_assembled, label: "git-assembled")
|
|
1058
|
+
|
|
1059
|
+
if submodules
|
|
1060
|
+
checkout_git_tree_with_submodules(
|
|
1061
|
+
bare_repo,
|
|
1062
|
+
tmp_checkout,
|
|
1063
|
+
resolved_revision,
|
|
1064
|
+
spec,
|
|
1065
|
+
uri,
|
|
1066
|
+
)
|
|
1067
|
+
else
|
|
1068
|
+
checkout_git_tree(
|
|
1069
|
+
bare_repo,
|
|
1070
|
+
tmp_checkout,
|
|
1071
|
+
resolved_revision,
|
|
1072
|
+
spec,
|
|
1073
|
+
uri,
|
|
1074
|
+
)
|
|
1075
|
+
end
|
|
569
1076
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
)
|
|
584
|
-
|
|
585
|
-
|
|
1077
|
+
# Remove .git artifacts from checkout so assembled output is
|
|
1078
|
+
# deterministic and contains no git internals.
|
|
1079
|
+
Dir.glob(File.join(tmp_checkout, "**", ".git"), File::FNM_DOTMATCH).each do |path|
|
|
1080
|
+
FileUtils.rm_rf(path)
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
gem_root = resolve_git_gem_subdir(tmp_checkout, spec)
|
|
1084
|
+
gem_rel = git_relative_root(tmp_checkout, gem_root)
|
|
1085
|
+
dest_root = tmp_assembled
|
|
1086
|
+
dest_path = gem_rel.empty? ? dest_root : File.join(dest_root, gem_rel)
|
|
1087
|
+
|
|
1088
|
+
promoter.validate_within_root!(cache.root, dest_path, label: "git-dest")
|
|
1089
|
+
|
|
1090
|
+
FS.clone_tree(gem_root, dest_path)
|
|
1091
|
+
copy_gemspec_root_files(tmp_checkout, gem_root, dest_root, spec)
|
|
1092
|
+
FS.atomic_move(tmp_assembled, assembling)
|
|
1093
|
+
|
|
1094
|
+
gem_subdir = begin
|
|
1095
|
+
resolve_git_gem_subdir(assembling, spec)
|
|
1096
|
+
rescue InstallError
|
|
1097
|
+
assembling
|
|
586
1098
|
end
|
|
1099
|
+
gemspec = read_gemspec_from_extracted(gem_subdir, spec)
|
|
1100
|
+
cache_gemspec(spec, gemspec, cache) if gemspec
|
|
587
1101
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1102
|
+
unless Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
1103
|
+
promote_assembled_gem(spec, cache, assembling, gemspec, extensions: false)
|
|
1104
|
+
end
|
|
591
1105
|
ensure
|
|
592
|
-
FileUtils.rm_rf(
|
|
1106
|
+
FileUtils.rm_rf(tmp_checkout) if tmp_checkout && File.exist?(tmp_checkout)
|
|
1107
|
+
FileUtils.rm_rf(tmp_assembled) if tmp_assembled && File.exist?(tmp_assembled)
|
|
593
1108
|
end
|
|
594
1109
|
end
|
|
595
1110
|
end
|
|
@@ -603,6 +1118,117 @@ module Scint
|
|
|
603
1118
|
[source.to_s, "HEAD"]
|
|
604
1119
|
end
|
|
605
1120
|
|
|
1121
|
+
def git_source_submodules?(source)
|
|
1122
|
+
source.respond_to?(:submodules) && !!source.submodules
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def copy_gemspec_root_files(repo_root, gem_root, dest_root, spec)
|
|
1126
|
+
repo_root = File.expand_path(repo_root.to_s)
|
|
1127
|
+
gem_root = File.expand_path(gem_root.to_s)
|
|
1128
|
+
return if repo_root == gem_root
|
|
1129
|
+
|
|
1130
|
+
gemspec_path = git_gemspec_path_for_root(gem_root, spec)
|
|
1131
|
+
return unless gemspec_path && File.exist?(gemspec_path)
|
|
1132
|
+
|
|
1133
|
+
content = File.read(gemspec_path) rescue nil
|
|
1134
|
+
return unless content
|
|
1135
|
+
|
|
1136
|
+
root_files = git_root_files_from_gemspec(content)
|
|
1137
|
+
root_files.each do |file|
|
|
1138
|
+
source = File.join(repo_root, file)
|
|
1139
|
+
next unless File.file?(source)
|
|
1140
|
+
|
|
1141
|
+
dest = File.join(dest_root, file)
|
|
1142
|
+
next if File.exist?(dest)
|
|
1143
|
+
|
|
1144
|
+
FS.clonefile(source, dest)
|
|
1145
|
+
end
|
|
1146
|
+
end
|
|
1147
|
+
|
|
1148
|
+
def git_gemspec_path_for_root(gem_root, spec)
|
|
1149
|
+
if spec && spec.respond_to?(:name)
|
|
1150
|
+
candidate = File.join(gem_root, "#{spec.name}.gemspec")
|
|
1151
|
+
return candidate if File.exist?(candidate)
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
Dir.glob(File.join(gem_root, "*.gemspec")).first
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
def git_root_files_from_gemspec(content)
|
|
1158
|
+
files = ["RAILS_VERSION", "VERSION"]
|
|
1159
|
+
files.select { |file| content.include?(file) }
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
def git_relative_root(repo_root, gem_root)
|
|
1163
|
+
repo_root = File.expand_path(repo_root.to_s)
|
|
1164
|
+
gem_root = File.expand_path(gem_root.to_s)
|
|
1165
|
+
return "" if repo_root == gem_root
|
|
1166
|
+
|
|
1167
|
+
if gem_root.start_with?("#{repo_root}/")
|
|
1168
|
+
return gem_root.delete_prefix("#{repo_root}/")
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
File.basename(gem_root)
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
def checkout_git_tree(bare_repo, destination, resolved_revision, spec, uri)
|
|
1175
|
+
FileUtils.mkdir_p(destination)
|
|
1176
|
+
_out, err, status = git_capture3(
|
|
1177
|
+
"--git-dir", bare_repo,
|
|
1178
|
+
"--work-tree", destination,
|
|
1179
|
+
"checkout",
|
|
1180
|
+
"-f",
|
|
1181
|
+
resolved_revision,
|
|
1182
|
+
"--",
|
|
1183
|
+
".",
|
|
1184
|
+
)
|
|
1185
|
+
unless status.success?
|
|
1186
|
+
raise InstallError, "Git checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
1187
|
+
end
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
def checkout_git_tree_with_submodules(bare_repo, destination, resolved_revision, spec, uri)
|
|
1191
|
+
worktree = "#{destination}.worktree"
|
|
1192
|
+
FileUtils.rm_rf(worktree)
|
|
1193
|
+
|
|
1194
|
+
_out, err, status = git_capture3(
|
|
1195
|
+
"--git-dir", bare_repo,
|
|
1196
|
+
"worktree",
|
|
1197
|
+
"add",
|
|
1198
|
+
"--detach",
|
|
1199
|
+
"--force",
|
|
1200
|
+
worktree,
|
|
1201
|
+
resolved_revision,
|
|
1202
|
+
)
|
|
1203
|
+
unless status.success?
|
|
1204
|
+
raise InstallError, "Git worktree checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
1205
|
+
end
|
|
1206
|
+
|
|
1207
|
+
begin
|
|
1208
|
+
_sub_out, sub_err, sub_status = git_capture3(
|
|
1209
|
+
"-C", worktree,
|
|
1210
|
+
"-c", "protocol.file.allow=always",
|
|
1211
|
+
"submodule",
|
|
1212
|
+
"update",
|
|
1213
|
+
"--init",
|
|
1214
|
+
"--recursive",
|
|
1215
|
+
)
|
|
1216
|
+
unless sub_status.success?
|
|
1217
|
+
raise InstallError, "Git submodule update failed for #{spec.name} (#{uri}@#{resolved_revision}): #{sub_err.to_s.strip}"
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
FS.clone_tree(worktree, destination)
|
|
1221
|
+
|
|
1222
|
+
# Keep cache/extracted trees deterministic and detached from git internals.
|
|
1223
|
+
Dir.glob(File.join(destination, "**", ".git"), File::FNM_DOTMATCH).each do |path|
|
|
1224
|
+
FileUtils.rm_rf(path)
|
|
1225
|
+
end
|
|
1226
|
+
ensure
|
|
1227
|
+
git_capture3("--git-dir", bare_repo, "worktree", "remove", "--force", worktree)
|
|
1228
|
+
FileUtils.rm_rf(worktree)
|
|
1229
|
+
end
|
|
1230
|
+
end
|
|
1231
|
+
|
|
606
1232
|
def git_mutex_for(repo_path)
|
|
607
1233
|
@git_mutexes_lock ||= Thread::Mutex.new
|
|
608
1234
|
@git_mutexes_lock.synchronize do
|
|
@@ -645,30 +1271,59 @@ module Scint
|
|
|
645
1271
|
Open3.capture3("git", "-c", "core.fsmonitor=false", *args)
|
|
646
1272
|
end
|
|
647
1273
|
|
|
648
|
-
def
|
|
649
|
-
|
|
1274
|
+
def compile_slots_for(worker_count)
|
|
1275
|
+
# Scale compile concurrency with available CPUs.
|
|
1276
|
+
# Most native extensions have 1-3 source files and don't benefit from
|
|
1277
|
+
# high make -j; running more concurrent builds is more effective.
|
|
1278
|
+
# Each slot gets cpu_count/slots make jobs (see adaptive_make_jobs).
|
|
1279
|
+
workers = [worker_count.to_i, 1].max
|
|
1280
|
+
override = positive_integer_env("SCINT_COMPILE_CONCURRENCY")
|
|
1281
|
+
return [override, workers].min if override
|
|
1282
|
+
|
|
1283
|
+
cpus = Platform.cpu_count
|
|
1284
|
+
# Aim for 8 make-jobs per slot → slots = cpus / 8, clamped.
|
|
1285
|
+
slots = cpus / 8
|
|
1286
|
+
slots = [[slots, 2].max, workers].min
|
|
1287
|
+
slots
|
|
650
1288
|
end
|
|
651
1289
|
|
|
652
|
-
def
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
1
|
|
1290
|
+
def git_slots_for(worker_count)
|
|
1291
|
+
workers = [worker_count.to_i, 1].max
|
|
1292
|
+
override = positive_integer_env("SCINT_GIT_CONCURRENCY")
|
|
1293
|
+
slots = override || workers
|
|
1294
|
+
[[slots, workers].min, 1].max
|
|
657
1295
|
end
|
|
658
1296
|
|
|
659
|
-
def install_task_limits(worker_count, compile_slots)
|
|
1297
|
+
def install_task_limits(worker_count, compile_slots, git_slots = worker_count)
|
|
660
1298
|
# Leave headroom for compile and binstub lanes so link/download
|
|
661
1299
|
# throughput cannot fully starve them.
|
|
662
|
-
|
|
1300
|
+
workers = [worker_count.to_i, 1].max
|
|
1301
|
+
io_cpu_limit = [workers - compile_slots - 1, 1].max
|
|
1302
|
+
# Keep download in-flight set bounded so fail-fast exits quickly on
|
|
1303
|
+
# auth/source errors instead of queueing a large burst.
|
|
1304
|
+
download_limit = [io_cpu_limit, 8].min
|
|
1305
|
+
git_limit = [[git_slots.to_i, 1].max, workers].min
|
|
663
1306
|
{
|
|
664
|
-
download:
|
|
1307
|
+
download: download_limit,
|
|
665
1308
|
extract: io_cpu_limit,
|
|
666
1309
|
link: io_cpu_limit,
|
|
1310
|
+
git_clone: git_limit,
|
|
667
1311
|
build_ext: compile_slots,
|
|
668
1312
|
binstub: 1,
|
|
669
1313
|
}
|
|
670
1314
|
end
|
|
671
1315
|
|
|
1316
|
+
def positive_integer_env(key)
|
|
1317
|
+
raw = ENV[key]
|
|
1318
|
+
return nil if raw.nil? || raw.empty?
|
|
1319
|
+
|
|
1320
|
+
value = Integer(raw, exception: false)
|
|
1321
|
+
return nil unless value
|
|
1322
|
+
return nil if value <= 0
|
|
1323
|
+
|
|
1324
|
+
value
|
|
1325
|
+
end
|
|
1326
|
+
|
|
672
1327
|
def display_bundle_path(path)
|
|
673
1328
|
return path if path.start_with?("/", "./", "../")
|
|
674
1329
|
|
|
@@ -770,7 +1425,7 @@ module Scint
|
|
|
770
1425
|
end
|
|
771
1426
|
|
|
772
1427
|
def spec_key(spec)
|
|
773
|
-
|
|
1428
|
+
SpecUtils.full_key(spec)
|
|
774
1429
|
end
|
|
775
1430
|
|
|
776
1431
|
def dependency_link_job_ids(spec, link_job_by_name)
|
|
@@ -805,11 +1460,36 @@ module Scint
|
|
|
805
1460
|
def extracted_path_for_entry(entry, cache)
|
|
806
1461
|
source_str = entry.spec.source.to_s
|
|
807
1462
|
if source_str.start_with?("/") && Dir.exist?(source_str)
|
|
808
|
-
|
|
1463
|
+
begin
|
|
1464
|
+
if path_source?(entry.spec.source)
|
|
1465
|
+
resolve_path_gem_subdir(source_str, entry.spec)
|
|
1466
|
+
else
|
|
1467
|
+
resolve_git_gem_subdir(source_str, entry.spec)
|
|
1468
|
+
end
|
|
1469
|
+
rescue InstallError
|
|
1470
|
+
source_str
|
|
1471
|
+
end
|
|
809
1472
|
else
|
|
810
|
-
|
|
811
|
-
|
|
1473
|
+
cached_dir = cache.cached_path(entry.spec)
|
|
1474
|
+
assembling = cache.assembling_path(entry.spec)
|
|
1475
|
+
base = if entry.cached_path
|
|
1476
|
+
entry.cached_path
|
|
1477
|
+
elsif Cache::Validity.cached_valid?(entry.spec, cache)
|
|
1478
|
+
cached_dir
|
|
1479
|
+
elsif Dir.exist?(assembling)
|
|
1480
|
+
assembling
|
|
1481
|
+
else
|
|
1482
|
+
nil
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
if git_source?(entry.spec.source) && base && Dir.exist?(base)
|
|
812
1486
|
resolve_git_gem_subdir(base, entry.spec)
|
|
1487
|
+
elsif path_source?(entry.spec.source) && base && Dir.exist?(base)
|
|
1488
|
+
begin
|
|
1489
|
+
resolve_path_gem_subdir(base, entry.spec)
|
|
1490
|
+
rescue InstallError
|
|
1491
|
+
base
|
|
1492
|
+
end
|
|
813
1493
|
else
|
|
814
1494
|
base
|
|
815
1495
|
end
|
|
@@ -826,8 +1506,40 @@ module Scint
|
|
|
826
1506
|
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
827
1507
|
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
828
1508
|
end
|
|
1509
|
+
# Compatibility fallback for monorepos whose gem layout does not match
|
|
1510
|
+
# the lockfile glob exactly.
|
|
1511
|
+
Dir.glob(File.join(repo_root, "**", "*.gemspec")).each do |path|
|
|
1512
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
source_uri = source.respond_to?(:uri) ? source.uri : source.to_s
|
|
1516
|
+
raise InstallError,
|
|
1517
|
+
"Git source #{source_uri} does not contain #{name}.gemspec (glob: #{glob.inspect}); lockfile source mapping may be stale"
|
|
1518
|
+
end
|
|
1519
|
+
|
|
1520
|
+
# For path monorepo sources, map gem name to its gemspec subdirectory.
|
|
1521
|
+
def resolve_path_gem_subdir(repo_root, spec)
|
|
1522
|
+
name = spec.name
|
|
1523
|
+
return repo_root if File.exist?(File.join(repo_root, "#{name}.gemspec"))
|
|
1524
|
+
|
|
1525
|
+
source = spec.source
|
|
1526
|
+
glob = source.respond_to?(:glob) ? source.glob : Source::Path::DEFAULT_GLOB
|
|
1527
|
+
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
1528
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
1529
|
+
end
|
|
1530
|
+
Dir.glob(File.join(repo_root, "**", "*.gemspec")).each do |path|
|
|
1531
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
1532
|
+
end
|
|
829
1533
|
|
|
830
|
-
|
|
1534
|
+
source_uri =
|
|
1535
|
+
if source.respond_to?(:path)
|
|
1536
|
+
source.path
|
|
1537
|
+
elsif source.respond_to?(:uri)
|
|
1538
|
+
source.uri
|
|
1539
|
+
else
|
|
1540
|
+
source.to_s
|
|
1541
|
+
end
|
|
1542
|
+
raise InstallError, "Path source #{source_uri} does not contain #{name}.gemspec (glob: #{glob.inspect})"
|
|
831
1543
|
end
|
|
832
1544
|
|
|
833
1545
|
def link_gem_files(entry, cache, bundle_path)
|
|
@@ -843,19 +1555,18 @@ module Scint
|
|
|
843
1555
|
from_cache: true,
|
|
844
1556
|
)
|
|
845
1557
|
Installer::Linker.link_files(prepared, bundle_path)
|
|
846
|
-
# If this gem has a cached native build, materialize it during link.
|
|
847
|
-
# This lets reinstalling into a fresh .bundle skip build_ext entirely.
|
|
848
|
-
Installer::ExtensionBuilder.link_cached_build(prepared, bundle_path, cache)
|
|
849
1558
|
end
|
|
850
1559
|
|
|
851
1560
|
def build_extensions(entry, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
1561
|
+
spec = entry.spec
|
|
852
1562
|
extracted = extracted_path_for_entry(entry, cache)
|
|
853
|
-
gemspec = load_gemspec(extracted,
|
|
1563
|
+
gemspec = load_gemspec(extracted, spec, cache)
|
|
1564
|
+
promote_after_build = assembling_path?(extracted, cache)
|
|
854
1565
|
|
|
855
|
-
sync_build_env_dependencies(
|
|
1566
|
+
sync_build_env_dependencies(spec, bundle_path, cache)
|
|
856
1567
|
|
|
857
1568
|
prepared = PreparedGem.new(
|
|
858
|
-
spec:
|
|
1569
|
+
spec: spec,
|
|
859
1570
|
extracted_path: extracted,
|
|
860
1571
|
gemspec: gemspec,
|
|
861
1572
|
from_cache: true,
|
|
@@ -866,8 +1577,24 @@ module Scint
|
|
|
866
1577
|
bundle_path,
|
|
867
1578
|
cache,
|
|
868
1579
|
compile_slots: compile_slots,
|
|
869
|
-
output_tail: ->(lines) { progress&.on_build_tail(
|
|
1580
|
+
output_tail: ->(lines) { progress&.on_build_tail(spec.name, lines) },
|
|
870
1581
|
)
|
|
1582
|
+
|
|
1583
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
1584
|
+
bundle_gem_dir = File.join(ruby_dir, "gems", SpecUtils.full_name(spec))
|
|
1585
|
+
if Dir.exist?(bundle_gem_dir)
|
|
1586
|
+
Installer::ExtensionBuilder.sync_extensions_into_gem(extracted, bundle_gem_dir)
|
|
1587
|
+
File.write(File.join(bundle_gem_dir, Installer::ExtensionBuilder::BUILD_MARKER), "")
|
|
1588
|
+
end
|
|
1589
|
+
|
|
1590
|
+
return unless promote_after_build
|
|
1591
|
+
|
|
1592
|
+
promote_assembled_gem(spec, cache, extracted, gemspec, extensions: true)
|
|
1593
|
+
rescue StandardError
|
|
1594
|
+
if promote_after_build && extracted && Dir.exist?(extracted)
|
|
1595
|
+
FileUtils.rm_rf(extracted)
|
|
1596
|
+
end
|
|
1597
|
+
raise
|
|
871
1598
|
end
|
|
872
1599
|
|
|
873
1600
|
def sync_build_env_dependencies(spec, bundle_path, cache)
|
|
@@ -882,7 +1609,7 @@ module Scint
|
|
|
882
1609
|
dep_names.uniq!
|
|
883
1610
|
return if dep_names.empty?
|
|
884
1611
|
|
|
885
|
-
source_ruby_dir =
|
|
1612
|
+
source_ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
886
1613
|
target_ruby_dir = cache.install_ruby_dir
|
|
887
1614
|
|
|
888
1615
|
dep_names.each do |name|
|
|
@@ -922,8 +1649,21 @@ module Scint
|
|
|
922
1649
|
end
|
|
923
1650
|
|
|
924
1651
|
def load_gemspec(extracted_path, spec, cache)
|
|
1652
|
+
cache_key = "#{cache.full_name(spec)}@#{extracted_path}"
|
|
1653
|
+
cached_value = @gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] }
|
|
1654
|
+
return cached_value if cached_value
|
|
1655
|
+
|
|
925
1656
|
cached = load_cached_gemspec(spec, cache, extracted_path)
|
|
926
|
-
|
|
1657
|
+
if cached
|
|
1658
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = cached }
|
|
1659
|
+
return cached
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
direct = read_gemspec_from_extracted(extracted_path, spec)
|
|
1663
|
+
if direct
|
|
1664
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = direct }
|
|
1665
|
+
return direct
|
|
1666
|
+
end
|
|
927
1667
|
|
|
928
1668
|
inbound = cache.inbound_path(spec)
|
|
929
1669
|
return nil unless File.exist?(inbound)
|
|
@@ -931,30 +1671,97 @@ module Scint
|
|
|
931
1671
|
begin
|
|
932
1672
|
metadata = GemPkg::Package.new.read_metadata(inbound)
|
|
933
1673
|
cache_gemspec(spec, metadata, cache)
|
|
1674
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = metadata }
|
|
934
1675
|
metadata
|
|
935
1676
|
rescue StandardError
|
|
936
1677
|
nil
|
|
937
1678
|
end
|
|
938
1679
|
end
|
|
939
1680
|
|
|
1681
|
+
def read_gemspec_from_extracted(extracted_dir, spec)
|
|
1682
|
+
return nil unless extracted_dir && Dir.exist?(extracted_dir)
|
|
1683
|
+
|
|
1684
|
+
pattern = File.join(extracted_dir, "*.gemspec")
|
|
1685
|
+
candidates = Dir.glob(pattern)
|
|
1686
|
+
return nil if candidates.empty?
|
|
1687
|
+
|
|
1688
|
+
load_gemspec_file(candidates.first, spec)
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
# Load a .gemspec file, temporarily injecting VERSION env var for gems
|
|
1692
|
+
# like kgio/unicorn that use `ENV["VERSION"] or abort` in their gemspec.
|
|
1693
|
+
def load_gemspec_file(path, spec = nil)
|
|
1694
|
+
version = spec.respond_to?(:version) ? spec.version.to_s : nil
|
|
1695
|
+
old_version = ENV["VERSION"]
|
|
1696
|
+
begin
|
|
1697
|
+
ENV["VERSION"] = version if version && !ENV["VERSION"]
|
|
1698
|
+
SpecUtils.load_gemspec(path, isolate: true)
|
|
1699
|
+
rescue SystemExit, StandardError
|
|
1700
|
+
nil
|
|
1701
|
+
ensure
|
|
1702
|
+
ENV["VERSION"] = old_version
|
|
1703
|
+
end
|
|
1704
|
+
end
|
|
1705
|
+
|
|
1706
|
+
def bulk_prelink_gem_files(entries, cache, bundle_path)
|
|
1707
|
+
return if entries.length < 32
|
|
1708
|
+
|
|
1709
|
+
ruby_dir = File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
1710
|
+
gems_dir = File.join(ruby_dir, "gems")
|
|
1711
|
+
cache_abi_dir = cache.cached_abi_dir
|
|
1712
|
+
|
|
1713
|
+
gem_names = []
|
|
1714
|
+
entries.each do |entry|
|
|
1715
|
+
next unless entry.action == :link || entry.action == :build_ext
|
|
1716
|
+
|
|
1717
|
+
source_dir = entry.cached_path
|
|
1718
|
+
next unless source_dir
|
|
1719
|
+
|
|
1720
|
+
full_name = cache.full_name(entry.spec)
|
|
1721
|
+
next unless File.basename(source_dir) == full_name
|
|
1722
|
+
next unless Dir.exist?(source_dir)
|
|
1723
|
+
next if Dir.exist?(File.join(gems_dir, full_name))
|
|
1724
|
+
|
|
1725
|
+
gem_names << full_name
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
return if gem_names.empty?
|
|
1729
|
+
|
|
1730
|
+
if ENV["SCINT_TIMING"]
|
|
1731
|
+
$stderr.puts " [timing] prelink: #{gem_names.size} gems via linker"
|
|
1732
|
+
end
|
|
1733
|
+
|
|
1734
|
+
FS.bulk_link_gems(cache_abi_dir, gems_dir, gem_names)
|
|
1735
|
+
rescue StandardError => e
|
|
1736
|
+
$stderr.puts("bulk prelink warning: #{e.message}") if ENV["SCINT_DEBUG"]
|
|
1737
|
+
end
|
|
1738
|
+
|
|
940
1739
|
def load_cached_gemspec(spec, cache, extracted_path)
|
|
941
|
-
|
|
942
|
-
return nil unless File.exist?(path)
|
|
1740
|
+
paths = [cache.cached_spec_path(spec)]
|
|
943
1741
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1742
|
+
paths.each do |path|
|
|
1743
|
+
next unless File.exist?(path)
|
|
1744
|
+
|
|
1745
|
+
data = File.binread(path)
|
|
1746
|
+
gemspec = if data.start_with?("# -*- encoding")
|
|
1747
|
+
# Ruby format (to_ruby output) — most reliable, preserves require_paths
|
|
1748
|
+
Gem::Specification.load(path)
|
|
1749
|
+
elsif data.start_with?("---")
|
|
1750
|
+
data.force_encoding("UTF-8") if data.encoding != Encoding::UTF_8
|
|
951
1751
|
Gem::Specification.from_yaml(data)
|
|
1752
|
+
else
|
|
1753
|
+
begin
|
|
1754
|
+
Marshal.load(data)
|
|
1755
|
+
rescue StandardError
|
|
1756
|
+
data.force_encoding("UTF-8") if data.encoding != Encoding::UTF_8
|
|
1757
|
+
Gem::Specification.from_yaml(data)
|
|
1758
|
+
end
|
|
952
1759
|
end
|
|
1760
|
+
return gemspec if cached_gemspec_valid?(gemspec, extracted_path)
|
|
953
1761
|
end
|
|
954
|
-
return gemspec if cached_gemspec_valid?(gemspec, extracted_path)
|
|
955
1762
|
|
|
956
1763
|
nil
|
|
957
|
-
rescue StandardError
|
|
1764
|
+
rescue SystemExit, StandardError
|
|
958
1765
|
nil
|
|
959
1766
|
end
|
|
960
1767
|
|
|
@@ -988,87 +1795,490 @@ module Scint
|
|
|
988
1795
|
end
|
|
989
1796
|
|
|
990
1797
|
def cache_gemspec(spec, gemspec, cache)
|
|
991
|
-
path = cache.
|
|
992
|
-
|
|
1798
|
+
path = cache.cached_spec_path(spec)
|
|
1799
|
+
content = if gemspec.respond_to?(:to_ruby)
|
|
1800
|
+
gemspec.to_ruby
|
|
1801
|
+
else
|
|
1802
|
+
gemspec.to_yaml
|
|
1803
|
+
end
|
|
1804
|
+
FS.atomic_write(path, content)
|
|
993
1805
|
rescue StandardError
|
|
994
1806
|
# Non-fatal: we'll read metadata from .gem next time.
|
|
995
1807
|
end
|
|
996
1808
|
|
|
997
|
-
|
|
1809
|
+
def cache_promoter(cache)
|
|
1810
|
+
@cache_promoter ||= Installer::Promoter.new(root: cache.root)
|
|
1811
|
+
end
|
|
998
1812
|
|
|
999
|
-
def
|
|
1000
|
-
|
|
1813
|
+
def assembling_path?(path, cache)
|
|
1814
|
+
return false if path.nil? || path.empty?
|
|
1001
1815
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1816
|
+
root = File.expand_path(cache.assembling_dir)
|
|
1817
|
+
candidate = File.expand_path(path)
|
|
1818
|
+
candidate == root || candidate.start_with?("#{root}/")
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
def promote_assembled_gem(spec, cache, assembling_path, gemspec, extensions:)
|
|
1822
|
+
return unless assembling_path && Dir.exist?(assembling_path)
|
|
1823
|
+
|
|
1824
|
+
cached_dir = cache.cached_path(spec)
|
|
1825
|
+
promoter = cache_promoter(cache)
|
|
1826
|
+
lock_key = "#{Platform.abi_key}-#{cache.full_name(spec)}"
|
|
1827
|
+
|
|
1828
|
+
promoter.validate_within_root!(cache.root, assembling_path, label: "assembling")
|
|
1829
|
+
promoter.validate_within_root!(cache.root, cached_dir, label: "cached")
|
|
1830
|
+
|
|
1831
|
+
begin
|
|
1832
|
+
result = nil
|
|
1833
|
+
promoter.with_staging_dir(prefix: "cached") do |staging|
|
|
1834
|
+
FS.clone_tree(assembling_path, staging)
|
|
1835
|
+
manifest = build_cached_manifest(spec, cache, staging, extensions: extensions)
|
|
1836
|
+
Cache::Manifest.write_dotfiles(staging, manifest)
|
|
1837
|
+
spec_payload = gemspec ? gemspec.to_ruby : nil
|
|
1838
|
+
result = promoter.promote_tree(
|
|
1839
|
+
staging_path: staging,
|
|
1840
|
+
target_path: cached_dir,
|
|
1841
|
+
lock_key: lock_key,
|
|
1013
1842
|
)
|
|
1843
|
+
if result == :promoted
|
|
1844
|
+
write_cached_metadata(spec, cache, spec_payload, manifest)
|
|
1845
|
+
end
|
|
1846
|
+
FileUtils.rm_rf(assembling_path) if Dir.exist?(assembling_path)
|
|
1014
1847
|
end
|
|
1848
|
+
result
|
|
1849
|
+
rescue StandardError
|
|
1850
|
+
FileUtils.rm_rf(cached_dir) if Dir.exist?(cached_dir)
|
|
1851
|
+
raise
|
|
1015
1852
|
end
|
|
1853
|
+
end
|
|
1016
1854
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
.uniq
|
|
1855
|
+
def write_cached_metadata(spec, cache, spec_payload, manifest)
|
|
1856
|
+
spec_path = cache.cached_spec_path(spec)
|
|
1857
|
+
manifest_path = cache.cached_manifest_path(spec)
|
|
1858
|
+
FS.mkdir_p(File.dirname(spec_path))
|
|
1022
1859
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1860
|
+
FS.atomic_write(spec_path, spec_payload) if spec_payload
|
|
1861
|
+
Cache::Manifest.write(manifest_path, manifest)
|
|
1862
|
+
end
|
|
1863
|
+
|
|
1864
|
+
def build_cached_manifest(spec, cache, gem_dir, extensions:)
|
|
1865
|
+
Cache::Manifest.build(
|
|
1866
|
+
spec: spec,
|
|
1867
|
+
gem_dir: gem_dir,
|
|
1868
|
+
abi_key: Platform.abi_key,
|
|
1869
|
+
source: manifest_source_for(spec),
|
|
1870
|
+
extensions: extensions,
|
|
1871
|
+
)
|
|
1872
|
+
end
|
|
1030
1873
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1874
|
+
def manifest_source_for(spec)
|
|
1875
|
+
source = spec.source
|
|
1876
|
+
if source.is_a?(Source::Git)
|
|
1877
|
+
{
|
|
1878
|
+
"type" => "git",
|
|
1879
|
+
"uri" => source.uri.to_s,
|
|
1880
|
+
"revision" => source.revision || source.ref || source.branch || source.tag,
|
|
1881
|
+
}.compact
|
|
1882
|
+
elsif source.is_a?(Source::Path)
|
|
1883
|
+
{
|
|
1884
|
+
"type" => "path",
|
|
1885
|
+
"path" => File.expand_path(source.path.to_s),
|
|
1886
|
+
"uri" => source.path.to_s,
|
|
1887
|
+
}
|
|
1888
|
+
else
|
|
1889
|
+
source_str = source.to_s
|
|
1890
|
+
if source_str.start_with?("http://", "https://")
|
|
1891
|
+
{ "type" => "rubygems", "uri" => source_str }
|
|
1892
|
+
elsif path_source?(source)
|
|
1893
|
+
{ "type" => "path", "path" => File.expand_path(source_str), "uri" => source_str }
|
|
1894
|
+
else
|
|
1895
|
+
{ "type" => "rubygems", "uri" => source_str }
|
|
1896
|
+
end
|
|
1034
1897
|
end
|
|
1898
|
+
end
|
|
1899
|
+
|
|
1900
|
+
# --- Lockfile + runtime config ---
|
|
1035
1901
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
default_remotes = ["https://rubygems.org"] if default_remotes.empty?
|
|
1039
|
-
sources << Source::Rubygems.new(remotes: default_remotes)
|
|
1902
|
+
def write_lockfile(resolved, gemfile, lockfile = nil)
|
|
1903
|
+
specs, sources, preserved_layout = build_lockfile_specs_and_sources(resolved, gemfile, lockfile)
|
|
1040
1904
|
|
|
1041
1905
|
lockfile_data = Lockfile::LockfileData.new(
|
|
1042
|
-
specs:
|
|
1043
|
-
dependencies: gemfile
|
|
1044
|
-
platforms:
|
|
1906
|
+
specs: specs,
|
|
1907
|
+
dependencies: build_lockfile_dependencies(gemfile, lockfile),
|
|
1908
|
+
platforms: preserved_layout && lockfile ? Array(lockfile.platforms) : build_lockfile_platforms(specs, lockfile),
|
|
1045
1909
|
sources: sources,
|
|
1046
|
-
bundler_version: Scint::VERSION,
|
|
1047
|
-
ruby_version:
|
|
1048
|
-
checksums:
|
|
1910
|
+
bundler_version: lockfile&.bundler_version || Scint::VERSION,
|
|
1911
|
+
ruby_version: lockfile&.ruby_version || gemfile.ruby_version,
|
|
1912
|
+
checksums: preserved_layout && lockfile ? lockfile.checksums : build_lockfile_checksums(specs, lockfile),
|
|
1049
1913
|
)
|
|
1050
1914
|
|
|
1051
1915
|
content = Lockfile::Writer.write(lockfile_data)
|
|
1052
1916
|
FS.atomic_write("Gemfile.lock", content)
|
|
1053
1917
|
end
|
|
1054
1918
|
|
|
1919
|
+
def build_lockfile_specs_and_sources(resolved, gemfile, lockfile)
|
|
1920
|
+
resolved_for_lockfile = filter_lockfile_specs(resolved)
|
|
1921
|
+
|
|
1922
|
+
if preserve_existing_lockfile_specs?(resolved_for_lockfile, lockfile)
|
|
1923
|
+
specs = Array(lockfile.specs).map { |spec| normalize_lockfile_spec(spec) }
|
|
1924
|
+
sources = uniq_sources(Array(lockfile.sources))
|
|
1925
|
+
return [specs, sources, true]
|
|
1926
|
+
end
|
|
1927
|
+
|
|
1928
|
+
dependency_sources = dependency_sources_from_gemfile(gemfile, lockfile)
|
|
1929
|
+
existing_sources = Array(lockfile&.sources)
|
|
1930
|
+
candidate_sources = uniq_sources(existing_sources + dependency_sources.values)
|
|
1931
|
+
|
|
1932
|
+
rubygems_uris = collect_lockfile_rubygems_uris(gemfile)
|
|
1933
|
+
if rubygems_uris.empty? && candidate_sources.none? { |src| src.is_a?(Source::Rubygems) }
|
|
1934
|
+
rubygems_uris << "https://rubygems.org"
|
|
1935
|
+
end
|
|
1936
|
+
rubygems_uris.each do |uri|
|
|
1937
|
+
source = find_matching_rubygems_source(candidate_sources, uri)
|
|
1938
|
+
candidate_sources << Source::Rubygems.new(remotes: [uri]) unless source
|
|
1939
|
+
end
|
|
1940
|
+
candidate_sources = uniq_sources(candidate_sources)
|
|
1941
|
+
|
|
1942
|
+
lock_source_by_full, lock_source_by_name_version = lockfile_sources_by_spec_key(lockfile)
|
|
1943
|
+
default_rubygems_source = candidate_sources.find { |src| src.is_a?(Source::Rubygems) }
|
|
1944
|
+
|
|
1945
|
+
specs = resolved_for_lockfile.map do |spec|
|
|
1946
|
+
normalized = normalize_resolved_spec(spec)
|
|
1947
|
+
source = source_for_spec(
|
|
1948
|
+
normalized,
|
|
1949
|
+
dependency_sources: dependency_sources,
|
|
1950
|
+
candidate_sources: candidate_sources,
|
|
1951
|
+
lock_source_by_full: lock_source_by_full,
|
|
1952
|
+
lock_source_by_name_version: lock_source_by_name_version,
|
|
1953
|
+
fallback: default_rubygems_source,
|
|
1954
|
+
)
|
|
1955
|
+
normalized.merge(source: source)
|
|
1956
|
+
end
|
|
1957
|
+
|
|
1958
|
+
sources = uniq_sources(specs.map { |spec| spec[:source] }.compact)
|
|
1959
|
+
if sources.empty?
|
|
1960
|
+
fallback = default_rubygems_source || Source::Rubygems.new(remotes: ["https://rubygems.org"])
|
|
1961
|
+
sources = [fallback]
|
|
1962
|
+
specs.each { |spec| spec[:source] = fallback }
|
|
1963
|
+
end
|
|
1964
|
+
|
|
1965
|
+
[specs, sources, false]
|
|
1966
|
+
end
|
|
1967
|
+
|
|
1968
|
+
def filter_lockfile_specs(specs)
|
|
1969
|
+
specs.reject do |spec|
|
|
1970
|
+
name = spec.is_a?(Hash) ? spec[:name].to_s : spec.name.to_s
|
|
1971
|
+
name == "scint"
|
|
1972
|
+
end
|
|
1973
|
+
end
|
|
1974
|
+
|
|
1975
|
+
def preserve_existing_lockfile_specs?(resolved, lockfile)
|
|
1976
|
+
return false unless lockfile && lockfile.respond_to?(:specs)
|
|
1977
|
+
|
|
1978
|
+
wanted = resolved.map { |spec| [spec.name.to_s, spec.version.to_s] }.uniq
|
|
1979
|
+
return false if wanted.empty?
|
|
1980
|
+
|
|
1981
|
+
available = Set.new
|
|
1982
|
+
Array(lockfile.specs).each do |spec|
|
|
1983
|
+
available << [spec[:name].to_s, spec[:version].to_s]
|
|
1984
|
+
end
|
|
1985
|
+
|
|
1986
|
+
wanted.all? { |tuple| available.include?(tuple) }
|
|
1987
|
+
end
|
|
1988
|
+
|
|
1989
|
+
def normalize_lockfile_spec(spec)
|
|
1990
|
+
if spec.is_a?(Hash)
|
|
1991
|
+
{
|
|
1992
|
+
name: spec[:name],
|
|
1993
|
+
version: spec[:version],
|
|
1994
|
+
platform: spec[:platform] || "ruby",
|
|
1995
|
+
dependencies: spec[:dependencies] || [],
|
|
1996
|
+
source: spec[:source],
|
|
1997
|
+
checksum: spec[:checksum],
|
|
1998
|
+
}
|
|
1999
|
+
else
|
|
2000
|
+
{
|
|
2001
|
+
name: spec.name,
|
|
2002
|
+
version: spec.version,
|
|
2003
|
+
platform: spec.platform || "ruby",
|
|
2004
|
+
dependencies: spec.dependencies || [],
|
|
2005
|
+
source: spec.source,
|
|
2006
|
+
checksum: spec.respond_to?(:checksum) ? spec.checksum : nil,
|
|
2007
|
+
}
|
|
2008
|
+
end
|
|
2009
|
+
end
|
|
2010
|
+
|
|
2011
|
+
def normalize_resolved_spec(spec)
|
|
2012
|
+
if spec.is_a?(Hash)
|
|
2013
|
+
{
|
|
2014
|
+
name: spec[:name],
|
|
2015
|
+
version: spec[:version],
|
|
2016
|
+
platform: spec[:platform] || "ruby",
|
|
2017
|
+
dependencies: spec[:dependencies] || [],
|
|
2018
|
+
source: spec[:source],
|
|
2019
|
+
checksum: spec[:checksum],
|
|
2020
|
+
}
|
|
2021
|
+
else
|
|
2022
|
+
{
|
|
2023
|
+
name: spec.name,
|
|
2024
|
+
version: spec.version,
|
|
2025
|
+
platform: spec.platform || "ruby",
|
|
2026
|
+
dependencies: spec.dependencies || [],
|
|
2027
|
+
source: spec.source,
|
|
2028
|
+
checksum: spec.respond_to?(:checksum) ? spec.checksum : nil,
|
|
2029
|
+
}
|
|
2030
|
+
end
|
|
2031
|
+
end
|
|
2032
|
+
|
|
2033
|
+
def collect_lockfile_rubygems_uris(gemfile)
|
|
2034
|
+
uris = gemfile.sources
|
|
2035
|
+
.select { |src| src[:type] == :rubygems && src[:uri] }
|
|
2036
|
+
.map { |src| src[:uri].to_s }
|
|
2037
|
+
|
|
2038
|
+
gemfile.dependencies.each do |dep|
|
|
2039
|
+
inline = dep.source_options[:source]
|
|
2040
|
+
uris << inline.to_s if inline
|
|
2041
|
+
end
|
|
2042
|
+
|
|
2043
|
+
uris.uniq
|
|
2044
|
+
end
|
|
2045
|
+
|
|
2046
|
+
def dependency_sources_from_gemfile(gemfile, lockfile)
|
|
2047
|
+
existing_sources = Array(lockfile&.sources)
|
|
2048
|
+
out = {}
|
|
2049
|
+
|
|
2050
|
+
gemfile.dependencies.each do |dep|
|
|
2051
|
+
opts = dep.source_options
|
|
2052
|
+
|
|
2053
|
+
source =
|
|
2054
|
+
if opts[:path]
|
|
2055
|
+
find_matching_path_source(existing_sources, opts[:path]) ||
|
|
2056
|
+
Source::Path.new(path: opts[:path], name: dep.name)
|
|
2057
|
+
elsif opts[:git]
|
|
2058
|
+
matched = find_matching_git_source(existing_sources, opts)
|
|
2059
|
+
Source::Git.new(
|
|
2060
|
+
uri: opts[:git],
|
|
2061
|
+
revision: matched&.revision,
|
|
2062
|
+
ref: opts[:ref] || matched&.ref,
|
|
2063
|
+
branch: opts[:branch] || matched&.branch,
|
|
2064
|
+
tag: opts[:tag] || matched&.tag,
|
|
2065
|
+
submodules: opts.fetch(:submodules, matched&.submodules),
|
|
2066
|
+
glob: matched&.glob,
|
|
2067
|
+
name: dep.name,
|
|
2068
|
+
)
|
|
2069
|
+
elsif opts[:source]
|
|
2070
|
+
find_matching_rubygems_source(existing_sources, opts[:source]) ||
|
|
2071
|
+
Source::Rubygems.new(remotes: [opts[:source]])
|
|
2072
|
+
end
|
|
2073
|
+
|
|
2074
|
+
out[dep.name] = source if source
|
|
2075
|
+
end
|
|
2076
|
+
|
|
2077
|
+
out
|
|
2078
|
+
end
|
|
2079
|
+
|
|
2080
|
+
def lockfile_sources_by_spec_key(lockfile)
|
|
2081
|
+
by_full = {}
|
|
2082
|
+
by_name_version = {}
|
|
2083
|
+
|
|
2084
|
+
Array(lockfile&.specs).each do |spec|
|
|
2085
|
+
source = spec[:source]
|
|
2086
|
+
next unless source
|
|
2087
|
+
|
|
2088
|
+
name = spec[:name].to_s
|
|
2089
|
+
version = spec[:version].to_s
|
|
2090
|
+
platform = (spec[:platform] || "ruby").to_s
|
|
2091
|
+
|
|
2092
|
+
by_full[[name, version, platform]] = source
|
|
2093
|
+
by_name_version[[name, version]] ||= source
|
|
2094
|
+
end
|
|
2095
|
+
|
|
2096
|
+
[by_full, by_name_version]
|
|
2097
|
+
end
|
|
2098
|
+
|
|
2099
|
+
def source_for_spec(spec, dependency_sources:, candidate_sources:, lock_source_by_full:, lock_source_by_name_version:, fallback:)
|
|
2100
|
+
key_full = [spec[:name].to_s, spec[:version].to_s, spec[:platform].to_s]
|
|
2101
|
+
locked_source = lock_source_by_full[key_full] || lock_source_by_name_version[key_full[0, 2]]
|
|
2102
|
+
return locked_source if locked_source
|
|
2103
|
+
|
|
2104
|
+
dep_source = dependency_sources[spec[:name].to_s]
|
|
2105
|
+
return dep_source if dep_source
|
|
2106
|
+
|
|
2107
|
+
spec_source = spec[:source]
|
|
2108
|
+
source = find_matching_source(candidate_sources, spec_source)
|
|
2109
|
+
return source if source
|
|
2110
|
+
|
|
2111
|
+
spec_source = spec_source.to_s
|
|
2112
|
+
if git_source?(spec_source)
|
|
2113
|
+
source = Source::Git.new(uri: spec_source, name: spec[:name])
|
|
2114
|
+
candidate_sources << source
|
|
2115
|
+
return source
|
|
2116
|
+
elsif spec_source.start_with?("/") || spec_source.start_with?(".")
|
|
2117
|
+
source = Source::Path.new(path: spec_source, name: spec[:name])
|
|
2118
|
+
candidate_sources << source
|
|
2119
|
+
return source
|
|
2120
|
+
end
|
|
2121
|
+
|
|
2122
|
+
if rubygems_source_uri?(spec_source.to_s)
|
|
2123
|
+
source = Source::Rubygems.new(remotes: [spec_source.to_s])
|
|
2124
|
+
candidate_sources << source
|
|
2125
|
+
return source
|
|
2126
|
+
end
|
|
2127
|
+
|
|
2128
|
+
fallback
|
|
2129
|
+
end
|
|
2130
|
+
|
|
2131
|
+
def find_matching_source(sources, source_ref)
|
|
2132
|
+
return nil if source_ref.nil?
|
|
2133
|
+
|
|
2134
|
+
sources.find do |source|
|
|
2135
|
+
source_matches?(source, source_ref)
|
|
2136
|
+
end
|
|
2137
|
+
end
|
|
2138
|
+
|
|
2139
|
+
def source_matches?(source, source_ref)
|
|
2140
|
+
return true if source.equal?(source_ref)
|
|
2141
|
+
return true if source == source_ref
|
|
2142
|
+
|
|
2143
|
+
source_key = normalize_source_key(source_ref)
|
|
2144
|
+
return false unless source_key
|
|
2145
|
+
|
|
2146
|
+
if source.is_a?(Source::Rubygems)
|
|
2147
|
+
source.remotes.any? { |remote| normalize_source_key(remote) == source_key }
|
|
2148
|
+
elsif source.respond_to?(:uri)
|
|
2149
|
+
normalize_source_key(source.uri) == source_key
|
|
2150
|
+
else
|
|
2151
|
+
normalize_source_key(source) == source_key
|
|
2152
|
+
end
|
|
2153
|
+
end
|
|
2154
|
+
|
|
2155
|
+
def normalize_source_key(source_ref)
|
|
2156
|
+
return nil if source_ref.nil?
|
|
2157
|
+
|
|
2158
|
+
raw =
|
|
2159
|
+
if source_ref.respond_to?(:uri)
|
|
2160
|
+
source_ref.uri.to_s
|
|
2161
|
+
elsif source_ref.respond_to?(:path)
|
|
2162
|
+
source_ref.path.to_s
|
|
2163
|
+
else
|
|
2164
|
+
source_ref.to_s
|
|
2165
|
+
end
|
|
2166
|
+
return nil if raw.empty?
|
|
2167
|
+
|
|
2168
|
+
if raw.match?(%r{\Ahttps?://}i)
|
|
2169
|
+
raw = raw.sub(%r{\Ahttps?://}i, "")
|
|
2170
|
+
raw = raw.sub(%r{\.git/?\z}i, "")
|
|
2171
|
+
raw.chomp("/").downcase
|
|
2172
|
+
elsif raw.start_with?("/") || raw.start_with?(".")
|
|
2173
|
+
File.expand_path(raw)
|
|
2174
|
+
else
|
|
2175
|
+
raw.sub(%r{\.git/?\z}i, "").chomp("/").downcase
|
|
2176
|
+
end
|
|
2177
|
+
end
|
|
2178
|
+
|
|
2179
|
+
def find_matching_rubygems_source(sources, uri)
|
|
2180
|
+
sources.find do |source|
|
|
2181
|
+
source.is_a?(Source::Rubygems) && source.remotes.any? { |remote| source_matches?(remote, uri) }
|
|
2182
|
+
end
|
|
2183
|
+
end
|
|
2184
|
+
|
|
2185
|
+
def find_matching_path_source(sources, path)
|
|
2186
|
+
sources.find { |source| source.is_a?(Source::Path) && source_matches?(source, path) }
|
|
2187
|
+
end
|
|
2188
|
+
|
|
2189
|
+
def find_matching_git_source(sources, opts)
|
|
2190
|
+
candidates = sources.select { |source| source.is_a?(Source::Git) && source_matches?(source, opts[:git]) }
|
|
2191
|
+
return nil if candidates.empty?
|
|
2192
|
+
|
|
2193
|
+
candidates.find { |source| git_source_options_match?(source, opts) } || candidates.first
|
|
2194
|
+
end
|
|
2195
|
+
|
|
2196
|
+
def git_source_options_match?(source, opts)
|
|
2197
|
+
return false if opts[:branch] && source.branch.to_s != opts[:branch].to_s
|
|
2198
|
+
return false if opts[:tag] && source.tag.to_s != opts[:tag].to_s
|
|
2199
|
+
return false if opts[:ref] && source.ref.to_s != opts[:ref].to_s
|
|
2200
|
+
|
|
2201
|
+
true
|
|
2202
|
+
end
|
|
2203
|
+
|
|
2204
|
+
def uniq_sources(sources)
|
|
2205
|
+
out = []
|
|
2206
|
+
sources.each do |source|
|
|
2207
|
+
next unless source
|
|
2208
|
+
out << source unless out.any? { |existing| existing.eql?(source) }
|
|
2209
|
+
end
|
|
2210
|
+
out
|
|
2211
|
+
end
|
|
2212
|
+
|
|
2213
|
+
def build_lockfile_dependencies(gemfile, lockfile)
|
|
2214
|
+
locked = lockfile&.dependencies || {}
|
|
2215
|
+
gemfile.dependencies
|
|
2216
|
+
.select { |dep| lockfile_dependency_direct?(dep) }
|
|
2217
|
+
.map do |dep|
|
|
2218
|
+
locked_dep = locked[dep.name]
|
|
2219
|
+
{
|
|
2220
|
+
name: dep.name,
|
|
2221
|
+
version_reqs: dep.version_reqs,
|
|
2222
|
+
pinned: !!(locked_dep && locked_dep[:pinned]),
|
|
2223
|
+
}
|
|
2224
|
+
end
|
|
2225
|
+
end
|
|
2226
|
+
|
|
2227
|
+
def lockfile_dependency_direct?(dep)
|
|
2228
|
+
opts = dep.source_options || {}
|
|
2229
|
+
return true unless opts[:gemspec_generated]
|
|
2230
|
+
|
|
2231
|
+
opts[:gemspec_primary] != false
|
|
2232
|
+
end
|
|
2233
|
+
|
|
2234
|
+
def build_lockfile_platforms(specs, lockfile)
|
|
2235
|
+
platforms = Set.new(Array(lockfile&.platforms))
|
|
2236
|
+
specs.each do |spec|
|
|
2237
|
+
platform = spec[:platform] || "ruby"
|
|
2238
|
+
platforms << platform
|
|
2239
|
+
end
|
|
2240
|
+
platforms << "ruby"
|
|
2241
|
+
platforms.to_a
|
|
2242
|
+
end
|
|
2243
|
+
|
|
2244
|
+
def build_lockfile_checksums(specs, lockfile)
|
|
2245
|
+
existing = lockfile&.checksums
|
|
2246
|
+
checksums = {}
|
|
2247
|
+
|
|
2248
|
+
specs.each do |spec|
|
|
2249
|
+
key = lockfile_spec_checksum_key(spec)
|
|
2250
|
+
checksum = spec[:checksum]
|
|
2251
|
+
if checksum && !Array(checksum).empty?
|
|
2252
|
+
checksums[key] = Array(checksum)
|
|
2253
|
+
elsif existing&.key?(key)
|
|
2254
|
+
checksums[key] = Array(existing[key])
|
|
2255
|
+
end
|
|
2256
|
+
end
|
|
2257
|
+
|
|
2258
|
+
return nil if checksums.empty?
|
|
2259
|
+
|
|
2260
|
+
checksums
|
|
2261
|
+
end
|
|
2262
|
+
|
|
2263
|
+
def lockfile_spec_checksum_key(spec)
|
|
2264
|
+
SpecUtils.full_name_for(spec[:name], spec[:version], spec[:platform] || "ruby")
|
|
2265
|
+
end
|
|
2266
|
+
|
|
1055
2267
|
def write_runtime_config(resolved, bundle_path)
|
|
1056
|
-
ruby_dir =
|
|
1057
|
-
RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
2268
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
1058
2269
|
|
|
1059
2270
|
data = {}
|
|
1060
2271
|
resolved.each do |spec|
|
|
1061
|
-
full =
|
|
2272
|
+
full = SpecUtils.full_name(spec)
|
|
1062
2273
|
gem_dir = File.join(ruby_dir, "gems", full)
|
|
1063
2274
|
spec_file = File.join(ruby_dir, "specifications", "#{full}.gemspec")
|
|
1064
2275
|
require_paths = read_require_paths(spec_file)
|
|
1065
2276
|
load_paths = require_paths
|
|
1066
|
-
.map { |rp|
|
|
2277
|
+
.map { |rp| expand_require_path(gem_dir, rp) }
|
|
1067
2278
|
.select { |path| Dir.exist?(path) }
|
|
1068
2279
|
|
|
1069
2280
|
default_lib = File.join(gem_dir, "lib")
|
|
1070
2281
|
load_paths << default_lib if load_paths.empty? && Dir.exist?(default_lib)
|
|
1071
|
-
load_paths.concat(detect_nested_lib_paths(gem_dir))
|
|
1072
2282
|
load_paths.uniq!
|
|
1073
2283
|
|
|
1074
2284
|
# Add ext load path if extensions exist
|
|
@@ -1076,6 +2286,12 @@ module Scint
|
|
|
1076
2286
|
Platform.gem_arch, Platform.extension_api_version, full)
|
|
1077
2287
|
load_paths << ext_dir if Dir.exist?(ext_dir)
|
|
1078
2288
|
|
|
2289
|
+
if load_paths.empty?
|
|
2290
|
+
source_paths = runtime_source_load_paths(spec)
|
|
2291
|
+
load_paths.concat(source_paths)
|
|
2292
|
+
load_paths.uniq!
|
|
2293
|
+
end
|
|
2294
|
+
|
|
1079
2295
|
data[spec.name] = {
|
|
1080
2296
|
version: spec.version.to_s,
|
|
1081
2297
|
load_paths: load_paths,
|
|
@@ -1089,33 +2305,47 @@ module Scint
|
|
|
1089
2305
|
def read_require_paths(spec_file)
|
|
1090
2306
|
return ["lib"] unless File.exist?(spec_file)
|
|
1091
2307
|
|
|
1092
|
-
gemspec =
|
|
2308
|
+
gemspec = SpecUtils.load_gemspec(spec_file)
|
|
1093
2309
|
paths = Array(gemspec&.require_paths).reject(&:empty?)
|
|
1094
2310
|
paths.empty? ? ["lib"] : paths
|
|
1095
|
-
rescue StandardError
|
|
2311
|
+
rescue SystemExit, StandardError
|
|
1096
2312
|
["lib"]
|
|
1097
2313
|
end
|
|
1098
2314
|
|
|
1099
|
-
def
|
|
1100
|
-
|
|
1101
|
-
return
|
|
2315
|
+
def expand_require_path(gem_dir, require_path)
|
|
2316
|
+
value = require_path.to_s
|
|
2317
|
+
return value if Pathname.new(value).absolute?
|
|
1102
2318
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
2319
|
+
File.join(gem_dir, value)
|
|
2320
|
+
rescue StandardError
|
|
2321
|
+
File.join(gem_dir, require_path.to_s)
|
|
2322
|
+
end
|
|
2323
|
+
|
|
2324
|
+
def runtime_source_load_paths(spec)
|
|
2325
|
+
source_root = spec.source.to_s
|
|
2326
|
+
return [] unless source_root.start_with?("/") && Dir.exist?(source_root)
|
|
2327
|
+
|
|
2328
|
+
source_dir = begin
|
|
2329
|
+
resolve_git_gem_subdir(source_root, spec)
|
|
2330
|
+
rescue InstallError
|
|
2331
|
+
source_root
|
|
1107
2332
|
end
|
|
1108
|
-
return [] if top_level_rb
|
|
1109
2333
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
2334
|
+
gemspec_file = File.join(source_dir, "#{spec.name}.gemspec")
|
|
2335
|
+
require_paths = read_require_paths(gemspec_file)
|
|
2336
|
+
paths = require_paths
|
|
2337
|
+
.map { |rp| expand_require_path(source_dir, rp) }
|
|
2338
|
+
.select { |path| Dir.exist?(path) }
|
|
2339
|
+
|
|
2340
|
+
default_lib = File.join(source_dir, "lib")
|
|
2341
|
+
paths << default_lib if paths.empty? && Dir.exist?(default_lib)
|
|
2342
|
+
paths.uniq
|
|
2343
|
+
rescue StandardError
|
|
2344
|
+
[]
|
|
1113
2345
|
end
|
|
1114
2346
|
|
|
1115
2347
|
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}"
|
|
2348
|
+
SpecUtils.full_name(spec)
|
|
1119
2349
|
end
|
|
1120
2350
|
|
|
1121
2351
|
def elapsed_ms_since(start_time)
|
|
@@ -1124,7 +2354,7 @@ module Scint
|
|
|
1124
2354
|
end
|
|
1125
2355
|
|
|
1126
2356
|
def force_purge_artifacts(resolved, bundle_path, cache)
|
|
1127
|
-
ruby_dir =
|
|
2357
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
1128
2358
|
ext_root = File.join(ruby_dir, "extensions", Platform.gem_arch, Platform.extension_api_version)
|
|
1129
2359
|
|
|
1130
2360
|
resolved.each do |spec|
|
|
@@ -1132,8 +2362,12 @@ module Scint
|
|
|
1132
2362
|
|
|
1133
2363
|
# Global cache artifacts.
|
|
1134
2364
|
FileUtils.rm_f(cache.inbound_path(spec))
|
|
1135
|
-
FileUtils.rm_rf(cache.
|
|
2365
|
+
FileUtils.rm_rf(cache.assembling_path(spec))
|
|
2366
|
+
FileUtils.rm_rf(cache.cached_path(spec))
|
|
2367
|
+
FileUtils.rm_f(cache.cached_spec_path(spec))
|
|
2368
|
+
FileUtils.rm_f(cache.cached_manifest_path(spec))
|
|
1136
2369
|
FileUtils.rm_f(cache.spec_cache_path(spec))
|
|
2370
|
+
FileUtils.rm_rf(cache.extracted_path(spec))
|
|
1137
2371
|
FileUtils.rm_rf(cache.ext_path(spec))
|
|
1138
2372
|
|
|
1139
2373
|
# Local bundle artifacts.
|
|
@@ -1160,6 +2394,28 @@ module Scint
|
|
|
1160
2394
|
"#{format_elapsed(elapsed_ms)}, #{workers} #{noun} used"
|
|
1161
2395
|
end
|
|
1162
2396
|
|
|
2397
|
+
def emit_network_error_details(error)
|
|
2398
|
+
return unless error.is_a?(NetworkError)
|
|
2399
|
+
|
|
2400
|
+
headers = error.response_headers
|
|
2401
|
+
body = error.response_body.to_s
|
|
2402
|
+
return if (headers.nil? || headers.empty?) && body.empty?
|
|
2403
|
+
|
|
2404
|
+
if headers && !headers.empty?
|
|
2405
|
+
$stderr.puts " headers:"
|
|
2406
|
+
headers.sort.each do |key, value|
|
|
2407
|
+
$stderr.puts " #{key}: #{value}"
|
|
2408
|
+
end
|
|
2409
|
+
end
|
|
2410
|
+
|
|
2411
|
+
return if body.empty?
|
|
2412
|
+
|
|
2413
|
+
$stderr.puts " body:"
|
|
2414
|
+
body.each_line do |line|
|
|
2415
|
+
$stderr.puts " #{line.rstrip}"
|
|
2416
|
+
end
|
|
2417
|
+
end
|
|
2418
|
+
|
|
1163
2419
|
def warn_missing_bundle_gitignore_entry
|
|
1164
2420
|
path = ".gitignore"
|
|
1165
2421
|
return unless File.file?(path)
|
|
@@ -1201,6 +2457,12 @@ module Scint
|
|
|
1201
2457
|
when "--path"
|
|
1202
2458
|
@path = @argv[i + 1]
|
|
1203
2459
|
i += 2
|
|
2460
|
+
when "--without"
|
|
2461
|
+
@without_groups = @argv[i + 1]&.split(/[\s:,]+/)&.map(&:to_sym) || []
|
|
2462
|
+
i += 2
|
|
2463
|
+
when "--with"
|
|
2464
|
+
@with_groups = @argv[i + 1]&.split(/[\s:,]+/)&.map(&:to_sym) || []
|
|
2465
|
+
i += 2
|
|
1204
2466
|
when "--verbose"
|
|
1205
2467
|
@verbose = true
|
|
1206
2468
|
i += 1
|
|
@@ -1211,6 +2473,32 @@ module Scint
|
|
|
1211
2473
|
i += 1
|
|
1212
2474
|
end
|
|
1213
2475
|
end
|
|
2476
|
+
|
|
2477
|
+
# Also read BUNDLE_WITHOUT / BUNDLE_WITH env vars (Bundler compat)
|
|
2478
|
+
if !@without_groups && ENV["BUNDLE_WITHOUT"]
|
|
2479
|
+
@without_groups = ENV["BUNDLE_WITHOUT"].split(/[\s:,]+/).map(&:to_sym)
|
|
2480
|
+
end
|
|
2481
|
+
if !@with_groups && ENV["BUNDLE_WITH"]
|
|
2482
|
+
@with_groups = ENV["BUNDLE_WITH"].split(/[\s:,]+/).map(&:to_sym)
|
|
2483
|
+
end
|
|
2484
|
+
|
|
2485
|
+
# Read from .bundle/config if present
|
|
2486
|
+
load_bundle_config_groups if !@without_groups && !@with_groups
|
|
2487
|
+
end
|
|
2488
|
+
|
|
2489
|
+
def load_bundle_config_groups
|
|
2490
|
+
config_path = File.join(".bundle", "config")
|
|
2491
|
+
return unless File.exist?(config_path)
|
|
2492
|
+
|
|
2493
|
+
config = YAML.safe_load(File.read(config_path)) rescue nil
|
|
2494
|
+
return unless config.is_a?(Hash)
|
|
2495
|
+
|
|
2496
|
+
if config["BUNDLE_WITHOUT"] && !@without_groups
|
|
2497
|
+
@without_groups = config["BUNDLE_WITHOUT"].to_s.split(/[\s:]+/).map(&:to_sym)
|
|
2498
|
+
end
|
|
2499
|
+
if config["BUNDLE_WITH"] && !@with_groups
|
|
2500
|
+
@with_groups = config["BUNDLE_WITH"].to_s.split(/[\s:]+/).map(&:to_sym)
|
|
2501
|
+
end
|
|
1214
2502
|
end
|
|
1215
2503
|
end
|
|
1216
2504
|
end
|