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/cache/prewarm.rb
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "open3"
|
|
4
5
|
require_relative "layout"
|
|
6
|
+
require_relative "manifest"
|
|
7
|
+
require_relative "validity"
|
|
5
8
|
require_relative "../errors"
|
|
6
9
|
require_relative "../downloader/pool"
|
|
7
10
|
require_relative "../gem/package"
|
|
8
11
|
require_relative "../fs"
|
|
9
12
|
require_relative "../platform"
|
|
10
13
|
require_relative "../worker_pool"
|
|
14
|
+
require_relative "../installer/extension_builder"
|
|
15
|
+
require_relative "../installer/promoter"
|
|
16
|
+
require_relative "../source/git"
|
|
17
|
+
require_relative "../source/path"
|
|
11
18
|
|
|
12
19
|
module Scint
|
|
13
20
|
module Cache
|
|
@@ -23,6 +30,8 @@ module Scint
|
|
|
23
30
|
@downloader_factory = downloader_factory || lambda { |size, creds|
|
|
24
31
|
Downloader::Pool.new(size: size, credentials: creds)
|
|
25
32
|
}
|
|
33
|
+
@git_mutexes = {}
|
|
34
|
+
@git_mutexes_lock = Thread::Mutex.new
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
# Returns summary hash:
|
|
@@ -32,65 +41,99 @@ module Scint
|
|
|
32
41
|
ignored = 0
|
|
33
42
|
skipped = 0
|
|
34
43
|
|
|
35
|
-
|
|
44
|
+
gem_tasks = []
|
|
45
|
+
git_tasks = []
|
|
46
|
+
|
|
36
47
|
specs.each do |spec|
|
|
37
|
-
if
|
|
38
|
-
|
|
48
|
+
if git_source?(spec.source)
|
|
49
|
+
git_tasks << task_for_git(spec)
|
|
50
|
+
elsif rubygems_source?(spec.source)
|
|
51
|
+
gem_tasks << task_for(spec)
|
|
39
52
|
else
|
|
40
53
|
ignored += 1
|
|
41
54
|
end
|
|
42
55
|
end
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
next unless @force
|
|
57
|
+
all_tasks = gem_tasks + git_tasks
|
|
46
58
|
|
|
59
|
+
all_tasks.each do |task|
|
|
60
|
+
next unless @force
|
|
47
61
|
purge_artifacts(task.spec)
|
|
48
62
|
task.download = true
|
|
49
63
|
task.extract = true
|
|
50
64
|
end
|
|
51
65
|
|
|
52
|
-
|
|
53
|
-
if !task.download && !task.extract
|
|
54
|
-
skipped += 1
|
|
55
|
-
end
|
|
66
|
+
all_tasks.each do |task|
|
|
67
|
+
skipped += 1 if !task.download && !task.extract
|
|
56
68
|
end
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
work_gem_tasks = gem_tasks.select { |t| t.download || t.extract }
|
|
71
|
+
work_git_tasks = git_tasks.select { |t| t.download || t.extract }
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
return result_hash(0, skipped, ignored, failures) if work_gem_tasks.empty? && work_git_tasks.empty?
|
|
74
|
+
|
|
75
|
+
# Phase 1: Fetch — download .gem files + clone/fetch git repos
|
|
76
|
+
download_errors = download_tasks(work_gem_tasks.select(&:download))
|
|
62
77
|
failures.concat(download_errors)
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
end
|
|
79
|
+
git_fetch_errors = git_fetch_tasks(work_git_tasks.select(&:download))
|
|
80
|
+
failures.concat(git_fetch_errors)
|
|
67
81
|
|
|
68
|
-
|
|
82
|
+
# Phase 2: Extract — expand .gem payloads + checkout/assemble git trees
|
|
83
|
+
remaining_gems = work_gem_tasks.reject { |t| failures.any? { |f| f[:spec] == t.spec } }
|
|
84
|
+
extract_errors = extract_tasks(remaining_gems.select(&:extract))
|
|
69
85
|
failures.concat(extract_errors)
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
remaining_gits = work_git_tasks.reject { |t| failures.any? { |f| f[:spec] == t.spec } }
|
|
88
|
+
git_assemble_errors = git_assemble_tasks(remaining_gits.select(&:extract))
|
|
89
|
+
failures.concat(git_assemble_errors)
|
|
90
|
+
|
|
91
|
+
total_work = work_gem_tasks.size + work_git_tasks.size
|
|
92
|
+
result_hash(total_work - failures.size, skipped, ignored, failures)
|
|
72
93
|
end
|
|
73
94
|
|
|
74
95
|
private
|
|
75
96
|
|
|
97
|
+
# -- Task classification -------------------------------------------------
|
|
98
|
+
|
|
99
|
+
def git_source?(source)
|
|
100
|
+
source.is_a?(Source::Git)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def rubygems_source?(source)
|
|
104
|
+
source_str = source.to_s
|
|
105
|
+
source_str.start_with?("http://", "https://")
|
|
106
|
+
end
|
|
107
|
+
|
|
76
108
|
def task_for(spec)
|
|
77
109
|
inbound = @cache.inbound_path(spec)
|
|
78
|
-
|
|
79
|
-
metadata = @cache.spec_cache_path(spec)
|
|
110
|
+
cached_valid = Cache::Validity.cached_valid?(spec, @cache)
|
|
80
111
|
|
|
81
112
|
Task.new(
|
|
82
113
|
spec: spec,
|
|
83
114
|
download: !File.exist?(inbound),
|
|
84
|
-
extract: !
|
|
115
|
+
extract: !cached_valid,
|
|
85
116
|
)
|
|
86
117
|
end
|
|
87
118
|
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
def task_for_git(spec)
|
|
120
|
+
cached_valid = Cache::Validity.cached_valid?(spec, @cache)
|
|
121
|
+
return Task.new(spec: spec, download: false, extract: false) if cached_valid
|
|
122
|
+
|
|
123
|
+
uri = spec.source.uri.to_s
|
|
124
|
+
bare_repo = @cache.git_path(uri)
|
|
125
|
+
|
|
126
|
+
Task.new(
|
|
127
|
+
spec: spec,
|
|
128
|
+
download: !Dir.exist?(bare_repo),
|
|
129
|
+
# Git gems always need assembly if not cached, even when the bare
|
|
130
|
+
# repo is already present (the checkout may be stale/missing).
|
|
131
|
+
extract: true,
|
|
132
|
+
)
|
|
92
133
|
end
|
|
93
134
|
|
|
135
|
+
# -- Rubygems download ---------------------------------------------------
|
|
136
|
+
|
|
94
137
|
def download_tasks(tasks)
|
|
95
138
|
return [] if tasks.empty?
|
|
96
139
|
|
|
@@ -114,6 +157,57 @@ module Scint
|
|
|
114
157
|
downloader&.close
|
|
115
158
|
end
|
|
116
159
|
|
|
160
|
+
# -- Git fetch (clone/fetch bare repos) ----------------------------------
|
|
161
|
+
|
|
162
|
+
def git_fetch_tasks(tasks)
|
|
163
|
+
return [] if tasks.empty?
|
|
164
|
+
|
|
165
|
+
failures = []
|
|
166
|
+
mutex = Thread::Mutex.new
|
|
167
|
+
done = Thread::Queue.new
|
|
168
|
+
|
|
169
|
+
# Deduplicate by URI so we only clone/fetch each repo once.
|
|
170
|
+
by_uri = {}
|
|
171
|
+
tasks.each do |task|
|
|
172
|
+
uri = task.spec.source.uri.to_s
|
|
173
|
+
by_uri[uri] ||= []
|
|
174
|
+
by_uri[uri] << task
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
pool = WorkerPool.new([@jobs, by_uri.size].min, name: "prewarm-git-fetch")
|
|
178
|
+
pool.start do |uri|
|
|
179
|
+
bare_repo = @cache.git_path(uri)
|
|
180
|
+
git_mutex_for(bare_repo).synchronize do
|
|
181
|
+
if Dir.exist?(bare_repo)
|
|
182
|
+
fetch_git_repo(bare_repo)
|
|
183
|
+
else
|
|
184
|
+
clone_git_repo(uri, bare_repo)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
true
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
by_uri.each do |uri, uri_tasks|
|
|
191
|
+
pool.enqueue(uri) do |job|
|
|
192
|
+
mutex.synchronize do
|
|
193
|
+
if job[:state] == :failed
|
|
194
|
+
uri_tasks.each do |task|
|
|
195
|
+
failures << { spec: task.spec, error: job[:error] }
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
done.push(true)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
by_uri.size.times { done.pop }
|
|
204
|
+
pool.stop
|
|
205
|
+
|
|
206
|
+
failures
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# -- Rubygems extract ----------------------------------------------------
|
|
210
|
+
|
|
117
211
|
def extract_tasks(tasks)
|
|
118
212
|
return [] if tasks.empty?
|
|
119
213
|
|
|
@@ -130,23 +224,58 @@ module Scint
|
|
|
130
224
|
raise CacheError, "Missing downloaded gem for #{spec.name}: #{inbound}"
|
|
131
225
|
end
|
|
132
226
|
|
|
133
|
-
|
|
134
|
-
|
|
227
|
+
assembling = @cache.assembling_path(spec)
|
|
228
|
+
tmp = "#{assembling}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
135
229
|
|
|
136
230
|
gemspec = nil
|
|
137
231
|
if task.extract
|
|
138
|
-
FileUtils.rm_rf(
|
|
139
|
-
|
|
140
|
-
|
|
232
|
+
FileUtils.rm_rf(assembling)
|
|
233
|
+
FileUtils.rm_rf(tmp)
|
|
234
|
+
FS.mkdir_p(File.dirname(assembling))
|
|
235
|
+
|
|
236
|
+
result = GemPkg::Package.new.extract(inbound, tmp)
|
|
141
237
|
gemspec = result[:gemspec]
|
|
142
|
-
|
|
143
|
-
gemspec = GemPkg::Package.new.read_metadata(inbound)
|
|
238
|
+
FS.atomic_move(tmp, assembling)
|
|
144
239
|
end
|
|
145
240
|
|
|
146
241
|
if gemspec
|
|
147
|
-
|
|
242
|
+
promote_assembled(spec, assembling, gemspec)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
true
|
|
246
|
+
ensure
|
|
247
|
+
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
tasks.each do |task|
|
|
251
|
+
pool.enqueue(task) do |job|
|
|
252
|
+
mutex.synchronize do
|
|
253
|
+
if job[:state] == :failed
|
|
254
|
+
failures << { spec: job[:payload].spec, error: job[:error] }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
done.push(true)
|
|
148
258
|
end
|
|
259
|
+
end
|
|
149
260
|
|
|
261
|
+
tasks.size.times { done.pop }
|
|
262
|
+
pool.stop
|
|
263
|
+
|
|
264
|
+
failures
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# -- Git assemble (checkout + promote) -----------------------------------
|
|
268
|
+
|
|
269
|
+
def git_assemble_tasks(tasks)
|
|
270
|
+
return [] if tasks.empty?
|
|
271
|
+
|
|
272
|
+
failures = []
|
|
273
|
+
mutex = Thread::Mutex.new
|
|
274
|
+
done = Thread::Queue.new
|
|
275
|
+
|
|
276
|
+
pool = WorkerPool.new(@jobs, name: "prewarm-git-assemble")
|
|
277
|
+
pool.start do |task|
|
|
278
|
+
assemble_git_spec(task.spec)
|
|
150
279
|
true
|
|
151
280
|
end
|
|
152
281
|
|
|
@@ -167,6 +296,279 @@ module Scint
|
|
|
167
296
|
failures
|
|
168
297
|
end
|
|
169
298
|
|
|
299
|
+
# Checkout a git source into the assembling cache and promote to cached.
|
|
300
|
+
# This mirrors CLI::Install#assemble_git_spec but without install/link.
|
|
301
|
+
def assemble_git_spec(spec)
|
|
302
|
+
return if Cache::Validity.cached_valid?(spec, @cache)
|
|
303
|
+
|
|
304
|
+
source = spec.source
|
|
305
|
+
uri = source.uri.to_s
|
|
306
|
+
revision = source.revision || source.ref || source.branch || source.tag || "HEAD"
|
|
307
|
+
submodules = source.respond_to?(:submodules) && !!source.submodules
|
|
308
|
+
|
|
309
|
+
bare_repo = @cache.git_path(uri)
|
|
310
|
+
raise CacheError, "Missing git repo for #{spec.name}: #{bare_repo}" unless Dir.exist?(bare_repo)
|
|
311
|
+
|
|
312
|
+
git_mutex_for(bare_repo).synchronize do
|
|
313
|
+
tmp_checkout = nil
|
|
314
|
+
tmp_assembled = nil
|
|
315
|
+
|
|
316
|
+
begin
|
|
317
|
+
resolved_revision = resolve_git_revision(bare_repo, revision)
|
|
318
|
+
assembling = @cache.assembling_path(spec)
|
|
319
|
+
tmp_checkout = "#{assembling}.checkout.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
320
|
+
tmp_assembled = "#{assembling}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
321
|
+
|
|
322
|
+
FileUtils.rm_rf(assembling)
|
|
323
|
+
FileUtils.rm_rf(tmp_checkout)
|
|
324
|
+
FileUtils.rm_rf(tmp_assembled)
|
|
325
|
+
FS.mkdir_p(File.dirname(assembling))
|
|
326
|
+
|
|
327
|
+
if submodules
|
|
328
|
+
checkout_git_tree_with_submodules(bare_repo, tmp_checkout, resolved_revision, spec, uri)
|
|
329
|
+
else
|
|
330
|
+
checkout_git_tree(bare_repo, tmp_checkout, resolved_revision, spec, uri)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Strip .git internals for deterministic cache content
|
|
334
|
+
Dir.glob(File.join(tmp_checkout, "**", ".git"), File::FNM_DOTMATCH).each do |path|
|
|
335
|
+
FileUtils.rm_rf(path)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
gem_root = resolve_git_gem_subdir(tmp_checkout, spec)
|
|
339
|
+
gem_rel = git_relative_root(tmp_checkout, gem_root)
|
|
340
|
+
dest_path = gem_rel.empty? ? tmp_assembled : File.join(tmp_assembled, gem_rel)
|
|
341
|
+
|
|
342
|
+
FS.clone_tree(gem_root, dest_path)
|
|
343
|
+
copy_gemspec_root_files(tmp_checkout, gem_root, tmp_assembled, spec)
|
|
344
|
+
FS.atomic_move(tmp_assembled, assembling)
|
|
345
|
+
|
|
346
|
+
gem_subdir = begin
|
|
347
|
+
resolve_git_gem_subdir(assembling, spec)
|
|
348
|
+
rescue InstallError
|
|
349
|
+
assembling
|
|
350
|
+
end
|
|
351
|
+
gemspec = read_gemspec_from_dir(gem_subdir, spec)
|
|
352
|
+
|
|
353
|
+
unless Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
354
|
+
promote_assembled(spec, assembling, gemspec)
|
|
355
|
+
end
|
|
356
|
+
ensure
|
|
357
|
+
FileUtils.rm_rf(tmp_checkout) if tmp_checkout && File.exist?(tmp_checkout)
|
|
358
|
+
FileUtils.rm_rf(tmp_assembled) if tmp_assembled && File.exist?(tmp_assembled)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# -- Promote / metadata --------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def promote_assembled(spec, assembling, gemspec)
|
|
366
|
+
return unless assembling && Dir.exist?(assembling)
|
|
367
|
+
|
|
368
|
+
cached_dir = @cache.cached_path(spec)
|
|
369
|
+
promoter = Installer::Promoter.new(root: @cache.root)
|
|
370
|
+
lock_key = "#{Platform.abi_key}-#{@cache.full_name(spec)}"
|
|
371
|
+
extensions = Installer::ExtensionBuilder.needs_build?(spec, assembling)
|
|
372
|
+
|
|
373
|
+
promoter.with_staging_dir(prefix: "cached") do |staging|
|
|
374
|
+
FS.clone_tree(assembling, staging)
|
|
375
|
+
manifest = Cache::Manifest.build(
|
|
376
|
+
spec: spec,
|
|
377
|
+
gem_dir: staging,
|
|
378
|
+
abi_key: Platform.abi_key,
|
|
379
|
+
source: manifest_source_for(spec),
|
|
380
|
+
extensions: extensions,
|
|
381
|
+
)
|
|
382
|
+
spec_payload = gemspec ? Marshal.dump(gemspec) : nil
|
|
383
|
+
result = promoter.promote_tree(
|
|
384
|
+
staging_path: staging,
|
|
385
|
+
target_path: cached_dir,
|
|
386
|
+
lock_key: lock_key,
|
|
387
|
+
)
|
|
388
|
+
write_cached_metadata(spec, spec_payload, manifest) if result == :promoted
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
FileUtils.rm_rf(assembling)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def manifest_source_for(spec)
|
|
395
|
+
source = spec.source
|
|
396
|
+
if source.is_a?(Source::Git)
|
|
397
|
+
{
|
|
398
|
+
"type" => "git",
|
|
399
|
+
"uri" => source.uri.to_s,
|
|
400
|
+
"revision" => source.revision || source.ref || source.branch || source.tag,
|
|
401
|
+
}.compact
|
|
402
|
+
else
|
|
403
|
+
{ "type" => "rubygems", "uri" => source.to_s }
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def write_cached_metadata(spec, spec_payload, manifest)
|
|
408
|
+
spec_path = @cache.cached_spec_path(spec)
|
|
409
|
+
manifest_path = @cache.cached_manifest_path(spec)
|
|
410
|
+
FS.mkdir_p(File.dirname(spec_path))
|
|
411
|
+
|
|
412
|
+
FS.atomic_write(spec_path, spec_payload) if spec_payload
|
|
413
|
+
Cache::Manifest.write(manifest_path, manifest)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# -- Git helpers (same logic as CLI::Install) ----------------------------
|
|
417
|
+
|
|
418
|
+
def clone_git_repo(uri, bare_repo)
|
|
419
|
+
FS.mkdir_p(File.dirname(bare_repo))
|
|
420
|
+
_out, err, status = git_capture3("clone", "--bare", uri.to_s, bare_repo)
|
|
421
|
+
unless status.success?
|
|
422
|
+
raise CacheError, "Git clone failed for #{uri}: #{err.to_s.strip}"
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def fetch_git_repo(bare_repo)
|
|
427
|
+
_out, err, status = git_capture3(
|
|
428
|
+
"--git-dir", bare_repo,
|
|
429
|
+
"fetch", "--prune", "origin",
|
|
430
|
+
"+refs/heads/*:refs/heads/*",
|
|
431
|
+
"+refs/tags/*:refs/tags/*",
|
|
432
|
+
)
|
|
433
|
+
unless status.success?
|
|
434
|
+
raise CacheError, "Git fetch failed for #{bare_repo}: #{err.to_s.strip}"
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def resolve_git_revision(bare_repo, revision)
|
|
439
|
+
out, err, status = git_capture3("--git-dir", bare_repo, "rev-parse", "#{revision}^{commit}")
|
|
440
|
+
unless status.success?
|
|
441
|
+
raise CacheError, "Unable to resolve git revision #{revision.inspect} in #{bare_repo}: #{err.to_s.strip}"
|
|
442
|
+
end
|
|
443
|
+
out.strip
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def checkout_git_tree(bare_repo, destination, resolved_revision, spec, uri)
|
|
447
|
+
FileUtils.mkdir_p(destination)
|
|
448
|
+
_out, err, status = git_capture3(
|
|
449
|
+
"--git-dir", bare_repo,
|
|
450
|
+
"--work-tree", destination,
|
|
451
|
+
"checkout", "-f", resolved_revision, "--", ".",
|
|
452
|
+
)
|
|
453
|
+
unless status.success?
|
|
454
|
+
raise CacheError, "Git checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def checkout_git_tree_with_submodules(bare_repo, destination, resolved_revision, spec, uri)
|
|
459
|
+
worktree = "#{destination}.worktree"
|
|
460
|
+
FileUtils.rm_rf(worktree)
|
|
461
|
+
|
|
462
|
+
_out, err, status = git_capture3(
|
|
463
|
+
"--git-dir", bare_repo,
|
|
464
|
+
"worktree", "add", "--detach", "--force", worktree, resolved_revision,
|
|
465
|
+
)
|
|
466
|
+
unless status.success?
|
|
467
|
+
raise CacheError, "Git worktree checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
begin
|
|
471
|
+
_sub_out, sub_err, sub_status = git_capture3(
|
|
472
|
+
"-C", worktree,
|
|
473
|
+
"-c", "protocol.file.allow=always",
|
|
474
|
+
"submodule", "update", "--init", "--recursive",
|
|
475
|
+
)
|
|
476
|
+
unless sub_status.success?
|
|
477
|
+
raise CacheError, "Git submodule update failed for #{spec.name} (#{uri}@#{resolved_revision}): #{sub_err.to_s.strip}"
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
FS.clone_tree(worktree, destination)
|
|
481
|
+
|
|
482
|
+
Dir.glob(File.join(destination, "**", ".git"), File::FNM_DOTMATCH).each do |path|
|
|
483
|
+
FileUtils.rm_rf(path)
|
|
484
|
+
end
|
|
485
|
+
ensure
|
|
486
|
+
git_capture3("--git-dir", bare_repo, "worktree", "remove", "--force", worktree)
|
|
487
|
+
FileUtils.rm_rf(worktree)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def resolve_git_gem_subdir(repo_root, spec)
|
|
492
|
+
name = spec.name
|
|
493
|
+
return repo_root if File.exist?(File.join(repo_root, "#{name}.gemspec"))
|
|
494
|
+
|
|
495
|
+
source = spec.source
|
|
496
|
+
glob = source.respond_to?(:glob) ? source.glob : Source::Git::DEFAULT_GLOB
|
|
497
|
+
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
498
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
499
|
+
end
|
|
500
|
+
Dir.glob(File.join(repo_root, "**", "*.gemspec")).each do |path|
|
|
501
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
raise CacheError,
|
|
505
|
+
"Git source #{source.uri} does not contain #{name}.gemspec (glob: #{glob.inspect})"
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def git_relative_root(repo_root, gem_root)
|
|
509
|
+
repo_root = File.expand_path(repo_root.to_s)
|
|
510
|
+
gem_root = File.expand_path(gem_root.to_s)
|
|
511
|
+
return "" if repo_root == gem_root
|
|
512
|
+
|
|
513
|
+
if gem_root.start_with?("#{repo_root}/")
|
|
514
|
+
return gem_root.delete_prefix("#{repo_root}/")
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
File.basename(gem_root)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def copy_gemspec_root_files(repo_root, gem_root, dest_root, spec)
|
|
521
|
+
repo_root = File.expand_path(repo_root.to_s)
|
|
522
|
+
gem_root = File.expand_path(gem_root.to_s)
|
|
523
|
+
return if repo_root == gem_root
|
|
524
|
+
|
|
525
|
+
gemspec_path = Dir.glob(File.join(gem_root, "*.gemspec")).first
|
|
526
|
+
gemspec_path ||= File.join(gem_root, "#{spec.name}.gemspec")
|
|
527
|
+
return unless File.exist?(gemspec_path)
|
|
528
|
+
|
|
529
|
+
content = File.read(gemspec_path) rescue nil
|
|
530
|
+
return unless content
|
|
531
|
+
|
|
532
|
+
%w[RAILS_VERSION VERSION].each do |file|
|
|
533
|
+
next unless content.include?(file)
|
|
534
|
+
source = File.join(repo_root, file)
|
|
535
|
+
next unless File.file?(source)
|
|
536
|
+
dest = File.join(dest_root, file)
|
|
537
|
+
next if File.exist?(dest)
|
|
538
|
+
FS.clonefile(source, dest)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def read_gemspec_from_dir(dir, spec)
|
|
543
|
+
return nil unless dir && Dir.exist?(dir)
|
|
544
|
+
|
|
545
|
+
candidates = Dir.glob(File.join(dir, "*.gemspec"))
|
|
546
|
+
return nil if candidates.empty?
|
|
547
|
+
|
|
548
|
+
version = spec.respond_to?(:version) ? spec.version.to_s : nil
|
|
549
|
+
old_version = ENV["VERSION"]
|
|
550
|
+
begin
|
|
551
|
+
ENV["VERSION"] = version if version && !ENV["VERSION"]
|
|
552
|
+
Gem::Specification.load(candidates.first)
|
|
553
|
+
rescue SystemExit, StandardError
|
|
554
|
+
nil
|
|
555
|
+
ensure
|
|
556
|
+
ENV["VERSION"] = old_version
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def git_mutex_for(repo_path)
|
|
561
|
+
@git_mutexes_lock.synchronize do
|
|
562
|
+
@git_mutexes[repo_path] ||= Thread::Mutex.new
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def git_capture3(*args)
|
|
567
|
+
Open3.capture3("git", "-c", "core.fsmonitor=false", *args)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# -- Rubygems helpers ----------------------------------------------------
|
|
571
|
+
|
|
170
572
|
def download_uri_for(spec)
|
|
171
573
|
source = spec.source.to_s.chomp("/")
|
|
172
574
|
"#{source}/gems/#{@cache.full_name(spec)}.gem"
|
|
@@ -174,8 +576,13 @@ module Scint
|
|
|
174
576
|
|
|
175
577
|
def purge_artifacts(spec)
|
|
176
578
|
FileUtils.rm_f(@cache.inbound_path(spec))
|
|
177
|
-
FileUtils.rm_rf(@cache.
|
|
579
|
+
FileUtils.rm_rf(@cache.assembling_path(spec))
|
|
580
|
+
FileUtils.rm_rf(@cache.cached_path(spec))
|
|
581
|
+
FileUtils.rm_f(@cache.cached_spec_path(spec))
|
|
582
|
+
FileUtils.rm_f(@cache.cached_manifest_path(spec))
|
|
178
583
|
FileUtils.rm_f(@cache.spec_cache_path(spec))
|
|
584
|
+
FileUtils.rm_rf(@cache.extracted_path(spec))
|
|
585
|
+
FileUtils.rm_rf(@cache.ext_path(spec))
|
|
179
586
|
end
|
|
180
587
|
|
|
181
588
|
def result_hash(warmed, skipped, ignored, failures)
|
|
@@ -187,6 +594,11 @@ module Scint
|
|
|
187
594
|
failures: failures,
|
|
188
595
|
}
|
|
189
596
|
end
|
|
597
|
+
|
|
598
|
+
# Legacy compatibility — old callers may still check this.
|
|
599
|
+
def prewarmable?(spec)
|
|
600
|
+
rubygems_source?(spec.source) || git_source?(spec.source)
|
|
601
|
+
end
|
|
190
602
|
end
|
|
191
603
|
end
|
|
192
604
|
end
|