scint 0.7.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 +71 -0
- data/VERSION +1 -1
- data/lib/scint/cache/layout.rb +61 -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 +553 -175
- 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/installer/extension_builder.rb +29 -34
- 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 +75 -0
- 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,29 +43,43 @@ 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)
|
|
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
|
|
50
54
|
@download_pool = nil
|
|
51
55
|
@download_pool_lock = Thread::Mutex.new
|
|
52
56
|
@gemspec_cache = {}
|
|
53
57
|
@gemspec_cache_lock = Thread::Mutex.new
|
|
54
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
|
|
55
68
|
end
|
|
56
69
|
|
|
57
70
|
def run
|
|
58
71
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
72
|
+
_t = start_time
|
|
59
73
|
|
|
60
74
|
cache = Scint::Cache::Layout.new
|
|
75
|
+
cache_telemetry = Scint::Cache::Telemetry.new
|
|
61
76
|
bundle_path = @path || ENV["BUNDLER_PATH"] || ".bundle"
|
|
62
77
|
bundle_display = display_bundle_path(bundle_path)
|
|
63
78
|
bundle_path = File.expand_path(bundle_path)
|
|
64
|
-
worker_count = @jobs || [Platform.cpu_count * 2, 50].min
|
|
79
|
+
worker_count = [(@jobs || [Platform.cpu_count * 2, 50].min).to_i, 1].max
|
|
65
80
|
compile_slots = compile_slots_for(worker_count)
|
|
66
|
-
|
|
81
|
+
git_slots = git_slots_for(worker_count)
|
|
82
|
+
per_type_limits = install_task_limits(worker_count, compile_slots, git_slots)
|
|
67
83
|
$stdout.puts "#{GREEN}💎#{RESET} Scintellating Gemfile into #{BOLD}#{bundle_display}#{RESET} #{DIM}(scint #{VERSION}, ruby #{RUBY_VERSION})#{RESET}"
|
|
68
84
|
$stdout.puts
|
|
69
85
|
|
|
@@ -75,6 +91,7 @@ module Scint
|
|
|
75
91
|
scheduler.start
|
|
76
92
|
|
|
77
93
|
begin
|
|
94
|
+
_t = _tmark("startup", _t)
|
|
78
95
|
# 2. Parse Gemfile
|
|
79
96
|
gemfile = Scint::Gemfile::Parser.parse("Gemfile")
|
|
80
97
|
|
|
@@ -86,6 +103,7 @@ module Scint
|
|
|
86
103
|
dep_count = gemfile.dependencies.size
|
|
87
104
|
scheduler.scale_workers(dep_count)
|
|
88
105
|
|
|
106
|
+
_t = _tmark("parse_gemfile", _t)
|
|
89
107
|
# 3. Enqueue index fetches for all sources immediately
|
|
90
108
|
gemfile.sources.each do |source|
|
|
91
109
|
scheduler.enqueue(:fetch_index, source[:uri] || source.to_s,
|
|
@@ -106,20 +124,26 @@ module Scint
|
|
|
106
124
|
-> { clone_git_source(source, cache) })
|
|
107
125
|
end
|
|
108
126
|
|
|
127
|
+
_t = _tmark("enqueue_fetches", _t)
|
|
109
128
|
# 6. Wait for index fetches, then resolve
|
|
110
129
|
scheduler.wait_for(:fetch_index)
|
|
130
|
+
_t = _tmark("wait_index", _t)
|
|
111
131
|
scheduler.wait_for(:git_clone)
|
|
132
|
+
_t = _tmark("wait_git", _t)
|
|
112
133
|
|
|
113
134
|
resolved = resolve(gemfile, lockfile, cache)
|
|
114
135
|
resolved = dedupe_resolved_specs(adjust_meta_gems(resolved))
|
|
136
|
+
resolved = filter_excluded_gems(resolved, gemfile)
|
|
115
137
|
force_purge_artifacts(resolved, bundle_path, cache) if @force
|
|
116
138
|
|
|
139
|
+
_t = _tmark("resolve", _t)
|
|
117
140
|
# 7. Plan: diff resolved vs installed
|
|
118
|
-
plan = Installer::Planner.plan(resolved, bundle_path, cache)
|
|
141
|
+
plan = Installer::Planner.plan(resolved, bundle_path, cache, telemetry: cache_telemetry)
|
|
119
142
|
total_gems = resolved.size
|
|
120
143
|
updated_gems = plan.count { |e| e.action != :skip }
|
|
121
144
|
cached_gems = total_gems - updated_gems
|
|
122
145
|
to_install = plan.reject { |e| e.action == :skip }
|
|
146
|
+
_t = _tmark("plan", _t)
|
|
123
147
|
|
|
124
148
|
# Scale up for download/install phase based on actual work count
|
|
125
149
|
scheduler.scale_workers(to_install.size)
|
|
@@ -127,6 +151,7 @@ module Scint
|
|
|
127
151
|
# Warm-cache accelerator: pre-materialize cache-backed gem trees in
|
|
128
152
|
# batches so install workers avoid one cp process per gem.
|
|
129
153
|
bulk_prelink_gem_files(to_install, cache, bundle_path)
|
|
154
|
+
_t = _tmark("prelink", _t)
|
|
130
155
|
|
|
131
156
|
if to_install.empty?
|
|
132
157
|
# Keep lock artifacts aligned even when everything is already installed.
|
|
@@ -192,9 +217,13 @@ module Scint
|
|
|
192
217
|
end
|
|
193
218
|
ensure
|
|
194
219
|
begin
|
|
195
|
-
|
|
220
|
+
cache_telemetry.warn_if_needed(cache_root: cache.root)
|
|
196
221
|
ensure
|
|
197
|
-
|
|
222
|
+
begin
|
|
223
|
+
scheduler.shutdown
|
|
224
|
+
ensure
|
|
225
|
+
close_download_pool
|
|
226
|
+
end
|
|
198
227
|
end
|
|
199
228
|
end
|
|
200
229
|
end
|
|
@@ -233,6 +262,91 @@ module Scint
|
|
|
233
262
|
seen.values
|
|
234
263
|
end
|
|
235
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
|
+
|
|
236
350
|
# Install scint into the bundle by copying our own lib tree.
|
|
237
351
|
# No download needed — we know exactly where we are.
|
|
238
352
|
def install_builtin_gem(entry, bundle_path)
|
|
@@ -429,9 +543,9 @@ module Scint
|
|
|
429
543
|
candidates.each do |gs|
|
|
430
544
|
next unless File.exist?(gs)
|
|
431
545
|
begin
|
|
432
|
-
spec =
|
|
546
|
+
spec = SpecUtils.load_gemspec(gs, isolate: true)
|
|
433
547
|
return spec if spec
|
|
434
|
-
rescue StandardError
|
|
548
|
+
rescue SystemExit, StandardError
|
|
435
549
|
nil
|
|
436
550
|
end
|
|
437
551
|
end
|
|
@@ -697,10 +811,8 @@ module Scint
|
|
|
697
811
|
absolute_gemspec = File.join(worktree, gemspec_path)
|
|
698
812
|
return nil unless File.exist?(absolute_gemspec)
|
|
699
813
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
end
|
|
703
|
-
rescue StandardError
|
|
814
|
+
SpecUtils.load_gemspec(absolute_gemspec, isolate: true)
|
|
815
|
+
rescue SystemExit, StandardError
|
|
704
816
|
nil
|
|
705
817
|
end
|
|
706
818
|
|
|
@@ -786,7 +898,7 @@ module Scint
|
|
|
786
898
|
spec = entry.spec
|
|
787
899
|
source = spec.source
|
|
788
900
|
if git_source?(source)
|
|
789
|
-
|
|
901
|
+
ensure_git_repo_for_spec(spec, cache, fetch: true)
|
|
790
902
|
return
|
|
791
903
|
end
|
|
792
904
|
source_uri = source.to_s
|
|
@@ -827,21 +939,34 @@ module Scint
|
|
|
827
939
|
# Git gems are extracted from the cached checkout; path gems are
|
|
828
940
|
# linked directly from local source.
|
|
829
941
|
if git_source?(spec.source)
|
|
830
|
-
|
|
942
|
+
assemble_git_spec(entry, cache, fetch: false)
|
|
831
943
|
return
|
|
832
944
|
end
|
|
833
945
|
return if source_uri.start_with?("/") || !source_uri.start_with?("http")
|
|
834
946
|
|
|
835
|
-
|
|
836
|
-
return if Dir.exist?(extracted)
|
|
947
|
+
return if Cache::Validity.cached_valid?(spec, cache)
|
|
837
948
|
|
|
838
949
|
dest_path = cache.inbound_path(spec)
|
|
839
950
|
raise InstallError, "Missing cached gem file for #{spec.name}: #{dest_path}" unless File.exist?(dest_path)
|
|
840
951
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
|
845
970
|
end
|
|
846
971
|
|
|
847
972
|
def git_source?(source)
|
|
@@ -870,16 +995,12 @@ module Scint
|
|
|
870
995
|
def prepare_git_source(entry, cache)
|
|
871
996
|
# Legacy helper used by tests/callers that expect git download+extract
|
|
872
997
|
# 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)
|
|
998
|
+
assemble_git_spec(entry, cache, fetch: true)
|
|
876
999
|
end
|
|
877
1000
|
|
|
878
|
-
def
|
|
1001
|
+
def ensure_git_repo_for_spec(spec, cache, fetch:)
|
|
879
1002
|
source = spec.source
|
|
880
|
-
uri,
|
|
881
|
-
submodules = git_source_submodules?(source)
|
|
882
|
-
|
|
1003
|
+
uri, _revision = git_source_ref(source)
|
|
883
1004
|
bare_repo = cache.git_path(uri)
|
|
884
1005
|
|
|
885
1006
|
# Serialize all git operations per bare repo — git uses index.lock
|
|
@@ -891,92 +1012,100 @@ module Scint
|
|
|
891
1012
|
clone_git_repo(uri, bare_repo)
|
|
892
1013
|
fetch_git_repo(bare_repo)
|
|
893
1014
|
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
1015
|
end
|
|
1016
|
+
|
|
1017
|
+
bare_repo
|
|
907
1018
|
end
|
|
908
1019
|
|
|
909
|
-
def
|
|
1020
|
+
def assemble_git_spec(entry, cache, fetch: true)
|
|
910
1021
|
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
|
|
1022
|
+
return if Cache::Validity.cached_valid?(spec, cache)
|
|
921
1023
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
FS.clone_tree(gem_root, tmp)
|
|
1024
|
+
source = spec.source
|
|
1025
|
+
uri, revision = git_source_ref(source)
|
|
1026
|
+
submodules = git_source_submodules?(source)
|
|
926
1027
|
|
|
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
|
|
1028
|
+
bare_repo = cache.git_path(uri)
|
|
934
1029
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1030
|
+
# Serialize all git operations per bare repo — git uses index.lock
|
|
1031
|
+
# and can't handle concurrent checkouts from the same repo.
|
|
1032
|
+
git_mutex_for(bare_repo).synchronize do
|
|
1033
|
+
tmp_checkout = nil
|
|
1034
|
+
tmp_assembled = nil
|
|
940
1035
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
|
947
1043
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
953
1076
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
|
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
|
|
974
1082
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
|
1098
|
+
end
|
|
1099
|
+
gemspec = read_gemspec_from_extracted(gem_subdir, spec)
|
|
1100
|
+
cache_gemspec(spec, gemspec, cache) if gemspec
|
|
1101
|
+
|
|
1102
|
+
unless Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
1103
|
+
promote_assembled_gem(spec, cache, assembling, gemspec, extensions: false)
|
|
1104
|
+
end
|
|
1105
|
+
ensure
|
|
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)
|
|
1108
|
+
end
|
|
980
1109
|
end
|
|
981
1110
|
end
|
|
982
1111
|
|
|
@@ -993,6 +1122,55 @@ module Scint
|
|
|
993
1122
|
source.respond_to?(:submodules) && !!source.submodules
|
|
994
1123
|
end
|
|
995
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
|
+
|
|
996
1174
|
def checkout_git_tree(bare_repo, destination, resolved_revision, spec, uri)
|
|
997
1175
|
FileUtils.mkdir_p(destination)
|
|
998
1176
|
_out, err, status = git_capture3(
|
|
@@ -1093,64 +1271,59 @@ module Scint
|
|
|
1093
1271
|
Open3.capture3("git", "-c", "core.fsmonitor=false", *args)
|
|
1094
1272
|
end
|
|
1095
1273
|
|
|
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
|
|
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
|
|
1119
1282
|
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
|
1123
1288
|
end
|
|
1124
1289
|
|
|
1125
|
-
def
|
|
1126
|
-
|
|
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
|
|
1127
1295
|
end
|
|
1128
1296
|
|
|
1129
|
-
def
|
|
1130
|
-
# Keep compile parallelism conservative: at most 2 native builds.
|
|
1131
|
-
# Small pools stay single-lane; larger pools can run two builds.
|
|
1132
|
-
workers = worker_count.to_i
|
|
1133
|
-
return 1 if workers <= 6
|
|
1134
|
-
|
|
1135
|
-
2
|
|
1136
|
-
end
|
|
1137
|
-
|
|
1138
|
-
def install_task_limits(worker_count, compile_slots)
|
|
1297
|
+
def install_task_limits(worker_count, compile_slots, git_slots = worker_count)
|
|
1139
1298
|
# Leave headroom for compile and binstub lanes so link/download
|
|
1140
1299
|
# throughput cannot fully starve them.
|
|
1141
|
-
|
|
1300
|
+
workers = [worker_count.to_i, 1].max
|
|
1301
|
+
io_cpu_limit = [workers - compile_slots - 1, 1].max
|
|
1142
1302
|
# Keep download in-flight set bounded so fail-fast exits quickly on
|
|
1143
1303
|
# auth/source errors instead of queueing a large burst.
|
|
1144
1304
|
download_limit = [io_cpu_limit, 8].min
|
|
1305
|
+
git_limit = [[git_slots.to_i, 1].max, workers].min
|
|
1145
1306
|
{
|
|
1146
1307
|
download: download_limit,
|
|
1147
1308
|
extract: io_cpu_limit,
|
|
1148
1309
|
link: io_cpu_limit,
|
|
1310
|
+
git_clone: git_limit,
|
|
1149
1311
|
build_ext: compile_slots,
|
|
1150
1312
|
binstub: 1,
|
|
1151
1313
|
}
|
|
1152
1314
|
end
|
|
1153
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
|
+
|
|
1154
1327
|
def display_bundle_path(path)
|
|
1155
1328
|
return path if path.start_with?("/", "./", "../")
|
|
1156
1329
|
|
|
@@ -1297,10 +1470,21 @@ module Scint
|
|
|
1297
1470
|
source_str
|
|
1298
1471
|
end
|
|
1299
1472
|
else
|
|
1300
|
-
|
|
1301
|
-
|
|
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)
|
|
1302
1486
|
resolve_git_gem_subdir(base, entry.spec)
|
|
1303
|
-
elsif path_source?(entry.spec.source) && Dir.exist?(base)
|
|
1487
|
+
elsif path_source?(entry.spec.source) && base && Dir.exist?(base)
|
|
1304
1488
|
begin
|
|
1305
1489
|
resolve_path_gem_subdir(base, entry.spec)
|
|
1306
1490
|
rescue InstallError
|
|
@@ -1371,19 +1555,18 @@ module Scint
|
|
|
1371
1555
|
from_cache: true,
|
|
1372
1556
|
)
|
|
1373
1557
|
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
1558
|
end
|
|
1378
1559
|
|
|
1379
1560
|
def build_extensions(entry, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
1561
|
+
spec = entry.spec
|
|
1380
1562
|
extracted = extracted_path_for_entry(entry, cache)
|
|
1381
|
-
gemspec = load_gemspec(extracted,
|
|
1563
|
+
gemspec = load_gemspec(extracted, spec, cache)
|
|
1564
|
+
promote_after_build = assembling_path?(extracted, cache)
|
|
1382
1565
|
|
|
1383
|
-
sync_build_env_dependencies(
|
|
1566
|
+
sync_build_env_dependencies(spec, bundle_path, cache)
|
|
1384
1567
|
|
|
1385
1568
|
prepared = PreparedGem.new(
|
|
1386
|
-
spec:
|
|
1569
|
+
spec: spec,
|
|
1387
1570
|
extracted_path: extracted,
|
|
1388
1571
|
gemspec: gemspec,
|
|
1389
1572
|
from_cache: true,
|
|
@@ -1394,8 +1577,24 @@ module Scint
|
|
|
1394
1577
|
bundle_path,
|
|
1395
1578
|
cache,
|
|
1396
1579
|
compile_slots: compile_slots,
|
|
1397
|
-
output_tail: ->(lines) { progress&.on_build_tail(
|
|
1580
|
+
output_tail: ->(lines) { progress&.on_build_tail(spec.name, lines) },
|
|
1398
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
|
|
1399
1598
|
end
|
|
1400
1599
|
|
|
1401
1600
|
def sync_build_env_dependencies(spec, bundle_path, cache)
|
|
@@ -1460,6 +1659,12 @@ module Scint
|
|
|
1460
1659
|
return cached
|
|
1461
1660
|
end
|
|
1462
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
|
|
1667
|
+
|
|
1463
1668
|
inbound = cache.inbound_path(spec)
|
|
1464
1669
|
return nil unless File.exist?(inbound)
|
|
1465
1670
|
|
|
@@ -1473,49 +1678,90 @@ module Scint
|
|
|
1473
1678
|
end
|
|
1474
1679
|
end
|
|
1475
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
|
+
|
|
1476
1706
|
def bulk_prelink_gem_files(entries, cache, bundle_path)
|
|
1477
|
-
# Keep small installs simple; batching is for large warm-path runs.
|
|
1478
1707
|
return if entries.length < 32
|
|
1479
1708
|
|
|
1480
1709
|
ruby_dir = File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
1481
1710
|
gems_dir = File.join(ruby_dir, "gems")
|
|
1711
|
+
cache_abi_dir = cache.cached_abi_dir
|
|
1482
1712
|
|
|
1483
|
-
|
|
1713
|
+
gem_names = []
|
|
1714
|
+
entries.each do |entry|
|
|
1484
1715
|
next unless entry.action == :link || entry.action == :build_ext
|
|
1485
1716
|
|
|
1486
|
-
|
|
1717
|
+
source_dir = entry.cached_path
|
|
1718
|
+
next unless source_dir
|
|
1719
|
+
|
|
1487
1720
|
full_name = cache.full_name(entry.spec)
|
|
1488
|
-
next unless File.basename(
|
|
1489
|
-
next unless Dir.exist?(
|
|
1721
|
+
next unless File.basename(source_dir) == full_name
|
|
1722
|
+
next unless Dir.exist?(source_dir)
|
|
1490
1723
|
next if Dir.exist?(File.join(gems_dir, full_name))
|
|
1491
1724
|
|
|
1492
|
-
|
|
1725
|
+
gem_names << full_name
|
|
1493
1726
|
end
|
|
1494
|
-
return if sources.empty?
|
|
1495
1727
|
|
|
1496
|
-
|
|
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)
|
|
1497
1735
|
rescue StandardError => e
|
|
1498
1736
|
$stderr.puts("bulk prelink warning: #{e.message}") if ENV["SCINT_DEBUG"]
|
|
1499
1737
|
end
|
|
1500
1738
|
|
|
1501
1739
|
def load_cached_gemspec(spec, cache, extracted_path)
|
|
1502
|
-
|
|
1503
|
-
return nil unless File.exist?(path)
|
|
1740
|
+
paths = [cache.cached_spec_path(spec)]
|
|
1504
1741
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
|
1512
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
|
|
1513
1759
|
end
|
|
1760
|
+
return gemspec if cached_gemspec_valid?(gemspec, extracted_path)
|
|
1514
1761
|
end
|
|
1515
|
-
return gemspec if cached_gemspec_valid?(gemspec, extracted_path)
|
|
1516
1762
|
|
|
1517
1763
|
nil
|
|
1518
|
-
rescue StandardError
|
|
1764
|
+
rescue SystemExit, StandardError
|
|
1519
1765
|
nil
|
|
1520
1766
|
end
|
|
1521
1767
|
|
|
@@ -1549,12 +1795,108 @@ module Scint
|
|
|
1549
1795
|
end
|
|
1550
1796
|
|
|
1551
1797
|
def cache_gemspec(spec, gemspec, cache)
|
|
1552
|
-
path = cache.
|
|
1553
|
-
|
|
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)
|
|
1554
1805
|
rescue StandardError
|
|
1555
1806
|
# Non-fatal: we'll read metadata from .gem next time.
|
|
1556
1807
|
end
|
|
1557
1808
|
|
|
1809
|
+
def cache_promoter(cache)
|
|
1810
|
+
@cache_promoter ||= Installer::Promoter.new(root: cache.root)
|
|
1811
|
+
end
|
|
1812
|
+
|
|
1813
|
+
def assembling_path?(path, cache)
|
|
1814
|
+
return false if path.nil? || path.empty?
|
|
1815
|
+
|
|
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,
|
|
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)
|
|
1847
|
+
end
|
|
1848
|
+
result
|
|
1849
|
+
rescue StandardError
|
|
1850
|
+
FileUtils.rm_rf(cached_dir) if Dir.exist?(cached_dir)
|
|
1851
|
+
raise
|
|
1852
|
+
end
|
|
1853
|
+
end
|
|
1854
|
+
|
|
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))
|
|
1859
|
+
|
|
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
|
|
1873
|
+
|
|
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
|
|
1897
|
+
end
|
|
1898
|
+
end
|
|
1899
|
+
|
|
1558
1900
|
# --- Lockfile + runtime config ---
|
|
1559
1901
|
|
|
1560
1902
|
def write_lockfile(resolved, gemfile, lockfile = nil)
|
|
@@ -1963,10 +2305,10 @@ module Scint
|
|
|
1963
2305
|
def read_require_paths(spec_file)
|
|
1964
2306
|
return ["lib"] unless File.exist?(spec_file)
|
|
1965
2307
|
|
|
1966
|
-
gemspec =
|
|
2308
|
+
gemspec = SpecUtils.load_gemspec(spec_file)
|
|
1967
2309
|
paths = Array(gemspec&.require_paths).reject(&:empty?)
|
|
1968
2310
|
paths.empty? ? ["lib"] : paths
|
|
1969
|
-
rescue StandardError
|
|
2311
|
+
rescue SystemExit, StandardError
|
|
1970
2312
|
["lib"]
|
|
1971
2313
|
end
|
|
1972
2314
|
|
|
@@ -2020,8 +2362,12 @@ module Scint
|
|
|
2020
2362
|
|
|
2021
2363
|
# Global cache artifacts.
|
|
2022
2364
|
FileUtils.rm_f(cache.inbound_path(spec))
|
|
2023
|
-
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))
|
|
2024
2369
|
FileUtils.rm_f(cache.spec_cache_path(spec))
|
|
2370
|
+
FileUtils.rm_rf(cache.extracted_path(spec))
|
|
2025
2371
|
FileUtils.rm_rf(cache.ext_path(spec))
|
|
2026
2372
|
|
|
2027
2373
|
# Local bundle artifacts.
|
|
@@ -2111,6 +2457,12 @@ module Scint
|
|
|
2111
2457
|
when "--path"
|
|
2112
2458
|
@path = @argv[i + 1]
|
|
2113
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
|
|
2114
2466
|
when "--verbose"
|
|
2115
2467
|
@verbose = true
|
|
2116
2468
|
i += 1
|
|
@@ -2121,6 +2473,32 @@ module Scint
|
|
|
2121
2473
|
i += 1
|
|
2122
2474
|
end
|
|
2123
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
|
|
2124
2502
|
end
|
|
2125
2503
|
end
|
|
2126
2504
|
end
|