scint 0.7.0 → 0.8.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/FEATURES.md +4 -0
- data/README.md +142 -198
- data/VERSION +1 -1
- data/lib/scint/cache/layout.rb +66 -5
- data/lib/scint/cache/manifest.rb +120 -0
- data/lib/scint/cache/prewarm.rb +445 -33
- data/lib/scint/cache/validity.rb +134 -0
- data/lib/scint/cli/cache.rb +34 -6
- data/lib/scint/cli/exec.rb +1 -1
- data/lib/scint/cli/install.rb +611 -292
- data/lib/scint/fs.rb +175 -28
- data/lib/scint/gem/package.rb +6 -2
- data/lib/scint/gemfile/parser.rb +13 -6
- data/lib/scint/index/client.rb +13 -2
- data/lib/scint/installer/extension_builder.rb +63 -43
- data/lib/scint/installer/linker.rb +43 -2
- data/lib/scint/installer/planner.rb +24 -28
- data/lib/scint/installer/preparer.rb +167 -37
- data/lib/scint/installer/promoter.rb +97 -0
- data/lib/scint/linker.sh +137 -0
- data/lib/scint/spec_utils.rb +79 -0
- data/lib/scint.rb +12 -4
- metadata +5 -1
data/lib/scint/cli/install.rb
CHANGED
|
@@ -23,7 +23,9 @@ require_relative "../downloader/pool"
|
|
|
23
23
|
require_relative "../gem/package"
|
|
24
24
|
require_relative "../gem/extractor"
|
|
25
25
|
require_relative "../cache/layout"
|
|
26
|
+
require_relative "../cache/manifest"
|
|
26
27
|
require_relative "../cache/metadata_store"
|
|
28
|
+
require_relative "../cache/validity"
|
|
27
29
|
require_relative "../installer/planner"
|
|
28
30
|
require_relative "../installer/linker"
|
|
29
31
|
require_relative "../installer/preparer"
|
|
@@ -41,40 +43,57 @@ module Scint
|
|
|
41
43
|
class Install
|
|
42
44
|
RUNTIME_LOCK = "scint.lock.marshal"
|
|
43
45
|
|
|
44
|
-
def initialize(argv = [])
|
|
46
|
+
def initialize(argv = [], without: nil, with: nil, output: $stderr)
|
|
45
47
|
@argv = argv
|
|
46
48
|
@jobs = nil
|
|
47
49
|
@path = nil
|
|
48
50
|
@verbose = false
|
|
49
51
|
@force = false
|
|
52
|
+
@without_groups = nil
|
|
53
|
+
@with_groups = nil
|
|
54
|
+
@output = output
|
|
50
55
|
@download_pool = nil
|
|
51
56
|
@download_pool_lock = Thread::Mutex.new
|
|
52
57
|
@gemspec_cache = {}
|
|
53
58
|
@gemspec_cache_lock = Thread::Mutex.new
|
|
54
59
|
parse_options
|
|
60
|
+
# Allow programmatic override (for tests)
|
|
61
|
+
@without_groups = Array(without).map(&:to_sym) if without
|
|
62
|
+
@with_groups = Array(with).map(&:to_sym) if with
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def _tmark(label, t0)
|
|
66
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
67
|
+
@output.puts " [timing] #{label}: #{((now - t0) * 1000).round}ms" if ENV["SCINT_TIMING"]
|
|
68
|
+
now
|
|
55
69
|
end
|
|
56
70
|
|
|
57
71
|
def run
|
|
58
72
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
73
|
+
_t = start_time
|
|
59
74
|
|
|
60
75
|
cache = Scint::Cache::Layout.new
|
|
76
|
+
cache_telemetry = Scint::Cache::Telemetry.new
|
|
61
77
|
bundle_path = @path || ENV["BUNDLER_PATH"] || ".bundle"
|
|
62
78
|
bundle_display = display_bundle_path(bundle_path)
|
|
63
79
|
bundle_path = File.expand_path(bundle_path)
|
|
64
|
-
worker_count = @jobs || [Platform.cpu_count * 2, 50].min
|
|
80
|
+
worker_count = [(@jobs || [Platform.cpu_count * 2, 50].min).to_i, 1].max
|
|
65
81
|
compile_slots = compile_slots_for(worker_count)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
82
|
+
git_slots = git_slots_for(worker_count)
|
|
83
|
+
per_type_limits = install_task_limits(worker_count, compile_slots, git_slots)
|
|
84
|
+
@output.puts "#{GREEN}💎#{RESET} Scintellating Gemfile into #{BOLD}#{bundle_display}#{RESET} #{DIM}(scint #{VERSION}, ruby #{RUBY_VERSION})#{RESET}"
|
|
85
|
+
@output.puts
|
|
69
86
|
|
|
70
87
|
# 0. Build credential store from config files (~/.bundle/config, XDG scint/credentials)
|
|
71
88
|
@credentials = Credentials.new
|
|
72
89
|
|
|
73
90
|
# 1. Start the scheduler with 1 worker — scale up dynamically
|
|
74
|
-
scheduler = Scheduler.new(max_workers: worker_count, fail_fast: true, per_type_limits: per_type_limits
|
|
91
|
+
scheduler = Scheduler.new(max_workers: worker_count, fail_fast: true, per_type_limits: per_type_limits,
|
|
92
|
+
progress: Progress.new(output: @output))
|
|
75
93
|
scheduler.start
|
|
76
94
|
|
|
77
95
|
begin
|
|
96
|
+
_t = _tmark("startup", _t)
|
|
78
97
|
# 2. Parse Gemfile
|
|
79
98
|
gemfile = Scint::Gemfile::Parser.parse("Gemfile")
|
|
80
99
|
|
|
@@ -86,6 +105,7 @@ module Scint
|
|
|
86
105
|
dep_count = gemfile.dependencies.size
|
|
87
106
|
scheduler.scale_workers(dep_count)
|
|
88
107
|
|
|
108
|
+
_t = _tmark("parse_gemfile", _t)
|
|
89
109
|
# 3. Enqueue index fetches for all sources immediately
|
|
90
110
|
gemfile.sources.each do |source|
|
|
91
111
|
scheduler.enqueue(:fetch_index, source[:uri] || source.to_s,
|
|
@@ -106,20 +126,26 @@ module Scint
|
|
|
106
126
|
-> { clone_git_source(source, cache) })
|
|
107
127
|
end
|
|
108
128
|
|
|
129
|
+
_t = _tmark("enqueue_fetches", _t)
|
|
109
130
|
# 6. Wait for index fetches, then resolve
|
|
110
131
|
scheduler.wait_for(:fetch_index)
|
|
132
|
+
_t = _tmark("wait_index", _t)
|
|
111
133
|
scheduler.wait_for(:git_clone)
|
|
134
|
+
_t = _tmark("wait_git", _t)
|
|
112
135
|
|
|
113
136
|
resolved = resolve(gemfile, lockfile, cache)
|
|
114
137
|
resolved = dedupe_resolved_specs(adjust_meta_gems(resolved))
|
|
138
|
+
resolved = filter_excluded_gems(resolved, gemfile)
|
|
115
139
|
force_purge_artifacts(resolved, bundle_path, cache) if @force
|
|
116
140
|
|
|
141
|
+
_t = _tmark("resolve", _t)
|
|
117
142
|
# 7. Plan: diff resolved vs installed
|
|
118
|
-
plan = Installer::Planner.plan(resolved, bundle_path, cache)
|
|
143
|
+
plan = Installer::Planner.plan(resolved, bundle_path, cache, telemetry: cache_telemetry)
|
|
119
144
|
total_gems = resolved.size
|
|
120
145
|
updated_gems = plan.count { |e| e.action != :skip }
|
|
121
146
|
cached_gems = total_gems - updated_gems
|
|
122
147
|
to_install = plan.reject { |e| e.action == :skip }
|
|
148
|
+
_t = _tmark("plan", _t)
|
|
123
149
|
|
|
124
150
|
# Scale up for download/install phase based on actual work count
|
|
125
151
|
scheduler.scale_workers(to_install.size)
|
|
@@ -127,6 +153,7 @@ module Scint
|
|
|
127
153
|
# Warm-cache accelerator: pre-materialize cache-backed gem trees in
|
|
128
154
|
# batches so install workers avoid one cp process per gem.
|
|
129
155
|
bulk_prelink_gem_files(to_install, cache, bundle_path)
|
|
156
|
+
_t = _tmark("prelink", _t)
|
|
130
157
|
|
|
131
158
|
if to_install.empty?
|
|
132
159
|
# Keep lock artifacts aligned even when everything is already installed.
|
|
@@ -135,7 +162,7 @@ module Scint
|
|
|
135
162
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
136
163
|
worker_count = scheduler.stats[:workers]
|
|
137
164
|
warn_missing_bundle_gitignore_entry
|
|
138
|
-
|
|
165
|
+
@output.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}"
|
|
139
166
|
return 0
|
|
140
167
|
end
|
|
141
168
|
|
|
@@ -160,14 +187,14 @@ module Scint
|
|
|
160
187
|
errors = scheduler.errors.dup
|
|
161
188
|
stats = scheduler.stats
|
|
162
189
|
if errors.any?
|
|
163
|
-
|
|
190
|
+
@output.puts "#{RED}Some gems failed to install:#{RESET}"
|
|
164
191
|
errors.each do |err|
|
|
165
192
|
error = err[:error]
|
|
166
|
-
|
|
193
|
+
@output.puts " #{BOLD}#{err[:name]}#{RESET}: #{error.message}"
|
|
167
194
|
emit_network_error_details(error)
|
|
168
195
|
end
|
|
169
196
|
elsif stats[:failed] > 0
|
|
170
|
-
|
|
197
|
+
@output.puts "#{YELLOW}Warning: #{stats[:failed]} jobs failed but no error details captured#{RESET}"
|
|
171
198
|
end
|
|
172
199
|
|
|
173
200
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
@@ -180,21 +207,25 @@ module Scint
|
|
|
180
207
|
|
|
181
208
|
if has_failures
|
|
182
209
|
warn_missing_bundle_gitignore_entry
|
|
183
|
-
|
|
210
|
+
@output.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}"
|
|
184
211
|
1
|
|
185
212
|
else
|
|
186
213
|
# 10. Write lockfile + runtime config only for successful installs
|
|
187
214
|
write_lockfile(resolved, gemfile, lockfile)
|
|
188
215
|
write_runtime_config(resolved, bundle_path)
|
|
189
216
|
warn_missing_bundle_gitignore_entry
|
|
190
|
-
|
|
217
|
+
@output.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}"
|
|
191
218
|
0
|
|
192
219
|
end
|
|
193
220
|
ensure
|
|
194
221
|
begin
|
|
195
|
-
|
|
222
|
+
cache_telemetry.warn_if_needed(cache_root: cache.root)
|
|
196
223
|
ensure
|
|
197
|
-
|
|
224
|
+
begin
|
|
225
|
+
scheduler.shutdown
|
|
226
|
+
ensure
|
|
227
|
+
close_download_pool
|
|
228
|
+
end
|
|
198
229
|
end
|
|
199
230
|
end
|
|
200
231
|
end
|
|
@@ -233,6 +264,91 @@ module Scint
|
|
|
233
264
|
seen.values
|
|
234
265
|
end
|
|
235
266
|
|
|
267
|
+
# Determine which gem names should be excluded based on group settings.
|
|
268
|
+
# A gem is excluded if ALL of its group memberships are in excluded groups.
|
|
269
|
+
# Gems appearing in any non-excluded group are kept.
|
|
270
|
+
def excluded_gem_names(gemfile, resolved: nil)
|
|
271
|
+
excluded_groups = compute_excluded_groups(gemfile)
|
|
272
|
+
return Set.new if excluded_groups.empty?
|
|
273
|
+
|
|
274
|
+
# Build map: gem_name => set of all groups it appears in (across all declarations)
|
|
275
|
+
gem_groups = Hash.new { |h, k| h[k] = Set.new }
|
|
276
|
+
gemfile.dependencies.each do |dep|
|
|
277
|
+
dep.groups.each { |g| gem_groups[dep.name] << g }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# A gem is directly excluded if ALL its groups are excluded
|
|
281
|
+
directly_excluded = Set.new
|
|
282
|
+
gem_groups.each do |name, groups|
|
|
283
|
+
directly_excluded << name if groups.subset?(excluded_groups)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# If we have resolved specs, also exclude transitive-only deps
|
|
287
|
+
if resolved && directly_excluded.any?
|
|
288
|
+
exclude_transitive_deps(directly_excluded, resolved, gem_groups)
|
|
289
|
+
else
|
|
290
|
+
directly_excluded
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Filter resolved specs, removing gems that belong only to excluded groups.
|
|
295
|
+
def filter_excluded_gems(resolved, gemfile)
|
|
296
|
+
excluded = excluded_gem_names(gemfile, resolved: resolved)
|
|
297
|
+
return resolved if excluded.empty?
|
|
298
|
+
|
|
299
|
+
resolved.reject { |spec| excluded.include?(spec.name) }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
private
|
|
303
|
+
|
|
304
|
+
def compute_excluded_groups(gemfile)
|
|
305
|
+
optional = Set.new(Array(gemfile.optional_groups))
|
|
306
|
+
without = Set.new(Array(@without_groups))
|
|
307
|
+
with = Set.new(Array(@with_groups))
|
|
308
|
+
|
|
309
|
+
# Optional groups are excluded by default unless explicitly included via --with
|
|
310
|
+
excluded = optional - with
|
|
311
|
+
# --without adds more groups to exclude
|
|
312
|
+
excluded.merge(without)
|
|
313
|
+
excluded
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Walk the dependency graph to find transitive deps that are ONLY
|
|
317
|
+
# reachable through excluded gems. Shared deps are kept.
|
|
318
|
+
def exclude_transitive_deps(directly_excluded, resolved, gem_groups)
|
|
319
|
+
# Build dependency graph: name => [dep_names]
|
|
320
|
+
dep_graph = {}
|
|
321
|
+
resolved.each do |spec|
|
|
322
|
+
dep_names = Array(spec.dependencies).filter_map do |dep|
|
|
323
|
+
if dep.is_a?(Hash)
|
|
324
|
+
dep[:name] || dep["name"]
|
|
325
|
+
elsif dep.respond_to?(:name)
|
|
326
|
+
dep.name
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
dep_graph[spec.name] = dep_names
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
all_names = Set.new(resolved.map(&:name))
|
|
333
|
+
|
|
334
|
+
# Start from Gemfile deps that are NOT excluded, then walk transitive deps
|
|
335
|
+
included_roots = gem_groups.keys.reject { |n| directly_excluded.include?(n) }
|
|
336
|
+
|
|
337
|
+
# BFS from included roots to find all reachable gems
|
|
338
|
+
reachable = Set.new
|
|
339
|
+
queue = included_roots.dup
|
|
340
|
+
while (name = queue.shift)
|
|
341
|
+
next if reachable.include?(name)
|
|
342
|
+
reachable << name
|
|
343
|
+
(dep_graph[name] || []).each { |dep| queue << dep }
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Everything not reachable from included roots is excluded
|
|
347
|
+
all_names - reachable
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
public
|
|
351
|
+
|
|
236
352
|
# Install scint into the bundle by copying our own lib tree.
|
|
237
353
|
# No download needed — we know exactly where we are.
|
|
238
354
|
def install_builtin_gem(entry, bundle_path)
|
|
@@ -354,20 +470,20 @@ module Scint
|
|
|
354
470
|
if opts[:git]
|
|
355
471
|
git_source = find_matching_git_source(Array(lockfile&.sources), opts) || find_matching_git_source(gemfile.sources, opts)
|
|
356
472
|
revision_hint = git_source&.revision || git_source&.ref || opts[:ref] || opts[:branch] || opts[:tag] || "HEAD"
|
|
357
|
-
|
|
358
|
-
if
|
|
359
|
-
clone_git_repo(opts[:git],
|
|
360
|
-
elsif
|
|
361
|
-
fetch_git_repo(
|
|
473
|
+
git_repo = cache&.git_path(opts[:git])
|
|
474
|
+
if git_repo && !Dir.exist?(git_repo)
|
|
475
|
+
clone_git_repo(opts[:git], git_repo)
|
|
476
|
+
elsif git_repo && Dir.exist?(git_repo)
|
|
477
|
+
fetch_git_repo(git_repo)
|
|
362
478
|
end
|
|
363
|
-
if
|
|
479
|
+
if git_repo && Dir.exist?(git_repo)
|
|
364
480
|
begin
|
|
365
|
-
resolved_revision = resolve_git_revision(
|
|
481
|
+
resolved_revision = resolve_git_revision(git_repo, revision_hint)
|
|
366
482
|
cache_key = "#{opts[:git]}@#{resolved_revision}"
|
|
367
483
|
git_metadata = git_source_metadata_cache[cache_key]
|
|
368
484
|
unless git_metadata
|
|
369
485
|
git_metadata = build_git_path_gems_for_revision(
|
|
370
|
-
|
|
486
|
+
git_repo,
|
|
371
487
|
resolved_revision,
|
|
372
488
|
glob: opts[:glob],
|
|
373
489
|
source_desc: opts[:git],
|
|
@@ -429,17 +545,17 @@ module Scint
|
|
|
429
545
|
candidates.each do |gs|
|
|
430
546
|
next unless File.exist?(gs)
|
|
431
547
|
begin
|
|
432
|
-
spec =
|
|
548
|
+
spec = SpecUtils.load_gemspec(gs, isolate: true)
|
|
433
549
|
return spec if spec
|
|
434
|
-
rescue StandardError
|
|
550
|
+
rescue SystemExit, StandardError
|
|
435
551
|
nil
|
|
436
552
|
end
|
|
437
553
|
end
|
|
438
554
|
nil
|
|
439
555
|
end
|
|
440
556
|
|
|
441
|
-
def find_git_gemspec(
|
|
442
|
-
gemspec_paths = gemspec_paths_in_git_revision(
|
|
557
|
+
def find_git_gemspec(git_repo, revision, gem_name, glob: nil)
|
|
558
|
+
gemspec_paths = gemspec_paths_in_git_revision(git_repo, revision)
|
|
443
559
|
return nil if gemspec_paths.empty?
|
|
444
560
|
|
|
445
561
|
path = gemspec_paths[gem_name.to_s]
|
|
@@ -450,23 +566,23 @@ module Scint
|
|
|
450
566
|
path ||= gemspec_paths.values.first
|
|
451
567
|
return nil if path.nil?
|
|
452
568
|
|
|
453
|
-
load_git_gemspec(
|
|
569
|
+
load_git_gemspec(git_repo, revision, path)
|
|
454
570
|
rescue StandardError
|
|
455
571
|
nil
|
|
456
572
|
end
|
|
457
573
|
|
|
458
|
-
def build_git_path_gems_for_revision(
|
|
459
|
-
gemspec_paths = gemspec_paths_in_git_revision(
|
|
574
|
+
def build_git_path_gems_for_revision(git_repo, revision, glob: nil, source_desc: nil)
|
|
575
|
+
gemspec_paths = gemspec_paths_in_git_revision(git_repo, revision)
|
|
460
576
|
return {} if gemspec_paths.empty?
|
|
461
577
|
|
|
462
578
|
glob_regex = glob ? git_glob_to_regex(glob) : nil
|
|
463
579
|
data = {}
|
|
464
580
|
|
|
465
|
-
|
|
581
|
+
with_git_checkout(git_repo, revision) do |checkout_dir|
|
|
466
582
|
gemspec_paths.each_value do |path|
|
|
467
583
|
next if glob_regex && !path.match?(glob_regex)
|
|
468
584
|
|
|
469
|
-
gemspec =
|
|
585
|
+
gemspec = load_gemspec_from_checkout(checkout_dir, path)
|
|
470
586
|
next unless gemspec
|
|
471
587
|
|
|
472
588
|
deps = gemspec.dependencies
|
|
@@ -611,19 +727,19 @@ module Scint
|
|
|
611
727
|
by_source = git_specs.group_by { |s| s[:source] }
|
|
612
728
|
by_source.each do |source, specs|
|
|
613
729
|
uri, revision = git_source_ref(source)
|
|
614
|
-
|
|
730
|
+
git_repo = cache.git_path(uri)
|
|
615
731
|
# Do not invalidate an otherwise-usable lockfile just because this
|
|
616
732
|
# git source has not been cached yet in the current machine/session.
|
|
617
|
-
next unless Dir.exist?(
|
|
733
|
+
next unless Dir.exist?(git_repo)
|
|
618
734
|
|
|
619
735
|
resolved_revision = begin
|
|
620
|
-
resolve_git_revision(
|
|
736
|
+
resolve_git_revision(git_repo, revision)
|
|
621
737
|
rescue InstallError
|
|
622
738
|
nil
|
|
623
739
|
end
|
|
624
740
|
return false unless resolved_revision
|
|
625
741
|
|
|
626
|
-
gemspec_paths = gemspec_paths_in_git_revision(
|
|
742
|
+
gemspec_paths = gemspec_paths_in_git_revision(git_repo, resolved_revision)
|
|
627
743
|
gemspec_names = gemspec_paths.keys.to_set
|
|
628
744
|
return false if gemspec_names.empty?
|
|
629
745
|
|
|
@@ -635,9 +751,9 @@ module Scint
|
|
|
635
751
|
true
|
|
636
752
|
end
|
|
637
753
|
|
|
638
|
-
def gemspec_paths_in_git_revision(
|
|
754
|
+
def gemspec_paths_in_git_revision(git_repo, revision)
|
|
639
755
|
out, _err, status = git_capture3(
|
|
640
|
-
"
|
|
756
|
+
"-C", git_repo,
|
|
641
757
|
"ls-tree",
|
|
642
758
|
"-r",
|
|
643
759
|
"--name-only",
|
|
@@ -657,8 +773,8 @@ module Scint
|
|
|
657
773
|
{}
|
|
658
774
|
end
|
|
659
775
|
|
|
660
|
-
def runtime_dependencies_for_git_gemspec(
|
|
661
|
-
spec = load_git_gemspec(
|
|
776
|
+
def runtime_dependencies_for_git_gemspec(git_repo, revision, gemspec_path)
|
|
777
|
+
spec = load_git_gemspec(git_repo, revision, gemspec_path)
|
|
662
778
|
return nil unless spec
|
|
663
779
|
|
|
664
780
|
spec.dependencies.select { |dep| dep.type == :runtime }
|
|
@@ -666,41 +782,32 @@ module Scint
|
|
|
666
782
|
nil
|
|
667
783
|
end
|
|
668
784
|
|
|
669
|
-
def load_git_gemspec(
|
|
785
|
+
def load_git_gemspec(git_repo, revision, gemspec_path)
|
|
670
786
|
return nil if gemspec_path.to_s.empty?
|
|
671
787
|
|
|
672
|
-
|
|
673
|
-
|
|
788
|
+
with_git_checkout(git_repo, revision) do |checkout_dir|
|
|
789
|
+
load_gemspec_from_checkout(checkout_dir, gemspec_path)
|
|
674
790
|
end
|
|
675
791
|
rescue StandardError
|
|
676
792
|
nil
|
|
677
793
|
end
|
|
678
794
|
|
|
679
|
-
def
|
|
680
|
-
worktree = Dir.mktmpdir("scint-gemspec")
|
|
795
|
+
def with_git_checkout(git_repo, revision)
|
|
681
796
|
_out, _err, status = git_capture3(
|
|
682
|
-
"
|
|
683
|
-
"
|
|
684
|
-
"checkout",
|
|
685
|
-
"--force",
|
|
686
|
-
revision,
|
|
797
|
+
"-C", git_repo,
|
|
798
|
+
"checkout", "-f", revision,
|
|
687
799
|
)
|
|
688
800
|
return nil unless status.success?
|
|
689
801
|
|
|
690
|
-
|
|
691
|
-
yield worktree if block_given?
|
|
692
|
-
ensure
|
|
693
|
-
FileUtils.rm_rf(worktree) if worktree && !worktree.empty?
|
|
802
|
+
yield git_repo if block_given?
|
|
694
803
|
end
|
|
695
804
|
|
|
696
|
-
def
|
|
697
|
-
absolute_gemspec = File.join(
|
|
805
|
+
def load_gemspec_from_checkout(checkout_dir, gemspec_path)
|
|
806
|
+
absolute_gemspec = File.join(checkout_dir, gemspec_path)
|
|
698
807
|
return nil unless File.exist?(absolute_gemspec)
|
|
699
808
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
end
|
|
703
|
-
rescue StandardError
|
|
809
|
+
SpecUtils.load_gemspec(absolute_gemspec, isolate: true)
|
|
810
|
+
rescue SystemExit, StandardError
|
|
704
811
|
nil
|
|
705
812
|
end
|
|
706
813
|
|
|
@@ -786,7 +893,7 @@ module Scint
|
|
|
786
893
|
spec = entry.spec
|
|
787
894
|
source = spec.source
|
|
788
895
|
if git_source?(source)
|
|
789
|
-
|
|
896
|
+
ensure_git_repo_for_spec(spec, cache, fetch: true)
|
|
790
897
|
return
|
|
791
898
|
end
|
|
792
899
|
source_uri = source.to_s
|
|
@@ -827,21 +934,34 @@ module Scint
|
|
|
827
934
|
# Git gems are extracted from the cached checkout; path gems are
|
|
828
935
|
# linked directly from local source.
|
|
829
936
|
if git_source?(spec.source)
|
|
830
|
-
|
|
937
|
+
assemble_git_spec(entry, cache, fetch: false)
|
|
831
938
|
return
|
|
832
939
|
end
|
|
833
940
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
834
941
|
|
|
835
|
-
|
|
836
|
-
return if Dir.exist?(extracted)
|
|
942
|
+
return if Scint::Cache::Validity.cached_valid?(spec, cache)
|
|
837
943
|
|
|
838
944
|
dest_path = cache.inbound_path(spec)
|
|
839
945
|
raise InstallError, "Missing cached gem file for #{spec.name}: #{dest_path}" unless File.exist?(dest_path)
|
|
840
946
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
947
|
+
assembling = cache.assembling_path(spec)
|
|
948
|
+
tmp = "#{assembling}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
949
|
+
begin
|
|
950
|
+
FileUtils.rm_rf(assembling)
|
|
951
|
+
FileUtils.rm_rf(tmp)
|
|
952
|
+
FS.mkdir_p(File.dirname(assembling))
|
|
953
|
+
|
|
954
|
+
pkg = GemPkg::Package.new
|
|
955
|
+
result = pkg.extract(dest_path, tmp)
|
|
956
|
+
FS.atomic_move(tmp, assembling)
|
|
957
|
+
cache_gemspec(spec, result[:gemspec], cache)
|
|
958
|
+
|
|
959
|
+
unless Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
960
|
+
promote_assembled_gem(spec, cache, assembling, result[:gemspec], extensions: false)
|
|
961
|
+
end
|
|
962
|
+
ensure
|
|
963
|
+
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
964
|
+
end
|
|
845
965
|
end
|
|
846
966
|
|
|
847
967
|
def git_source?(source)
|
|
@@ -870,113 +990,94 @@ module Scint
|
|
|
870
990
|
def prepare_git_source(entry, cache)
|
|
871
991
|
# Legacy helper used by tests/callers that expect git download+extract
|
|
872
992
|
# in a single step.
|
|
873
|
-
|
|
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)
|
|
993
|
+
assemble_git_spec(entry, cache, fetch: true)
|
|
876
994
|
end
|
|
877
995
|
|
|
878
|
-
def
|
|
996
|
+
def ensure_git_repo_for_spec(spec, cache, fetch:)
|
|
879
997
|
source = spec.source
|
|
880
|
-
uri,
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
bare_repo = cache.git_path(uri)
|
|
998
|
+
uri, _revision = git_source_ref(source)
|
|
999
|
+
git_repo = cache.git_path(uri)
|
|
884
1000
|
|
|
885
|
-
# Serialize all git operations per
|
|
1001
|
+
# Serialize all git operations per repo — git uses index.lock
|
|
886
1002
|
# and can't handle concurrent checkouts from the same repo.
|
|
887
|
-
git_mutex_for(
|
|
888
|
-
if Dir.exist?(
|
|
889
|
-
fetch_git_repo(
|
|
1003
|
+
git_mutex_for(git_repo).synchronize do
|
|
1004
|
+
if Dir.exist?(git_repo)
|
|
1005
|
+
fetch_git_repo(git_repo) if fetch
|
|
890
1006
|
else
|
|
891
|
-
clone_git_repo(uri,
|
|
892
|
-
fetch_git_repo(bare_repo)
|
|
1007
|
+
clone_git_repo(uri, git_repo)
|
|
893
1008
|
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
1009
|
end
|
|
1010
|
+
|
|
1011
|
+
git_repo
|
|
907
1012
|
end
|
|
908
1013
|
|
|
909
|
-
def
|
|
1014
|
+
def assemble_git_spec(entry, cache, fetch: true)
|
|
910
1015
|
spec = entry.spec
|
|
911
|
-
|
|
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
|
|
1016
|
+
return if Scint::Cache::Validity.cached_valid?(spec, cache)
|
|
921
1017
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
FS.clone_tree(gem_root, tmp)
|
|
1018
|
+
source = spec.source
|
|
1019
|
+
uri, revision = git_source_ref(source)
|
|
1020
|
+
submodules = git_source_submodules?(source)
|
|
926
1021
|
|
|
927
|
-
|
|
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
|
|
1022
|
+
git_repo = cache.git_path(uri)
|
|
934
1023
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
end
|
|
1024
|
+
# Serialize all git operations per repo — git uses index.lock
|
|
1025
|
+
# and can't handle concurrent checkouts from the same repo.
|
|
1026
|
+
git_mutex_for(git_repo).synchronize do
|
|
1027
|
+
tmp_assembled = nil
|
|
940
1028
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1029
|
+
begin
|
|
1030
|
+
if Dir.exist?(git_repo)
|
|
1031
|
+
fetch_git_repo(git_repo) if fetch
|
|
1032
|
+
else
|
|
1033
|
+
clone_git_repo(uri, git_repo)
|
|
1034
|
+
end
|
|
947
1035
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
end
|
|
1036
|
+
resolved_revision = resolve_git_revision(git_repo, revision)
|
|
1037
|
+
assembling = cache.assembling_path(spec)
|
|
1038
|
+
tmp_assembled = "#{assembling}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
1039
|
+
promoter = cache_promoter(cache)
|
|
953
1040
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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
|
-
)
|
|
973
|
-
end
|
|
1041
|
+
FileUtils.rm_rf(assembling)
|
|
1042
|
+
FileUtils.rm_rf(tmp_assembled)
|
|
1043
|
+
FS.mkdir_p(File.dirname(assembling))
|
|
974
1044
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1045
|
+
promoter.validate_within_root!(cache.root, assembling, label: "assembling")
|
|
1046
|
+
promoter.validate_within_root!(cache.root, tmp_assembled, label: "git-assembled")
|
|
1047
|
+
|
|
1048
|
+
checkout_git_revision(git_repo, resolved_revision, spec, uri, submodules: submodules)
|
|
1049
|
+
|
|
1050
|
+
gem_root = resolve_git_gem_subdir(git_repo, spec)
|
|
1051
|
+
gem_rel = git_relative_root(git_repo, gem_root)
|
|
1052
|
+
dest_root = tmp_assembled
|
|
1053
|
+
dest_path = gem_rel.empty? ? dest_root : File.join(dest_root, gem_rel)
|
|
1054
|
+
|
|
1055
|
+
promoter.validate_within_root!(cache.root, dest_path, label: "git-dest")
|
|
1056
|
+
|
|
1057
|
+
FS.clone_tree(gem_root, dest_path)
|
|
1058
|
+
|
|
1059
|
+
# Remove .git artifacts so assembled output is deterministic.
|
|
1060
|
+
Dir.glob(File.join(tmp_assembled, "**", ".git"), File::FNM_DOTMATCH).each do |path|
|
|
1061
|
+
FileUtils.rm_rf(path)
|
|
1062
|
+
end
|
|
1063
|
+
|
|
1064
|
+
copy_gemspec_root_files(git_repo, gem_root, dest_root, spec)
|
|
1065
|
+
FS.atomic_move(tmp_assembled, assembling)
|
|
1066
|
+
|
|
1067
|
+
gem_subdir = begin
|
|
1068
|
+
resolve_git_gem_subdir(assembling, spec)
|
|
1069
|
+
rescue InstallError
|
|
1070
|
+
assembling
|
|
1071
|
+
end
|
|
1072
|
+
gemspec = read_gemspec_from_extracted(gem_subdir, spec)
|
|
1073
|
+
cache_gemspec(spec, gemspec, cache) if gemspec
|
|
1074
|
+
|
|
1075
|
+
unless Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
1076
|
+
promote_assembled_gem(spec, cache, assembling, gemspec, extensions: false)
|
|
1077
|
+
end
|
|
1078
|
+
ensure
|
|
1079
|
+
FileUtils.rm_rf(tmp_assembled) if tmp_assembled && File.exist?(tmp_assembled)
|
|
1080
|
+
end
|
|
980
1081
|
end
|
|
981
1082
|
end
|
|
982
1083
|
|
|
@@ -993,61 +1094,76 @@ module Scint
|
|
|
993
1094
|
source.respond_to?(:submodules) && !!source.submodules
|
|
994
1095
|
end
|
|
995
1096
|
|
|
996
|
-
def
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1097
|
+
def copy_gemspec_root_files(repo_root, gem_root, dest_root, spec)
|
|
1098
|
+
repo_root = File.expand_path(repo_root.to_s)
|
|
1099
|
+
gem_root = File.expand_path(gem_root.to_s)
|
|
1100
|
+
return if repo_root == gem_root
|
|
1101
|
+
|
|
1102
|
+
gemspec_path = git_gemspec_path_for_root(gem_root, spec)
|
|
1103
|
+
return unless gemspec_path && File.exist?(gemspec_path)
|
|
1104
|
+
|
|
1105
|
+
content = File.read(gemspec_path) rescue nil
|
|
1106
|
+
return unless content
|
|
1107
|
+
|
|
1108
|
+
root_files = git_root_files_from_gemspec(content)
|
|
1109
|
+
root_files.each do |file|
|
|
1110
|
+
source = File.join(repo_root, file)
|
|
1111
|
+
next unless File.file?(source)
|
|
1112
|
+
|
|
1113
|
+
dest = File.join(dest_root, file)
|
|
1114
|
+
next if File.exist?(dest)
|
|
1115
|
+
|
|
1116
|
+
FS.clonefile(source, dest)
|
|
1009
1117
|
end
|
|
1010
1118
|
end
|
|
1011
1119
|
|
|
1012
|
-
def
|
|
1013
|
-
|
|
1014
|
-
|
|
1120
|
+
def git_gemspec_path_for_root(gem_root, spec)
|
|
1121
|
+
if spec && spec.respond_to?(:name)
|
|
1122
|
+
candidate = File.join(gem_root, "#{spec.name}.gemspec")
|
|
1123
|
+
return candidate if File.exist?(candidate)
|
|
1124
|
+
end
|
|
1125
|
+
|
|
1126
|
+
Dir.glob(File.join(gem_root, "*.gemspec")).first
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def git_root_files_from_gemspec(content)
|
|
1130
|
+
files = ["RAILS_VERSION", "VERSION"]
|
|
1131
|
+
files.select { |file| content.include?(file) }
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def git_relative_root(repo_root, gem_root)
|
|
1135
|
+
repo_root = File.expand_path(repo_root.to_s)
|
|
1136
|
+
gem_root = File.expand_path(gem_root.to_s)
|
|
1137
|
+
return "" if repo_root == gem_root
|
|
1138
|
+
|
|
1139
|
+
if gem_root.start_with?("#{repo_root}/")
|
|
1140
|
+
return gem_root.delete_prefix("#{repo_root}/")
|
|
1141
|
+
end
|
|
1142
|
+
|
|
1143
|
+
File.basename(gem_root)
|
|
1144
|
+
end
|
|
1015
1145
|
|
|
1146
|
+
def checkout_git_revision(git_repo, resolved_revision, spec, uri, submodules: false)
|
|
1016
1147
|
_out, err, status = git_capture3(
|
|
1017
|
-
"
|
|
1018
|
-
"
|
|
1019
|
-
"add",
|
|
1020
|
-
"--detach",
|
|
1021
|
-
"--force",
|
|
1022
|
-
worktree,
|
|
1023
|
-
resolved_revision,
|
|
1148
|
+
"-C", git_repo,
|
|
1149
|
+
"checkout", "-f", resolved_revision,
|
|
1024
1150
|
)
|
|
1025
1151
|
unless status.success?
|
|
1026
|
-
raise InstallError, "Git
|
|
1152
|
+
raise InstallError, "Git checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
1027
1153
|
end
|
|
1028
1154
|
|
|
1029
|
-
|
|
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)
|
|
1155
|
+
return unless submodules
|
|
1043
1156
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1157
|
+
_sub_out, sub_err, sub_status = git_capture3(
|
|
1158
|
+
"-C", git_repo,
|
|
1159
|
+
"-c", "protocol.file.allow=always",
|
|
1160
|
+
"submodule",
|
|
1161
|
+
"update",
|
|
1162
|
+
"--init",
|
|
1163
|
+
"--recursive",
|
|
1164
|
+
)
|
|
1165
|
+
unless sub_status.success?
|
|
1166
|
+
raise InstallError, "Git submodule update failed for #{spec.name} (#{uri}@#{resolved_revision}): #{sub_err.to_s.strip}"
|
|
1051
1167
|
end
|
|
1052
1168
|
end
|
|
1053
1169
|
|
|
@@ -1059,32 +1175,35 @@ module Scint
|
|
|
1059
1175
|
end
|
|
1060
1176
|
end
|
|
1061
1177
|
|
|
1062
|
-
def clone_git_repo(uri,
|
|
1063
|
-
FS.mkdir_p(File.dirname(
|
|
1064
|
-
_out, err, status = git_capture3("clone",
|
|
1178
|
+
def clone_git_repo(uri, git_repo)
|
|
1179
|
+
FS.mkdir_p(File.dirname(git_repo))
|
|
1180
|
+
_out, err, status = git_capture3("clone", uri.to_s, git_repo)
|
|
1065
1181
|
unless status.success?
|
|
1066
1182
|
raise InstallError, "Git clone failed for #{uri}: #{err.to_s.strip}"
|
|
1067
1183
|
end
|
|
1068
1184
|
end
|
|
1069
1185
|
|
|
1070
|
-
def fetch_git_repo(
|
|
1186
|
+
def fetch_git_repo(git_repo)
|
|
1071
1187
|
_out, err, status = git_capture3(
|
|
1072
|
-
"
|
|
1188
|
+
"-C", git_repo,
|
|
1073
1189
|
"fetch",
|
|
1074
1190
|
"--prune",
|
|
1191
|
+
"--force",
|
|
1075
1192
|
"origin",
|
|
1076
|
-
"+refs/heads/*:refs/heads/*",
|
|
1077
|
-
"+refs/tags/*:refs/tags/*",
|
|
1078
1193
|
)
|
|
1079
1194
|
unless status.success?
|
|
1080
|
-
raise InstallError, "Git fetch failed for #{
|
|
1195
|
+
raise InstallError, "Git fetch failed for #{git_repo}: #{err.to_s.strip}"
|
|
1081
1196
|
end
|
|
1082
1197
|
end
|
|
1083
1198
|
|
|
1084
|
-
def resolve_git_revision(
|
|
1085
|
-
|
|
1199
|
+
def resolve_git_revision(git_repo, revision)
|
|
1200
|
+
# Try origin/<revision> first so we pick up fetched branch tips.
|
|
1201
|
+
out, _err, status = git_capture3("-C", git_repo, "rev-parse", "origin/#{revision}^{commit}")
|
|
1202
|
+
return out.strip if status.success?
|
|
1203
|
+
|
|
1204
|
+
out, err, status = git_capture3("-C", git_repo, "rev-parse", "#{revision}^{commit}")
|
|
1086
1205
|
unless status.success?
|
|
1087
|
-
raise InstallError, "Unable to resolve git revision #{revision.inspect} in #{
|
|
1206
|
+
raise InstallError, "Unable to resolve git revision #{revision.inspect} in #{git_repo}: #{err.to_s.strip}"
|
|
1088
1207
|
end
|
|
1089
1208
|
out.strip
|
|
1090
1209
|
end
|
|
@@ -1093,64 +1212,59 @@ module Scint
|
|
|
1093
1212
|
Open3.capture3("git", "-c", "core.fsmonitor=false", *args)
|
|
1094
1213
|
end
|
|
1095
1214
|
|
|
1096
|
-
def
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
|
1215
|
+
def compile_slots_for(worker_count)
|
|
1216
|
+
# Scale compile concurrency with available CPUs.
|
|
1217
|
+
# Most native extensions have 1-3 source files and don't benefit from
|
|
1218
|
+
# high make -j; running more concurrent builds is more effective.
|
|
1219
|
+
# Each slot gets cpu_count/slots make jobs (see adaptive_make_jobs).
|
|
1220
|
+
workers = [worker_count.to_i, 1].max
|
|
1221
|
+
override = positive_integer_env("SCINT_COMPILE_CONCURRENCY")
|
|
1222
|
+
return [override, workers].min if override
|
|
1124
1223
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1224
|
+
cpus = Platform.cpu_count
|
|
1225
|
+
# Aim for 8 make-jobs per slot → slots = cpus / 8, clamped.
|
|
1226
|
+
slots = cpus / 8
|
|
1227
|
+
slots = [[slots, 2].max, workers].min
|
|
1228
|
+
slots
|
|
1127
1229
|
end
|
|
1128
1230
|
|
|
1129
|
-
def
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
2
|
|
1231
|
+
def git_slots_for(worker_count)
|
|
1232
|
+
workers = [worker_count.to_i, 1].max
|
|
1233
|
+
override = positive_integer_env("SCINT_GIT_CONCURRENCY")
|
|
1234
|
+
slots = override || workers
|
|
1235
|
+
[[slots, workers].min, 1].max
|
|
1136
1236
|
end
|
|
1137
1237
|
|
|
1138
|
-
def install_task_limits(worker_count, compile_slots)
|
|
1238
|
+
def install_task_limits(worker_count, compile_slots, git_slots = worker_count)
|
|
1139
1239
|
# Leave headroom for compile and binstub lanes so link/download
|
|
1140
1240
|
# throughput cannot fully starve them.
|
|
1141
|
-
|
|
1241
|
+
workers = [worker_count.to_i, 1].max
|
|
1242
|
+
io_cpu_limit = [workers - compile_slots - 1, 1].max
|
|
1142
1243
|
# Keep download in-flight set bounded so fail-fast exits quickly on
|
|
1143
1244
|
# auth/source errors instead of queueing a large burst.
|
|
1144
1245
|
download_limit = [io_cpu_limit, 8].min
|
|
1246
|
+
git_limit = [[git_slots.to_i, 1].max, workers].min
|
|
1145
1247
|
{
|
|
1146
1248
|
download: download_limit,
|
|
1147
1249
|
extract: io_cpu_limit,
|
|
1148
1250
|
link: io_cpu_limit,
|
|
1251
|
+
git_clone: git_limit,
|
|
1149
1252
|
build_ext: compile_slots,
|
|
1150
1253
|
binstub: 1,
|
|
1151
1254
|
}
|
|
1152
1255
|
end
|
|
1153
1256
|
|
|
1257
|
+
def positive_integer_env(key)
|
|
1258
|
+
raw = ENV[key]
|
|
1259
|
+
return nil if raw.nil? || raw.empty?
|
|
1260
|
+
|
|
1261
|
+
value = Integer(raw, exception: false)
|
|
1262
|
+
return nil unless value
|
|
1263
|
+
return nil if value <= 0
|
|
1264
|
+
|
|
1265
|
+
value
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1154
1268
|
def display_bundle_path(path)
|
|
1155
1269
|
return path if path.start_with?("/", "./", "../")
|
|
1156
1270
|
|
|
@@ -1297,10 +1411,21 @@ module Scint
|
|
|
1297
1411
|
source_str
|
|
1298
1412
|
end
|
|
1299
1413
|
else
|
|
1300
|
-
|
|
1301
|
-
|
|
1414
|
+
cached_dir = cache.cached_path(entry.spec)
|
|
1415
|
+
assembling = cache.assembling_path(entry.spec)
|
|
1416
|
+
base = if entry.cached_path
|
|
1417
|
+
entry.cached_path
|
|
1418
|
+
elsif Scint::Cache::Validity.cached_valid?(entry.spec, cache)
|
|
1419
|
+
cached_dir
|
|
1420
|
+
elsif Dir.exist?(assembling)
|
|
1421
|
+
assembling
|
|
1422
|
+
else
|
|
1423
|
+
nil
|
|
1424
|
+
end
|
|
1425
|
+
|
|
1426
|
+
if git_source?(entry.spec.source) && base && Dir.exist?(base)
|
|
1302
1427
|
resolve_git_gem_subdir(base, entry.spec)
|
|
1303
|
-
elsif path_source?(entry.spec.source) && Dir.exist?(base)
|
|
1428
|
+
elsif path_source?(entry.spec.source) && base && Dir.exist?(base)
|
|
1304
1429
|
begin
|
|
1305
1430
|
resolve_path_gem_subdir(base, entry.spec)
|
|
1306
1431
|
rescue InstallError
|
|
@@ -1371,19 +1496,18 @@ module Scint
|
|
|
1371
1496
|
from_cache: true,
|
|
1372
1497
|
)
|
|
1373
1498
|
Installer::Linker.link_files(prepared, bundle_path)
|
|
1374
|
-
# If this gem has a cached native build, materialize it during link.
|
|
1375
|
-
# This lets reinstalling into a fresh .bundle skip build_ext entirely.
|
|
1376
|
-
Installer::ExtensionBuilder.link_cached_build(prepared, bundle_path, cache)
|
|
1377
1499
|
end
|
|
1378
1500
|
|
|
1379
1501
|
def build_extensions(entry, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
1502
|
+
spec = entry.spec
|
|
1380
1503
|
extracted = extracted_path_for_entry(entry, cache)
|
|
1381
|
-
gemspec = load_gemspec(extracted,
|
|
1504
|
+
gemspec = load_gemspec(extracted, spec, cache)
|
|
1505
|
+
promote_after_build = assembling_path?(extracted, cache)
|
|
1382
1506
|
|
|
1383
|
-
sync_build_env_dependencies(
|
|
1507
|
+
sync_build_env_dependencies(spec, bundle_path, cache)
|
|
1384
1508
|
|
|
1385
1509
|
prepared = PreparedGem.new(
|
|
1386
|
-
spec:
|
|
1510
|
+
spec: spec,
|
|
1387
1511
|
extracted_path: extracted,
|
|
1388
1512
|
gemspec: gemspec,
|
|
1389
1513
|
from_cache: true,
|
|
@@ -1394,8 +1518,24 @@ module Scint
|
|
|
1394
1518
|
bundle_path,
|
|
1395
1519
|
cache,
|
|
1396
1520
|
compile_slots: compile_slots,
|
|
1397
|
-
output_tail: ->(lines) { progress&.on_build_tail(
|
|
1521
|
+
output_tail: ->(lines) { progress&.on_build_tail(spec.name, lines) },
|
|
1398
1522
|
)
|
|
1523
|
+
|
|
1524
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
1525
|
+
bundle_gem_dir = File.join(ruby_dir, "gems", SpecUtils.full_name(spec))
|
|
1526
|
+
if Dir.exist?(bundle_gem_dir)
|
|
1527
|
+
Installer::ExtensionBuilder.sync_extensions_into_gem(extracted, bundle_gem_dir)
|
|
1528
|
+
File.write(File.join(bundle_gem_dir, Installer::ExtensionBuilder::BUILD_MARKER), "")
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1531
|
+
return unless promote_after_build
|
|
1532
|
+
|
|
1533
|
+
promote_assembled_gem(spec, cache, extracted, gemspec, extensions: true)
|
|
1534
|
+
rescue StandardError
|
|
1535
|
+
if promote_after_build && extracted && Dir.exist?(extracted)
|
|
1536
|
+
FileUtils.rm_rf(extracted)
|
|
1537
|
+
end
|
|
1538
|
+
raise
|
|
1399
1539
|
end
|
|
1400
1540
|
|
|
1401
1541
|
def sync_build_env_dependencies(spec, bundle_path, cache)
|
|
@@ -1460,6 +1600,12 @@ module Scint
|
|
|
1460
1600
|
return cached
|
|
1461
1601
|
end
|
|
1462
1602
|
|
|
1603
|
+
direct = read_gemspec_from_extracted(extracted_path, spec)
|
|
1604
|
+
if direct
|
|
1605
|
+
@gemspec_cache_lock.synchronize { @gemspec_cache[cache_key] = direct }
|
|
1606
|
+
return direct
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1463
1609
|
inbound = cache.inbound_path(spec)
|
|
1464
1610
|
return nil unless File.exist?(inbound)
|
|
1465
1611
|
|
|
@@ -1473,49 +1619,90 @@ module Scint
|
|
|
1473
1619
|
end
|
|
1474
1620
|
end
|
|
1475
1621
|
|
|
1622
|
+
def read_gemspec_from_extracted(extracted_dir, spec)
|
|
1623
|
+
return nil unless extracted_dir && Dir.exist?(extracted_dir)
|
|
1624
|
+
|
|
1625
|
+
pattern = File.join(extracted_dir, "*.gemspec")
|
|
1626
|
+
candidates = Dir.glob(pattern)
|
|
1627
|
+
return nil if candidates.empty?
|
|
1628
|
+
|
|
1629
|
+
load_gemspec_file(candidates.first, spec)
|
|
1630
|
+
end
|
|
1631
|
+
|
|
1632
|
+
# Load a .gemspec file, temporarily injecting VERSION env var for gems
|
|
1633
|
+
# like kgio/unicorn that use `ENV["VERSION"] or abort` in their gemspec.
|
|
1634
|
+
def load_gemspec_file(path, spec = nil)
|
|
1635
|
+
version = spec.respond_to?(:version) ? spec.version.to_s : nil
|
|
1636
|
+
old_version = ENV["VERSION"]
|
|
1637
|
+
begin
|
|
1638
|
+
ENV["VERSION"] = version if version && !ENV["VERSION"]
|
|
1639
|
+
SpecUtils.load_gemspec(path, isolate: true)
|
|
1640
|
+
rescue SystemExit, StandardError
|
|
1641
|
+
nil
|
|
1642
|
+
ensure
|
|
1643
|
+
ENV["VERSION"] = old_version
|
|
1644
|
+
end
|
|
1645
|
+
end
|
|
1646
|
+
|
|
1476
1647
|
def bulk_prelink_gem_files(entries, cache, bundle_path)
|
|
1477
|
-
# Keep small installs simple; batching is for large warm-path runs.
|
|
1478
1648
|
return if entries.length < 32
|
|
1479
1649
|
|
|
1480
1650
|
ruby_dir = File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
1481
1651
|
gems_dir = File.join(ruby_dir, "gems")
|
|
1652
|
+
cache_abi_dir = cache.cached_abi_dir
|
|
1482
1653
|
|
|
1483
|
-
|
|
1654
|
+
gem_names = []
|
|
1655
|
+
entries.each do |entry|
|
|
1484
1656
|
next unless entry.action == :link || entry.action == :build_ext
|
|
1485
1657
|
|
|
1486
|
-
|
|
1658
|
+
source_dir = entry.cached_path
|
|
1659
|
+
next unless source_dir
|
|
1660
|
+
|
|
1487
1661
|
full_name = cache.full_name(entry.spec)
|
|
1488
|
-
next unless File.basename(
|
|
1489
|
-
next unless Dir.exist?(
|
|
1662
|
+
next unless File.basename(source_dir) == full_name
|
|
1663
|
+
next unless Dir.exist?(source_dir)
|
|
1490
1664
|
next if Dir.exist?(File.join(gems_dir, full_name))
|
|
1491
1665
|
|
|
1492
|
-
|
|
1666
|
+
gem_names << full_name
|
|
1667
|
+
end
|
|
1668
|
+
|
|
1669
|
+
return if gem_names.empty?
|
|
1670
|
+
|
|
1671
|
+
if ENV["SCINT_TIMING"]
|
|
1672
|
+
@output.puts " [timing] prelink: #{gem_names.size} gems via linker"
|
|
1493
1673
|
end
|
|
1494
|
-
return if sources.empty?
|
|
1495
1674
|
|
|
1496
|
-
FS.
|
|
1675
|
+
FS.bulk_link_gems(cache_abi_dir, gems_dir, gem_names)
|
|
1497
1676
|
rescue StandardError => e
|
|
1498
|
-
|
|
1677
|
+
@output.puts("bulk prelink warning: #{e.message}") if ENV["SCINT_DEBUG"]
|
|
1499
1678
|
end
|
|
1500
1679
|
|
|
1501
1680
|
def load_cached_gemspec(spec, cache, extracted_path)
|
|
1502
|
-
|
|
1503
|
-
return nil unless File.exist?(path)
|
|
1681
|
+
paths = [cache.cached_spec_path(spec)]
|
|
1504
1682
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1683
|
+
paths.each do |path|
|
|
1684
|
+
next unless File.exist?(path)
|
|
1685
|
+
|
|
1686
|
+
data = File.binread(path)
|
|
1687
|
+
gemspec = if data.start_with?("# -*- encoding")
|
|
1688
|
+
# Ruby format (to_ruby output) — most reliable, preserves require_paths
|
|
1689
|
+
Gem::Specification.load(path)
|
|
1690
|
+
elsif data.start_with?("---")
|
|
1691
|
+
data.force_encoding("UTF-8") if data.encoding != Encoding::UTF_8
|
|
1512
1692
|
Gem::Specification.from_yaml(data)
|
|
1693
|
+
else
|
|
1694
|
+
begin
|
|
1695
|
+
Marshal.load(data)
|
|
1696
|
+
rescue StandardError
|
|
1697
|
+
data.force_encoding("UTF-8") if data.encoding != Encoding::UTF_8
|
|
1698
|
+
Gem::Specification.from_yaml(data)
|
|
1699
|
+
end
|
|
1513
1700
|
end
|
|
1701
|
+
return gemspec if cached_gemspec_valid?(gemspec, extracted_path)
|
|
1514
1702
|
end
|
|
1515
|
-
return gemspec if cached_gemspec_valid?(gemspec, extracted_path)
|
|
1516
1703
|
|
|
1517
1704
|
nil
|
|
1518
|
-
rescue StandardError
|
|
1705
|
+
rescue SystemExit, StandardError
|
|
1519
1706
|
nil
|
|
1520
1707
|
end
|
|
1521
1708
|
|
|
@@ -1549,12 +1736,108 @@ module Scint
|
|
|
1549
1736
|
end
|
|
1550
1737
|
|
|
1551
1738
|
def cache_gemspec(spec, gemspec, cache)
|
|
1552
|
-
path = cache.
|
|
1553
|
-
|
|
1739
|
+
path = cache.cached_spec_path(spec)
|
|
1740
|
+
content = if gemspec.respond_to?(:to_ruby)
|
|
1741
|
+
gemspec.to_ruby
|
|
1742
|
+
else
|
|
1743
|
+
gemspec.to_yaml
|
|
1744
|
+
end
|
|
1745
|
+
FS.atomic_write(path, content)
|
|
1554
1746
|
rescue StandardError
|
|
1555
1747
|
# Non-fatal: we'll read metadata from .gem next time.
|
|
1556
1748
|
end
|
|
1557
1749
|
|
|
1750
|
+
def cache_promoter(cache)
|
|
1751
|
+
@cache_promoter ||= Installer::Promoter.new(root: cache.root)
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
def assembling_path?(path, cache)
|
|
1755
|
+
return false if path.nil? || path.empty?
|
|
1756
|
+
|
|
1757
|
+
root = File.expand_path(cache.assembling_dir)
|
|
1758
|
+
candidate = File.expand_path(path)
|
|
1759
|
+
candidate == root || candidate.start_with?("#{root}/")
|
|
1760
|
+
end
|
|
1761
|
+
|
|
1762
|
+
def promote_assembled_gem(spec, cache, assembling_path, gemspec, extensions:)
|
|
1763
|
+
return unless assembling_path && Dir.exist?(assembling_path)
|
|
1764
|
+
|
|
1765
|
+
cached_dir = cache.cached_path(spec)
|
|
1766
|
+
promoter = cache_promoter(cache)
|
|
1767
|
+
lock_key = "#{Platform.abi_key}-#{cache.full_name(spec)}"
|
|
1768
|
+
|
|
1769
|
+
promoter.validate_within_root!(cache.root, assembling_path, label: "assembling")
|
|
1770
|
+
promoter.validate_within_root!(cache.root, cached_dir, label: "cached")
|
|
1771
|
+
|
|
1772
|
+
begin
|
|
1773
|
+
result = nil
|
|
1774
|
+
promoter.with_staging_dir(prefix: "cached") do |staging|
|
|
1775
|
+
FS.clone_tree(assembling_path, staging)
|
|
1776
|
+
manifest = build_cached_manifest(spec, cache, staging, extensions: extensions)
|
|
1777
|
+
Scint::Cache::Manifest.write_dotfiles(staging, manifest)
|
|
1778
|
+
spec_payload = gemspec ? gemspec.to_ruby : nil
|
|
1779
|
+
result = promoter.promote_tree(
|
|
1780
|
+
staging_path: staging,
|
|
1781
|
+
target_path: cached_dir,
|
|
1782
|
+
lock_key: lock_key,
|
|
1783
|
+
)
|
|
1784
|
+
if result == :promoted
|
|
1785
|
+
write_cached_metadata(spec, cache, spec_payload, manifest)
|
|
1786
|
+
end
|
|
1787
|
+
FileUtils.rm_rf(assembling_path) if Dir.exist?(assembling_path)
|
|
1788
|
+
end
|
|
1789
|
+
result
|
|
1790
|
+
rescue StandardError
|
|
1791
|
+
FileUtils.rm_rf(cached_dir) if Dir.exist?(cached_dir)
|
|
1792
|
+
raise
|
|
1793
|
+
end
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def write_cached_metadata(spec, cache, spec_payload, manifest)
|
|
1797
|
+
spec_path = cache.cached_spec_path(spec)
|
|
1798
|
+
manifest_path = cache.cached_manifest_path(spec)
|
|
1799
|
+
FS.mkdir_p(File.dirname(spec_path))
|
|
1800
|
+
|
|
1801
|
+
FS.atomic_write(spec_path, spec_payload) if spec_payload
|
|
1802
|
+
Scint::Cache::Manifest.write(manifest_path, manifest)
|
|
1803
|
+
end
|
|
1804
|
+
|
|
1805
|
+
def build_cached_manifest(spec, cache, gem_dir, extensions:)
|
|
1806
|
+
Scint::Cache::Manifest.build(
|
|
1807
|
+
spec: spec,
|
|
1808
|
+
gem_dir: gem_dir,
|
|
1809
|
+
abi_key: Platform.abi_key,
|
|
1810
|
+
source: manifest_source_for(spec),
|
|
1811
|
+
extensions: extensions,
|
|
1812
|
+
)
|
|
1813
|
+
end
|
|
1814
|
+
|
|
1815
|
+
def manifest_source_for(spec)
|
|
1816
|
+
source = spec.source
|
|
1817
|
+
if source.is_a?(Source::Git)
|
|
1818
|
+
{
|
|
1819
|
+
"type" => "git",
|
|
1820
|
+
"uri" => source.uri.to_s,
|
|
1821
|
+
"revision" => source.revision || source.ref || source.branch || source.tag,
|
|
1822
|
+
}.compact
|
|
1823
|
+
elsif source.is_a?(Source::Path)
|
|
1824
|
+
{
|
|
1825
|
+
"type" => "path",
|
|
1826
|
+
"path" => File.expand_path(source.path.to_s),
|
|
1827
|
+
"uri" => source.path.to_s,
|
|
1828
|
+
}
|
|
1829
|
+
else
|
|
1830
|
+
source_str = source.to_s
|
|
1831
|
+
if source_str.start_with?("http://", "https://")
|
|
1832
|
+
{ "type" => "rubygems", "uri" => source_str }
|
|
1833
|
+
elsif path_source?(source)
|
|
1834
|
+
{ "type" => "path", "path" => File.expand_path(source_str), "uri" => source_str }
|
|
1835
|
+
else
|
|
1836
|
+
{ "type" => "rubygems", "uri" => source_str }
|
|
1837
|
+
end
|
|
1838
|
+
end
|
|
1839
|
+
end
|
|
1840
|
+
|
|
1558
1841
|
# --- Lockfile + runtime config ---
|
|
1559
1842
|
|
|
1560
1843
|
def write_lockfile(resolved, gemfile, lockfile = nil)
|
|
@@ -1963,10 +2246,10 @@ module Scint
|
|
|
1963
2246
|
def read_require_paths(spec_file)
|
|
1964
2247
|
return ["lib"] unless File.exist?(spec_file)
|
|
1965
2248
|
|
|
1966
|
-
gemspec =
|
|
2249
|
+
gemspec = SpecUtils.load_gemspec(spec_file)
|
|
1967
2250
|
paths = Array(gemspec&.require_paths).reject(&:empty?)
|
|
1968
2251
|
paths.empty? ? ["lib"] : paths
|
|
1969
|
-
rescue StandardError
|
|
2252
|
+
rescue SystemExit, StandardError
|
|
1970
2253
|
["lib"]
|
|
1971
2254
|
end
|
|
1972
2255
|
|
|
@@ -2020,8 +2303,12 @@ module Scint
|
|
|
2020
2303
|
|
|
2021
2304
|
# Global cache artifacts.
|
|
2022
2305
|
FileUtils.rm_f(cache.inbound_path(spec))
|
|
2023
|
-
FileUtils.rm_rf(cache.
|
|
2306
|
+
FileUtils.rm_rf(cache.assembling_path(spec))
|
|
2307
|
+
FileUtils.rm_rf(cache.cached_path(spec))
|
|
2308
|
+
FileUtils.rm_f(cache.cached_spec_path(spec))
|
|
2309
|
+
FileUtils.rm_f(cache.cached_manifest_path(spec))
|
|
2024
2310
|
FileUtils.rm_f(cache.spec_cache_path(spec))
|
|
2311
|
+
FileUtils.rm_rf(cache.extracted_path(spec))
|
|
2025
2312
|
FileUtils.rm_rf(cache.ext_path(spec))
|
|
2026
2313
|
|
|
2027
2314
|
# Local bundle artifacts.
|
|
@@ -2056,17 +2343,17 @@ module Scint
|
|
|
2056
2343
|
return if (headers.nil? || headers.empty?) && body.empty?
|
|
2057
2344
|
|
|
2058
2345
|
if headers && !headers.empty?
|
|
2059
|
-
|
|
2346
|
+
@output.puts " headers:"
|
|
2060
2347
|
headers.sort.each do |key, value|
|
|
2061
|
-
|
|
2348
|
+
@output.puts " #{key}: #{value}"
|
|
2062
2349
|
end
|
|
2063
2350
|
end
|
|
2064
2351
|
|
|
2065
2352
|
return if body.empty?
|
|
2066
2353
|
|
|
2067
|
-
|
|
2354
|
+
@output.puts " body:"
|
|
2068
2355
|
body.each_line do |line|
|
|
2069
|
-
|
|
2356
|
+
@output.puts " #{line.rstrip}"
|
|
2070
2357
|
end
|
|
2071
2358
|
end
|
|
2072
2359
|
|
|
@@ -2075,7 +2362,7 @@ module Scint
|
|
|
2075
2362
|
return unless File.file?(path)
|
|
2076
2363
|
return if gitignore_has_bundle_entry?(path)
|
|
2077
2364
|
|
|
2078
|
-
|
|
2365
|
+
@output.puts "#{YELLOW}Warning: .gitignore exists but does not ignore .bundle (add `.bundle/`).#{RESET}"
|
|
2079
2366
|
end
|
|
2080
2367
|
|
|
2081
2368
|
def gitignore_has_bundle_entry?(path)
|
|
@@ -2111,6 +2398,12 @@ module Scint
|
|
|
2111
2398
|
when "--path"
|
|
2112
2399
|
@path = @argv[i + 1]
|
|
2113
2400
|
i += 2
|
|
2401
|
+
when "--without"
|
|
2402
|
+
@without_groups = @argv[i + 1]&.split(/[\s:,]+/)&.map(&:to_sym) || []
|
|
2403
|
+
i += 2
|
|
2404
|
+
when "--with"
|
|
2405
|
+
@with_groups = @argv[i + 1]&.split(/[\s:,]+/)&.map(&:to_sym) || []
|
|
2406
|
+
i += 2
|
|
2114
2407
|
when "--verbose"
|
|
2115
2408
|
@verbose = true
|
|
2116
2409
|
i += 1
|
|
@@ -2121,6 +2414,32 @@ module Scint
|
|
|
2121
2414
|
i += 1
|
|
2122
2415
|
end
|
|
2123
2416
|
end
|
|
2417
|
+
|
|
2418
|
+
# Also read BUNDLE_WITHOUT / BUNDLE_WITH env vars (Bundler compat)
|
|
2419
|
+
if !@without_groups && ENV["BUNDLE_WITHOUT"]
|
|
2420
|
+
@without_groups = ENV["BUNDLE_WITHOUT"].split(/[\s:,]+/).map(&:to_sym)
|
|
2421
|
+
end
|
|
2422
|
+
if !@with_groups && ENV["BUNDLE_WITH"]
|
|
2423
|
+
@with_groups = ENV["BUNDLE_WITH"].split(/[\s:,]+/).map(&:to_sym)
|
|
2424
|
+
end
|
|
2425
|
+
|
|
2426
|
+
# Read from .bundle/config if present
|
|
2427
|
+
load_bundle_config_groups if !@without_groups && !@with_groups
|
|
2428
|
+
end
|
|
2429
|
+
|
|
2430
|
+
def load_bundle_config_groups
|
|
2431
|
+
config_path = File.join(".bundle", "config")
|
|
2432
|
+
return unless File.exist?(config_path)
|
|
2433
|
+
|
|
2434
|
+
config = YAML.safe_load(File.read(config_path)) rescue nil
|
|
2435
|
+
return unless config.is_a?(Hash)
|
|
2436
|
+
|
|
2437
|
+
if config["BUNDLE_WITHOUT"] && !@without_groups
|
|
2438
|
+
@without_groups = config["BUNDLE_WITHOUT"].to_s.split(/[\s:]+/).map(&:to_sym)
|
|
2439
|
+
end
|
|
2440
|
+
if config["BUNDLE_WITH"] && !@with_groups
|
|
2441
|
+
@with_groups = config["BUNDLE_WITH"].to_s.split(/[\s:]+/).map(&:to_sym)
|
|
2442
|
+
end
|
|
2124
2443
|
end
|
|
2125
2444
|
end
|
|
2126
2445
|
end
|