scint 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/FEATURES.md +4 -0
- data/README.md +161 -41
- data/VERSION +1 -1
- data/bin/scint +9 -0
- data/lib/bundler.rb +106 -0
- data/lib/scint/cache/layout.rb +72 -14
- data/lib/scint/cache/manifest.rb +120 -0
- data/lib/scint/cache/metadata_store.rb +4 -11
- data/lib/scint/cache/prewarm.rb +445 -33
- data/lib/scint/cache/validity.rb +134 -0
- data/lib/scint/cli/cache.rb +36 -7
- data/lib/scint/cli/exec.rb +13 -25
- data/lib/scint/cli/install.rb +1452 -164
- data/lib/scint/credentials.rb +78 -15
- data/lib/scint/debug/io_trace.rb +26 -7
- data/lib/scint/downloader/fetcher.rb +25 -1
- data/lib/scint/downloader/pool.rb +67 -15
- data/lib/scint/errors.rb +10 -0
- data/lib/scint/fs.rb +215 -26
- data/lib/scint/gem/package.rb +6 -2
- data/lib/scint/gemfile/parser.rb +44 -10
- data/lib/scint/installer/extension_builder.rb +80 -55
- data/lib/scint/installer/linker.rb +51 -26
- data/lib/scint/installer/planner.rb +53 -34
- data/lib/scint/installer/preparer.rb +170 -47
- data/lib/scint/installer/promoter.rb +97 -0
- data/lib/scint/linker.sh +137 -0
- data/lib/scint/lockfile/parser.rb +2 -1
- data/lib/scint/lockfile/writer.rb +85 -36
- data/lib/scint/platform.rb +8 -0
- data/lib/scint/resolver/provider.rb +15 -2
- data/lib/scint/runtime/exec.rb +52 -26
- data/lib/scint/runtime/setup.rb +29 -1
- data/lib/scint/scheduler.rb +6 -1
- data/lib/scint/spec_utils.rb +133 -0
- data/lib/scint.rb +1 -0
- metadata +6 -1
|
@@ -4,8 +4,16 @@ require_relative "../downloader/pool"
|
|
|
4
4
|
require_relative "../gem/package"
|
|
5
5
|
require_relative "../gem/extractor"
|
|
6
6
|
require_relative "../cache/layout"
|
|
7
|
+
require_relative "../cache/manifest"
|
|
8
|
+
require_relative "../cache/validity"
|
|
7
9
|
require_relative "../fs"
|
|
8
10
|
require_relative "../errors"
|
|
11
|
+
require_relative "../spec_utils"
|
|
12
|
+
require_relative "../platform"
|
|
13
|
+
require_relative "./promoter"
|
|
14
|
+
require_relative "./extension_builder"
|
|
15
|
+
require_relative "../source/git"
|
|
16
|
+
require_relative "../source/path"
|
|
9
17
|
|
|
10
18
|
module Scint
|
|
11
19
|
module Installer
|
|
@@ -44,21 +52,30 @@ module Scint
|
|
|
44
52
|
already_cached = []
|
|
45
53
|
|
|
46
54
|
sorted.each do |entry|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
spec = entry.spec
|
|
56
|
+
inbound = @layout.inbound_path(spec)
|
|
57
|
+
assembling = @layout.assembling_path(spec)
|
|
58
|
+
cached = @layout.cached_path(spec)
|
|
49
59
|
|
|
50
|
-
if
|
|
51
|
-
|
|
52
|
-
gemspec = load_cached_spec(entry.spec) || read_gemspec_from_extracted(extracted, entry.spec)
|
|
60
|
+
if Cache::Validity.cached_valid?(spec, @layout)
|
|
61
|
+
gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(cached, spec)
|
|
53
62
|
already_cached << PreparedGem.new(
|
|
54
|
-
spec:
|
|
55
|
-
extracted_path:
|
|
63
|
+
spec: spec,
|
|
64
|
+
extracted_path: cached,
|
|
56
65
|
gemspec: gemspec,
|
|
57
66
|
from_cache: true,
|
|
58
67
|
)
|
|
68
|
+
elsif File.directory?(assembling)
|
|
69
|
+
gemspec = read_gemspec_from_extracted(assembling, spec)
|
|
70
|
+
already_cached << PreparedGem.new(
|
|
71
|
+
spec: spec,
|
|
72
|
+
extracted_path: assembling,
|
|
73
|
+
gemspec: gemspec,
|
|
74
|
+
from_cache: false,
|
|
75
|
+
)
|
|
59
76
|
elsif File.exist?(inbound)
|
|
60
77
|
# Downloaded but not extracted
|
|
61
|
-
already_cached << extract_gem(
|
|
78
|
+
already_cached << extract_gem(spec, inbound)
|
|
62
79
|
else
|
|
63
80
|
# Need to download
|
|
64
81
|
to_download << entry
|
|
@@ -104,65 +121,98 @@ module Scint
|
|
|
104
121
|
|
|
105
122
|
# Prepare a single entry (for use with scheduler).
|
|
106
123
|
def prepare_one(entry)
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
spec = entry.spec
|
|
125
|
+
inbound = @layout.inbound_path(spec)
|
|
126
|
+
assembling = @layout.assembling_path(spec)
|
|
127
|
+
cached = @layout.cached_path(spec)
|
|
109
128
|
|
|
110
|
-
if
|
|
111
|
-
gemspec = load_cached_spec(
|
|
129
|
+
if Cache::Validity.cached_valid?(spec, @layout)
|
|
130
|
+
gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(cached, spec)
|
|
112
131
|
return PreparedGem.new(
|
|
113
|
-
spec:
|
|
114
|
-
extracted_path:
|
|
132
|
+
spec: spec,
|
|
133
|
+
extracted_path: cached,
|
|
115
134
|
gemspec: gemspec,
|
|
116
135
|
from_cache: true,
|
|
117
136
|
)
|
|
118
137
|
end
|
|
119
138
|
|
|
139
|
+
if File.directory?(assembling)
|
|
140
|
+
gemspec = read_gemspec_from_extracted(assembling, spec)
|
|
141
|
+
return PreparedGem.new(
|
|
142
|
+
spec: spec,
|
|
143
|
+
extracted_path: assembling,
|
|
144
|
+
gemspec: gemspec,
|
|
145
|
+
from_cache: false,
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
|
|
120
150
|
unless File.exist?(inbound)
|
|
121
151
|
uri = gem_download_uri(entry)
|
|
122
152
|
@download_pool.download(uri, inbound)
|
|
123
153
|
end
|
|
124
154
|
|
|
125
|
-
extract_gem(
|
|
155
|
+
extract_gem(spec, inbound)
|
|
126
156
|
end
|
|
127
157
|
|
|
128
158
|
private
|
|
129
159
|
|
|
130
160
|
def extract_gem(spec, gem_path)
|
|
131
|
-
|
|
161
|
+
cached = @layout.cached_path(spec)
|
|
162
|
+
assembling = @layout.assembling_path(spec)
|
|
132
163
|
|
|
133
|
-
if
|
|
134
|
-
gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(
|
|
135
|
-
return PreparedGem.new(spec: spec, extracted_path:
|
|
164
|
+
if Cache::Validity.cached_valid?(spec, @layout)
|
|
165
|
+
gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(cached, spec)
|
|
166
|
+
return PreparedGem.new(spec: spec, extracted_path: cached, gemspec: gemspec, from_cache: true)
|
|
136
167
|
end
|
|
137
168
|
|
|
138
|
-
|
|
139
|
-
|
|
169
|
+
if File.directory?(assembling)
|
|
170
|
+
gemspec = read_gemspec_from_extracted(assembling, spec)
|
|
171
|
+
return PreparedGem.new(spec: spec, extracted_path: assembling, gemspec: gemspec, from_cache: false)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Extract to temp dir in assembling, then atomic move
|
|
175
|
+
tmp_dest = "#{assembling}.#{Process.pid}.tmp"
|
|
140
176
|
FileUtils.rm_rf(tmp_dest) if File.exist?(tmp_dest)
|
|
141
177
|
|
|
142
178
|
result = @package.extract(gem_path, tmp_dest)
|
|
143
|
-
FS.atomic_move(tmp_dest,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
179
|
+
FS.atomic_move(tmp_dest, assembling)
|
|
180
|
+
|
|
181
|
+
if ExtensionBuilder.needs_build?(spec, assembling)
|
|
182
|
+
PreparedGem.new(
|
|
183
|
+
spec: spec,
|
|
184
|
+
extracted_path: assembling,
|
|
185
|
+
gemspec: result[:gemspec],
|
|
186
|
+
from_cache: false,
|
|
187
|
+
)
|
|
188
|
+
else
|
|
189
|
+
promote_assembled(spec, assembling, result[:gemspec])
|
|
190
|
+
PreparedGem.new(
|
|
191
|
+
spec: spec,
|
|
192
|
+
extracted_path: @layout.cached_path(spec),
|
|
193
|
+
gemspec: result[:gemspec],
|
|
194
|
+
from_cache: false,
|
|
195
|
+
)
|
|
196
|
+
end
|
|
154
197
|
end
|
|
155
198
|
|
|
156
199
|
def load_cached_spec(spec)
|
|
157
|
-
path = @layout.
|
|
200
|
+
path = @layout.cached_spec_path(spec)
|
|
158
201
|
return nil unless File.exist?(path)
|
|
159
|
-
|
|
160
|
-
|
|
202
|
+
|
|
203
|
+
data = File.binread(path)
|
|
204
|
+
if data.start_with?("---")
|
|
205
|
+
data.force_encoding("UTF-8") if data.encoding != Encoding::UTF_8
|
|
206
|
+
return Gem::Specification.from_yaml(data)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
Marshal.load(data)
|
|
210
|
+
rescue ArgumentError, TypeError, EOFError, StandardError
|
|
161
211
|
nil
|
|
162
212
|
end
|
|
163
213
|
|
|
164
214
|
def cache_spec(spec, gemspec)
|
|
165
|
-
path = @layout.
|
|
215
|
+
path = @layout.cached_spec_path(spec)
|
|
166
216
|
FS.atomic_write(path, Marshal.dump(gemspec))
|
|
167
217
|
rescue StandardError
|
|
168
218
|
# Non-fatal: cache miss on next load
|
|
@@ -172,25 +222,98 @@ module Scint
|
|
|
172
222
|
pattern = File.join(extracted_dir, "*.gemspec")
|
|
173
223
|
candidates = Dir.glob(pattern)
|
|
174
224
|
if candidates.any?
|
|
225
|
+
version = spec.respond_to?(:version) ? spec.version.to_s : nil
|
|
226
|
+
old_version = ENV["VERSION"]
|
|
175
227
|
begin
|
|
176
|
-
|
|
177
|
-
|
|
228
|
+
ENV["VERSION"] = version if version && !ENV["VERSION"]
|
|
229
|
+
SpecUtils.load_gemspec(candidates.first, isolate: true)
|
|
230
|
+
rescue SystemExit, StandardError
|
|
178
231
|
nil
|
|
232
|
+
ensure
|
|
233
|
+
ENV["VERSION"] = old_version
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def promote_assembled(spec, assembling_path, gemspec)
|
|
239
|
+
return unless assembling_path && Dir.exist?(assembling_path)
|
|
240
|
+
|
|
241
|
+
cached_dir = @layout.cached_path(spec)
|
|
242
|
+
promoter = Promoter.new(root: @layout.root)
|
|
243
|
+
lock_key = "#{Platform.abi_key}-#{@layout.full_name(spec)}"
|
|
244
|
+
|
|
245
|
+
promoter.validate_within_root!(@layout.root, assembling_path, label: "assembling")
|
|
246
|
+
promoter.validate_within_root!(@layout.root, cached_dir, label: "cached")
|
|
247
|
+
|
|
248
|
+
result = nil
|
|
249
|
+
promoter.with_staging_dir(prefix: "cached") do |staging|
|
|
250
|
+
FS.clone_tree(assembling_path, staging)
|
|
251
|
+
manifest = build_cached_manifest(spec, staging)
|
|
252
|
+
spec_payload = gemspec ? Marshal.dump(gemspec) : nil
|
|
253
|
+
result = promoter.promote_tree(
|
|
254
|
+
staging_path: staging,
|
|
255
|
+
target_path: cached_dir,
|
|
256
|
+
lock_key: lock_key,
|
|
257
|
+
)
|
|
258
|
+
if result == :promoted
|
|
259
|
+
write_cached_metadata(spec, spec_payload, manifest)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
FileUtils.rm_rf(assembling_path) if Dir.exist?(assembling_path)
|
|
263
|
+
result
|
|
264
|
+
rescue StandardError
|
|
265
|
+
FileUtils.rm_rf(cached_dir) if Dir.exist?(cached_dir)
|
|
266
|
+
raise
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def write_cached_metadata(spec, spec_payload, manifest)
|
|
270
|
+
spec_path = @layout.cached_spec_path(spec)
|
|
271
|
+
manifest_path = @layout.cached_manifest_path(spec)
|
|
272
|
+
FS.mkdir_p(File.dirname(spec_path))
|
|
273
|
+
|
|
274
|
+
FS.atomic_write(spec_path, spec_payload) if spec_payload
|
|
275
|
+
Cache::Manifest.write(manifest_path, manifest)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def build_cached_manifest(spec, cached_dir)
|
|
279
|
+
Cache::Manifest.build(
|
|
280
|
+
spec: spec,
|
|
281
|
+
gem_dir: cached_dir,
|
|
282
|
+
abi_key: Platform.abi_key,
|
|
283
|
+
source: manifest_source_for(spec),
|
|
284
|
+
extensions: ExtensionBuilder.needs_build?(spec, cached_dir),
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def manifest_source_for(spec)
|
|
289
|
+
source = spec.source
|
|
290
|
+
if source.is_a?(Source::Git)
|
|
291
|
+
{
|
|
292
|
+
"type" => "git",
|
|
293
|
+
"uri" => source.uri.to_s,
|
|
294
|
+
"revision" => source.revision || source.ref || source.branch || source.tag,
|
|
295
|
+
}.compact
|
|
296
|
+
elsif source.is_a?(Source::Path)
|
|
297
|
+
{
|
|
298
|
+
"type" => "path",
|
|
299
|
+
"path" => File.expand_path(source.path.to_s),
|
|
300
|
+
"uri" => source.path.to_s,
|
|
301
|
+
}
|
|
302
|
+
else
|
|
303
|
+
source_str = source.to_s
|
|
304
|
+
if source_str.start_with?("http://", "https://")
|
|
305
|
+
{ "type" => "rubygems", "uri" => source_str }
|
|
306
|
+
elsif source_str.start_with?("/", ".", "~")
|
|
307
|
+
{ "type" => "path", "path" => File.expand_path(source_str), "uri" => source_str }
|
|
308
|
+
else
|
|
309
|
+
{ "type" => "rubygems", "uri" => source_str }
|
|
179
310
|
end
|
|
180
311
|
end
|
|
181
312
|
end
|
|
182
313
|
|
|
183
314
|
def gem_download_uri(entry)
|
|
184
315
|
spec = entry.spec
|
|
185
|
-
|
|
186
|
-
version = spec.respond_to?(:version) ? spec.version : spec[:version]
|
|
187
|
-
platform = spec.respond_to?(:platform) ? spec.platform : spec[:platform]
|
|
188
|
-
|
|
189
|
-
filename = if platform && platform.to_s != "ruby" && platform.to_s != ""
|
|
190
|
-
"#{name}-#{version}-#{platform}.gem"
|
|
191
|
-
else
|
|
192
|
-
"#{name}-#{version}.gem"
|
|
193
|
-
end
|
|
316
|
+
filename = "#{SpecUtils.full_name(spec)}.gem"
|
|
194
317
|
|
|
195
318
|
# Use cached_path if provided, otherwise construct from source
|
|
196
319
|
if entry.cached_path
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../fs"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module Scint
|
|
9
|
+
module Installer
|
|
10
|
+
class Promoter
|
|
11
|
+
attr_reader :root, :lock_dir, :staging_dir
|
|
12
|
+
|
|
13
|
+
def initialize(root:, lock_dir: nil, staging_dir: nil)
|
|
14
|
+
@root = File.expand_path(root.to_s)
|
|
15
|
+
@lock_dir = File.expand_path(lock_dir.to_s) if lock_dir
|
|
16
|
+
@staging_dir = File.expand_path(staging_dir.to_s) if staging_dir
|
|
17
|
+
|
|
18
|
+
@lock_dir ||= File.join(@root, "locks", "promotion")
|
|
19
|
+
@staging_dir ||= File.join(@root, "staging")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with_lock(lock_key)
|
|
23
|
+
lock_path = lock_path_for(lock_key)
|
|
24
|
+
validate_within_root!(@root, lock_path, label: "lock")
|
|
25
|
+
FS.mkdir_p(File.dirname(lock_path))
|
|
26
|
+
|
|
27
|
+
File.open(lock_path, "w") do |file|
|
|
28
|
+
file.flock(File::LOCK_EX)
|
|
29
|
+
yield
|
|
30
|
+
ensure
|
|
31
|
+
file.flock(File::LOCK_UN) rescue nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def with_staging_dir(prefix:)
|
|
36
|
+
FS.mkdir_p(@staging_dir)
|
|
37
|
+
staging_path = File.join(@staging_dir, staging_suffix(prefix))
|
|
38
|
+
validate_within_root!(@root, staging_path, label: "staging")
|
|
39
|
+
FileUtils.rm_rf(staging_path) if File.exist?(staging_path)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
FS.mkdir_p(staging_path)
|
|
43
|
+
yield staging_path
|
|
44
|
+
ensure
|
|
45
|
+
FileUtils.rm_rf(staging_path) if Dir.exist?(staging_path)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def promote_tree(staging_path:, target_path:, lock_key:)
|
|
50
|
+
validate_within_root!(@root, staging_path, label: "staging")
|
|
51
|
+
validate_within_root!(@root, target_path, label: "target")
|
|
52
|
+
raise CacheError, "Staging path does not exist: #{staging_path}" unless Dir.exist?(staging_path)
|
|
53
|
+
|
|
54
|
+
with_lock(lock_key) do
|
|
55
|
+
if Dir.exist?(target_path)
|
|
56
|
+
FileUtils.rm_rf(staging_path) if Dir.exist?(staging_path)
|
|
57
|
+
return :exists
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
FS.atomic_move(staging_path, target_path)
|
|
62
|
+
rescue StandardError
|
|
63
|
+
FileUtils.rm_rf(staging_path) if Dir.exist?(staging_path)
|
|
64
|
+
raise
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
:promoted
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_within_root!(root_path, candidate_path, label: "path")
|
|
72
|
+
root_expanded = File.expand_path(root_path.to_s)
|
|
73
|
+
candidate_expanded = File.expand_path(candidate_path.to_s)
|
|
74
|
+
within_root = candidate_expanded == root_expanded ||
|
|
75
|
+
candidate_expanded.start_with?("#{root_expanded}/")
|
|
76
|
+
return if within_root
|
|
77
|
+
|
|
78
|
+
raise CacheError, "#{label.capitalize} escapes cache root: #{candidate_path}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def lock_path_for(lock_key)
|
|
82
|
+
safe_key = sanitize_key(lock_key)
|
|
83
|
+
File.join(@lock_dir, "#{safe_key}.lock")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def sanitize_key(key)
|
|
87
|
+
key.to_s.gsub(/[^0-9A-Za-z._-]/, "_")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def staging_suffix(prefix)
|
|
91
|
+
safe = sanitize_key(prefix)
|
|
92
|
+
token = SecureRandom.hex(6)
|
|
93
|
+
"#{safe}.#{Process.pid}.#{Thread.current.object_id}.#{token}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/scint/linker.sh
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# scint-linker — bulk-hardlink cached gems into a bundle.
|
|
3
|
+
#
|
|
4
|
+
# Protocol (stdin):
|
|
5
|
+
# Line 1: source parent dir (e.g. ~/.cache/scint/cached/ruby-3.4.7-x86_64-linux)
|
|
6
|
+
# Line 2: dest parent dir (e.g. .bundle/ruby/3.4.0/gems)
|
|
7
|
+
# Remaining lines: gem directory basenames (e.g. "rack-3.2.4")
|
|
8
|
+
#
|
|
9
|
+
# The script probes the best copy strategy once (cpio, hardlink, reflink,
|
|
10
|
+
# copy) and uses it for every gem. When cpio is available it reads
|
|
11
|
+
# .scint-files from each cached gem so only listed files are materialized.
|
|
12
|
+
# Otherwise falls back to cp -al / cp --reflink / cp -R.
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
read -r SRC
|
|
17
|
+
read -r DST
|
|
18
|
+
mkdir -p "$DST"
|
|
19
|
+
|
|
20
|
+
# ── detect strategy ──────────────────────────────────────────────
|
|
21
|
+
STRATEGY=""
|
|
22
|
+
|
|
23
|
+
# Probe needs a real file to test against. Find one quickly.
|
|
24
|
+
probe_src=""
|
|
25
|
+
for candidate in "$SRC"/*/.scint-files; do
|
|
26
|
+
[ -f "$candidate" ] && probe_src="$candidate" && break
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
if [ -z "$probe_src" ]; then
|
|
30
|
+
# No .scint-files at all — fall back to cp -al or cp -R
|
|
31
|
+
if cp -al --version >/dev/null 2>&1; then
|
|
32
|
+
STRATEGY="cp-al"
|
|
33
|
+
else
|
|
34
|
+
STRATEGY="cp"
|
|
35
|
+
fi
|
|
36
|
+
else
|
|
37
|
+
probe_dst="$DST/.scint-probe-$$"
|
|
38
|
+
|
|
39
|
+
# 1. cpio -pld (file-list driven hardlinks — ideal)
|
|
40
|
+
if command -v cpio >/dev/null 2>&1; then
|
|
41
|
+
if echo "$probe_src" | cpio -pld "$DST" >/dev/null 2>&1; then
|
|
42
|
+
rm -f "$probe_dst" 2>/dev/null
|
|
43
|
+
STRATEGY="cpio"
|
|
44
|
+
fi
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# 2. hardlink via ln
|
|
48
|
+
if [ -z "$STRATEGY" ]; then
|
|
49
|
+
if ln "$probe_src" "$probe_dst" 2>/dev/null; then
|
|
50
|
+
rm -f "$probe_dst"
|
|
51
|
+
STRATEGY="cp-al"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# 3. reflink (btrfs/xfs/APFS)
|
|
56
|
+
if [ -z "$STRATEGY" ]; then
|
|
57
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
58
|
+
if cp -c "$probe_src" "$probe_dst" 2>/dev/null; then
|
|
59
|
+
rm -f "$probe_dst"
|
|
60
|
+
STRATEGY="reflink"
|
|
61
|
+
fi
|
|
62
|
+
else
|
|
63
|
+
if cp --reflink=always "$probe_src" "$probe_dst" 2>/dev/null; then
|
|
64
|
+
rm -f "$probe_dst"
|
|
65
|
+
STRATEGY="reflink"
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
rm -f "$probe_dst" 2>/dev/null
|
|
71
|
+
[ -z "$STRATEGY" ] && STRATEGY="cp"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# ── link gems ────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
link_cpio() {
|
|
77
|
+
# Read .scint-files, prefix each line with gem name, pipe to cpio -pld.
|
|
78
|
+
# One cpio call per gem (cpio needs a single source root).
|
|
79
|
+
local gem="$1"
|
|
80
|
+
local dotfiles="$SRC/$gem/.scint-files"
|
|
81
|
+
if [ -f "$dotfiles" ]; then
|
|
82
|
+
(cd "$SRC/$gem" && cpio -pld "$DST/$gem" < "$dotfiles" 2>/dev/null)
|
|
83
|
+
else
|
|
84
|
+
cp -al "$SRC/$gem" "$DST/$gem"
|
|
85
|
+
fi
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# For cp-al: batch all gems into one call
|
|
89
|
+
BATCH=()
|
|
90
|
+
|
|
91
|
+
flush_batch() {
|
|
92
|
+
[ ${#BATCH[@]} -eq 0 ] && return
|
|
93
|
+
case "$STRATEGY" in
|
|
94
|
+
cp-al)
|
|
95
|
+
cp -al "${BATCH[@]}" "$DST/" 2>/dev/null || {
|
|
96
|
+
# Individual fallback on batch failure
|
|
97
|
+
for s in "${BATCH[@]}"; do
|
|
98
|
+
cp -al "$s" "$DST/" 2>/dev/null || cp -R "$s" "$DST/"
|
|
99
|
+
done
|
|
100
|
+
}
|
|
101
|
+
;;
|
|
102
|
+
reflink)
|
|
103
|
+
if [ "$(uname)" = "Darwin" ]; then
|
|
104
|
+
cp -cR "${BATCH[@]}" "$DST/" 2>/dev/null || {
|
|
105
|
+
for s in "${BATCH[@]}"; do cp -R "$s" "$DST/"; done
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
cp --reflink=always -R "${BATCH[@]}" "$DST/" 2>/dev/null || {
|
|
109
|
+
for s in "${BATCH[@]}"; do cp -R "$s" "$DST/"; done
|
|
110
|
+
}
|
|
111
|
+
fi
|
|
112
|
+
;;
|
|
113
|
+
cp)
|
|
114
|
+
cp -R "${BATCH[@]}" "$DST/"
|
|
115
|
+
;;
|
|
116
|
+
esac
|
|
117
|
+
BATCH=()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
while IFS= read -r gem; do
|
|
121
|
+
[ -z "$gem" ] && continue
|
|
122
|
+
[ -d "$SRC/$gem" ] || continue
|
|
123
|
+
[ -d "$DST/$gem" ] && continue
|
|
124
|
+
|
|
125
|
+
case "$STRATEGY" in
|
|
126
|
+
cpio)
|
|
127
|
+
link_cpio "$gem"
|
|
128
|
+
;;
|
|
129
|
+
*)
|
|
130
|
+
BATCH+=("$SRC/$gem")
|
|
131
|
+
# Flush in chunks to stay under ARG_MAX
|
|
132
|
+
[ ${#BATCH[@]} -ge 200 ] && flush_batch
|
|
133
|
+
;;
|
|
134
|
+
esac
|
|
135
|
+
done
|
|
136
|
+
|
|
137
|
+
flush_batch
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../source/rubygems"
|
|
4
4
|
require_relative "../source/git"
|
|
5
5
|
require_relative "../source/path"
|
|
6
|
+
require_relative "../spec_utils"
|
|
6
7
|
|
|
7
8
|
module Scint
|
|
8
9
|
module Lockfile
|
|
@@ -209,7 +210,7 @@ module Scint
|
|
|
209
210
|
platform = $4 || "ruby"
|
|
210
211
|
checksums_str = $6
|
|
211
212
|
|
|
212
|
-
key =
|
|
213
|
+
key = SpecUtils.full_name_for(name, version, platform)
|
|
213
214
|
|
|
214
215
|
if checksums_str
|
|
215
216
|
@checksums[key] = checksums_str.split(",").map(&:strip)
|