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/fs.rb
CHANGED
|
@@ -21,28 +21,75 @@ module Scint
|
|
|
21
21
|
@mkdir_mutex.synchronize { @mkdir_cache[path] = true }
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Detect best file copy strategy once per process.
|
|
25
|
+
# Returns :reflink, :hardlink, or :copy.
|
|
26
|
+
@copy_strategy = nil
|
|
27
|
+
@copy_strategy_mutex = Thread::Mutex.new
|
|
28
|
+
|
|
29
|
+
def detect_copy_strategy(src_dir, dst_dir)
|
|
30
|
+
@copy_strategy_mutex.synchronize do
|
|
31
|
+
return @copy_strategy if @copy_strategy
|
|
32
|
+
|
|
33
|
+
@copy_strategy = _probe_copy_strategy(src_dir, dst_dir)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def _probe_copy_strategy(src_dir, dst_dir)
|
|
38
|
+
# Create a temp file in src to test against dst
|
|
39
|
+
probe_src = File.join(src_dir, ".scint_probe_#{$$}")
|
|
40
|
+
probe_dst = File.join(dst_dir, ".scint_probe_#{$$}")
|
|
41
|
+
begin
|
|
42
|
+
File.write(probe_src, "x")
|
|
43
|
+
mkdir_p(dst_dir)
|
|
44
|
+
|
|
45
|
+
if Platform.macos?
|
|
46
|
+
if system("cp", "-c", probe_src, probe_dst, [:out, :err] => File::NULL)
|
|
47
|
+
return :reflink
|
|
48
|
+
end
|
|
49
|
+
File.delete(probe_dst) if File.exist?(probe_dst)
|
|
50
|
+
elsif Platform.linux?
|
|
51
|
+
if system("cp", "--reflink=always", probe_src, probe_dst, [:out, :err] => File::NULL)
|
|
52
|
+
return :reflink
|
|
53
|
+
end
|
|
54
|
+
File.delete(probe_dst) if File.exist?(probe_dst)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
File.link(probe_src, probe_dst)
|
|
59
|
+
return :hardlink
|
|
60
|
+
rescue SystemCallError
|
|
61
|
+
File.delete(probe_dst) if File.exist?(probe_dst)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
:copy
|
|
65
|
+
ensure
|
|
66
|
+
File.delete(probe_src) if File.exist?(probe_src)
|
|
67
|
+
File.delete(probe_dst) if File.exist?(probe_dst)
|
|
68
|
+
end
|
|
69
|
+
rescue StandardError
|
|
70
|
+
:copy
|
|
71
|
+
end
|
|
72
|
+
|
|
24
73
|
# APFS clonefile (CoW copy). Falls back to hardlink, then regular copy.
|
|
25
74
|
def clonefile(src, dst)
|
|
26
75
|
src = src.to_s
|
|
27
76
|
dst = dst.to_s
|
|
28
77
|
mkdir_p(File.dirname(dst))
|
|
29
78
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
rescue SystemCallError
|
|
45
|
-
# cross-device or unsupported
|
|
79
|
+
case detect_copy_strategy(File.dirname(src), File.dirname(dst))
|
|
80
|
+
when :reflink
|
|
81
|
+
if Platform.macos?
|
|
82
|
+
return if system("cp", "-c", src, dst, [:out, :err] => File::NULL)
|
|
83
|
+
elsif Platform.linux?
|
|
84
|
+
return if system("cp", "--reflink=always", src, dst, [:out, :err] => File::NULL)
|
|
85
|
+
end
|
|
86
|
+
when :hardlink
|
|
87
|
+
begin
|
|
88
|
+
File.link(src, dst)
|
|
89
|
+
return
|
|
90
|
+
rescue SystemCallError
|
|
91
|
+
# fall through to copy
|
|
92
|
+
end
|
|
46
93
|
end
|
|
47
94
|
|
|
48
95
|
# Final fallback: regular copy
|
|
@@ -58,21 +105,117 @@ module Scint
|
|
|
58
105
|
raise Errno::ENOENT, src_dir unless Dir.exist?(src_dir)
|
|
59
106
|
mkdir_p(dst_dir)
|
|
60
107
|
|
|
61
|
-
|
|
62
|
-
if Platform.macos?
|
|
63
|
-
src_contents = File.join(src_dir, ".")
|
|
64
|
-
return if system("cp", "-cR", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
65
|
-
end
|
|
108
|
+
strategy = detect_copy_strategy(src_dir, dst_dir)
|
|
66
109
|
|
|
67
|
-
|
|
68
|
-
if Platform.linux?
|
|
110
|
+
if strategy == :reflink
|
|
69
111
|
src_contents = File.join(src_dir, ".")
|
|
70
|
-
|
|
112
|
+
if Platform.macos?
|
|
113
|
+
return if system("cp", "-cR", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
114
|
+
elsif Platform.linux?
|
|
115
|
+
return if system("cp", "--reflink=always", "-R", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
116
|
+
end
|
|
71
117
|
end
|
|
72
118
|
|
|
73
119
|
hardlink_tree(src_dir, dst_dir)
|
|
74
120
|
end
|
|
75
121
|
|
|
122
|
+
# Materialize a tree using a manifest to avoid directory scans.
|
|
123
|
+
# Manifest entries must be hashes with "path" and "type" keys.
|
|
124
|
+
# Uses the fastest available file copy strategy (reflink > hardlink > copy).
|
|
125
|
+
def materialize_from_manifest(src_dir, dst_dir, entries)
|
|
126
|
+
src_dir = src_dir.to_s
|
|
127
|
+
dst_dir = dst_dir.to_s
|
|
128
|
+
entries = Array(entries)
|
|
129
|
+
raise Errno::ENOENT, src_dir unless Dir.exist?(src_dir)
|
|
130
|
+
mkdir_p(dst_dir)
|
|
131
|
+
|
|
132
|
+
strategy = detect_copy_strategy(src_dir, dst_dir)
|
|
133
|
+
|
|
134
|
+
entries.each do |entry|
|
|
135
|
+
rel = entry["path"].to_s
|
|
136
|
+
next if rel.empty? || rel.start_with?("/") || rel.include?("..")
|
|
137
|
+
|
|
138
|
+
src_path = File.join(src_dir, rel)
|
|
139
|
+
dst_path = File.join(dst_dir, rel)
|
|
140
|
+
|
|
141
|
+
case entry["type"]
|
|
142
|
+
when "dir"
|
|
143
|
+
mkdir_p(dst_path)
|
|
144
|
+
when "symlink"
|
|
145
|
+
mkdir_p(File.dirname(dst_path))
|
|
146
|
+
next if File.exist?(dst_path) || File.symlink?(dst_path)
|
|
147
|
+
|
|
148
|
+
target = File.readlink(src_path)
|
|
149
|
+
begin
|
|
150
|
+
File.symlink(target, dst_path)
|
|
151
|
+
rescue Errno::EEXIST
|
|
152
|
+
next
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
mkdir_p(File.dirname(dst_path))
|
|
156
|
+
next if File.exist?(dst_path)
|
|
157
|
+
|
|
158
|
+
begin
|
|
159
|
+
_link_or_copy(src_path, dst_path, strategy)
|
|
160
|
+
rescue Errno::EEXIST
|
|
161
|
+
next
|
|
162
|
+
rescue SystemCallError
|
|
163
|
+
next if File.exist?(dst_path)
|
|
164
|
+
raise
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Fast file link/copy using a pre-detected strategy (no per-file probing).
|
|
171
|
+
def _link_or_copy(src, dst, strategy)
|
|
172
|
+
case strategy
|
|
173
|
+
when :reflink
|
|
174
|
+
if Platform.macos?
|
|
175
|
+
return if system("cp", "-c", src, dst, [:out, :err] => File::NULL)
|
|
176
|
+
elsif Platform.linux?
|
|
177
|
+
return if system("cp", "--reflink=always", src, dst, [:out, :err] => File::NULL)
|
|
178
|
+
end
|
|
179
|
+
# reflink failed for this file, try hardlink
|
|
180
|
+
begin
|
|
181
|
+
File.link(src, dst)
|
|
182
|
+
return
|
|
183
|
+
rescue SystemCallError; end
|
|
184
|
+
FileUtils.cp(src, dst)
|
|
185
|
+
when :hardlink
|
|
186
|
+
begin
|
|
187
|
+
File.link(src, dst)
|
|
188
|
+
return
|
|
189
|
+
rescue SystemCallError; end
|
|
190
|
+
FileUtils.cp(src, dst)
|
|
191
|
+
else
|
|
192
|
+
FileUtils.cp(src, dst)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
LINKER_SCRIPT = File.expand_path("linker.sh", __dir__).freeze
|
|
197
|
+
|
|
198
|
+
# Bulk-link cached gem directories into dst_parent.
|
|
199
|
+
# Opens one helper process (linker.sh) and writes gem basenames to its
|
|
200
|
+
# stdin. The helper probes the fastest FS strategy once then applies it
|
|
201
|
+
# to every gem.
|
|
202
|
+
def bulk_link_gems(src_parent, dst_parent, gem_names)
|
|
203
|
+
src_parent = src_parent.to_s
|
|
204
|
+
dst_parent = dst_parent.to_s
|
|
205
|
+
gem_names = Array(gem_names)
|
|
206
|
+
return 0 if gem_names.empty?
|
|
207
|
+
|
|
208
|
+
mkdir_p(dst_parent)
|
|
209
|
+
|
|
210
|
+
IO.popen(["/bin/bash", LINKER_SCRIPT], "w") do |io|
|
|
211
|
+
io.puts src_parent
|
|
212
|
+
io.puts dst_parent
|
|
213
|
+
gem_names.each { |name| io.puts name }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
gem_names.size
|
|
217
|
+
end
|
|
218
|
+
|
|
76
219
|
# Clone many source directories into one destination parent directory.
|
|
77
220
|
# This is significantly faster than one process per gem on large warm
|
|
78
221
|
# installs because it batches cp invocations while preserving CoW/reflink.
|
|
@@ -86,6 +229,8 @@ module Scint
|
|
|
86
229
|
return 0 if sources.empty?
|
|
87
230
|
|
|
88
231
|
copied = 0
|
|
232
|
+
strategy = sources.first ? detect_copy_strategy(sources.first, dst_parent) : :copy
|
|
233
|
+
|
|
89
234
|
sources.each_slice([chunk_size.to_i, 1].max) do |slice|
|
|
90
235
|
pending = slice.reject do |src|
|
|
91
236
|
Dir.exist?(File.join(dst_parent, File.basename(src)))
|
|
@@ -93,10 +238,12 @@ module Scint
|
|
|
93
238
|
next if pending.empty?
|
|
94
239
|
|
|
95
240
|
ok = false
|
|
96
|
-
if
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
241
|
+
if strategy == :reflink
|
|
242
|
+
if Platform.macos?
|
|
243
|
+
ok = system("cp", "-cR", *pending, dst_parent, [:out, :err] => File::NULL)
|
|
244
|
+
elsif Platform.linux?
|
|
245
|
+
ok = system("cp", "--reflink=always", "-R", *pending, dst_parent, [:out, :err] => File::NULL)
|
|
246
|
+
end
|
|
100
247
|
end
|
|
101
248
|
|
|
102
249
|
unless ok
|
data/lib/scint/gem/package.rb
CHANGED
|
@@ -17,7 +17,9 @@ module Scint
|
|
|
17
17
|
tar.each do |entry|
|
|
18
18
|
if entry.full_name == "metadata.gz"
|
|
19
19
|
gz = Zlib::GzipReader.new(StringIO.new(entry.read))
|
|
20
|
-
|
|
20
|
+
yaml = gz.read
|
|
21
|
+
yaml.force_encoding("UTF-8") if yaml.encoding == Encoding::US_ASCII
|
|
22
|
+
return ::Gem::Specification.from_yaml(yaml)
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
@@ -38,7 +40,9 @@ module Scint
|
|
|
38
40
|
case entry.full_name
|
|
39
41
|
when "metadata.gz"
|
|
40
42
|
gz = Zlib::GzipReader.new(StringIO.new(entry.read))
|
|
41
|
-
|
|
43
|
+
yaml = gz.read
|
|
44
|
+
yaml.force_encoding("UTF-8") if yaml.encoding == Encoding::US_ASCII
|
|
45
|
+
gemspec = ::Gem::Specification.from_yaml(yaml)
|
|
42
46
|
when "data.tar.gz"
|
|
43
47
|
# Write data.tar.gz to a temp file for extraction
|
|
44
48
|
tmp = File.join(dest_dir, ".data.tar.gz.tmp")
|
data/lib/scint/gemfile/parser.rb
CHANGED
|
@@ -6,7 +6,7 @@ require_relative "../source/path"
|
|
|
6
6
|
module Scint
|
|
7
7
|
module Gemfile
|
|
8
8
|
# Result of parsing a Gemfile.
|
|
9
|
-
ParseResult = Struct.new(:dependencies, :sources, :ruby_version, :platforms, keyword_init: true)
|
|
9
|
+
ParseResult = Struct.new(:dependencies, :sources, :ruby_version, :platforms, :optional_groups, keyword_init: true)
|
|
10
10
|
|
|
11
11
|
# Evaluates a Gemfile using instance_eval, just like stock bundler.
|
|
12
12
|
# Supports the full Gemfile DSL: source, gem, group, platforms, git_source,
|
|
@@ -20,14 +20,16 @@ module Scint
|
|
|
20
20
|
sources: parser.parsed_sources.uniq,
|
|
21
21
|
ruby_version: parser.parsed_ruby_version,
|
|
22
22
|
platforms: parser.parsed_platforms,
|
|
23
|
+
optional_groups: parser.parsed_optional_groups,
|
|
23
24
|
)
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
# Accessors that don't collide with DSL method names
|
|
27
|
-
def parsed_dependencies;
|
|
28
|
-
def parsed_sources;
|
|
29
|
-
def parsed_ruby_version;
|
|
30
|
-
def parsed_platforms;
|
|
28
|
+
def parsed_dependencies; @dependencies; end
|
|
29
|
+
def parsed_sources; @sources; end
|
|
30
|
+
def parsed_ruby_version; @ruby_version; end
|
|
31
|
+
def parsed_platforms; @declared_platforms; end
|
|
32
|
+
def parsed_optional_groups; @optional_groups; end
|
|
31
33
|
|
|
32
34
|
def initialize(gemfile_path)
|
|
33
35
|
@gemfile_path = File.expand_path(gemfile_path)
|
|
@@ -39,6 +41,7 @@ module Scint
|
|
|
39
41
|
@current_source_options = {}
|
|
40
42
|
@ruby_version = nil
|
|
41
43
|
@declared_platforms = []
|
|
44
|
+
@optional_groups = []
|
|
42
45
|
|
|
43
46
|
add_default_git_sources
|
|
44
47
|
end
|
|
@@ -167,7 +170,11 @@ module Scint
|
|
|
167
170
|
|
|
168
171
|
def group(*names, **opts, &blk)
|
|
169
172
|
old_groups = @current_groups.dup
|
|
170
|
-
|
|
173
|
+
group_syms = names.map(&:to_sym)
|
|
174
|
+
@current_groups.concat(group_syms)
|
|
175
|
+
if opts[:optional]
|
|
176
|
+
group_syms.each { |g| @optional_groups << g unless @optional_groups.include?(g) }
|
|
177
|
+
end
|
|
171
178
|
yield
|
|
172
179
|
ensure
|
|
173
180
|
@current_groups = old_groups
|
|
@@ -11,24 +11,21 @@ module Scint
|
|
|
11
11
|
module ExtensionBuilder
|
|
12
12
|
module_function
|
|
13
13
|
|
|
14
|
+
BUILD_MARKER = ".scint.build_complete"
|
|
15
|
+
|
|
14
16
|
# Build native extensions for a prepared gem.
|
|
15
17
|
# prepared_gem: PreparedGem struct
|
|
16
18
|
# bundle_path: .bundle/ root
|
|
17
19
|
# abi_key: e.g. "ruby-3.3.0-arm64-darwin24" (defaults to Platform.abi_key)
|
|
18
20
|
def build(prepared_gem, bundle_path, cache_layout, abi_key: Platform.abi_key, compile_slots: 1, output_tail: nil)
|
|
19
21
|
spec = prepared_gem.spec
|
|
20
|
-
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
21
22
|
build_ruby_dir = cache_layout.install_ruby_dir
|
|
23
|
+
src_dir = prepared_gem.extracted_path
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if Dir.exist?(cached_ext) && File.exist?(File.join(cached_ext, "gem.build_complete"))
|
|
26
|
-
link_extensions(cached_ext, ruby_dir, spec, abi_key)
|
|
27
|
-
return true
|
|
28
|
-
end
|
|
25
|
+
marker = build_marker_path(src_dir)
|
|
26
|
+
return true if File.exist?(marker)
|
|
29
27
|
|
|
30
|
-
# Build in a temp dir, then
|
|
31
|
-
src_dir = prepared_gem.extracted_path
|
|
28
|
+
# Build in a temp dir, then sync artifacts into the source tree.
|
|
32
29
|
FS.with_tempdir("scint-ext") do |tmpdir|
|
|
33
30
|
# Stage the full gem source tree in an isolated workspace.
|
|
34
31
|
# Many extconf scripts use paths like ../../vendor relative to ext/,
|
|
@@ -53,34 +50,24 @@ module Scint
|
|
|
53
50
|
compile_extension(ext_dir, ext_build_dir, install_dir, staged_src_dir, spec, build_ruby_dir, compile_slots, output_tail)
|
|
54
51
|
end
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
File.write(File.join(install_dir, "gem.build_complete"), "")
|
|
58
|
-
|
|
59
|
-
# Cache globally
|
|
60
|
-
FS.mkdir_p(File.dirname(cached_ext))
|
|
61
|
-
FS.atomic_move(install_dir, cached_ext)
|
|
53
|
+
sync_extensions_into_gem(install_dir, src_dir)
|
|
62
54
|
end
|
|
63
55
|
|
|
64
|
-
|
|
56
|
+
File.write(marker, "")
|
|
65
57
|
true
|
|
66
58
|
end
|
|
67
59
|
|
|
68
60
|
# True when a completed global extension build exists for this spec + ABI.
|
|
69
61
|
def cached_build_available?(spec, cache_layout, abi_key: Platform.abi_key)
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
cached_dir = cache_layout.cached_path(spec, abi_key)
|
|
63
|
+
File.exist?(build_marker_path(cached_dir))
|
|
72
64
|
end
|
|
73
65
|
|
|
74
|
-
# Link already-compiled extensions from
|
|
75
|
-
# Returns true when cache
|
|
76
|
-
def link_cached_build(prepared_gem,
|
|
66
|
+
# Link already-compiled extensions from the cached gem tree.
|
|
67
|
+
# Returns true when cache marker is present, false otherwise.
|
|
68
|
+
def link_cached_build(prepared_gem, _bundle_path, cache_layout, abi_key: Platform.abi_key)
|
|
77
69
|
spec = prepared_gem.spec
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
81
|
-
cached_ext = cache_layout.ext_path(spec, abi_key)
|
|
82
|
-
link_extensions(cached_ext, ruby_dir, spec, abi_key)
|
|
83
|
-
true
|
|
70
|
+
cached_build_available?(spec, cache_layout, abi_key: abi_key)
|
|
84
71
|
end
|
|
85
72
|
|
|
86
73
|
# True when a gem has native extension sources that need compiling.
|
|
@@ -235,22 +222,29 @@ module Scint
|
|
|
235
222
|
Platform.gem_arch, Platform.extension_api_version,
|
|
236
223
|
spec_full_name(spec))
|
|
237
224
|
FS.clone_tree(cached_ext, ext_install_dir) unless Dir.exist?(ext_install_dir)
|
|
238
|
-
|
|
225
|
+
gem_dir = File.join(ruby_dir, "gems", spec_full_name(spec))
|
|
226
|
+
sync_extensions_into_gem(cached_ext, gem_dir)
|
|
239
227
|
end
|
|
240
228
|
|
|
241
|
-
|
|
242
|
-
|
|
229
|
+
# Sync compiled extension artifacts into a gem's lib directory.
|
|
230
|
+
# source_dir should contain the compiled artifacts (from build output
|
|
231
|
+
# or a cached gem tree).
|
|
232
|
+
def sync_extensions_into_gem(cached_ext, gem_dir)
|
|
243
233
|
lib_dir = File.join(gem_dir, "lib")
|
|
244
|
-
|
|
234
|
+
FS.mkdir_p(lib_dir)
|
|
245
235
|
|
|
246
|
-
Dir.glob(File.join(
|
|
247
|
-
rel = artifact.delete_prefix("#{
|
|
236
|
+
Dir.glob(File.join(cached_ext, "**", "*.{so,bundle,dll,dylib}")).each do |artifact|
|
|
237
|
+
rel = artifact.delete_prefix("#{cached_ext}/")
|
|
248
238
|
dest = File.join(lib_dir, rel)
|
|
249
239
|
FS.mkdir_p(File.dirname(dest))
|
|
250
240
|
FS.clonefile(artifact, dest)
|
|
251
241
|
end
|
|
252
242
|
end
|
|
253
243
|
|
|
244
|
+
def build_marker_path(gem_dir)
|
|
245
|
+
File.join(gem_dir, BUILD_MARKER)
|
|
246
|
+
end
|
|
247
|
+
|
|
254
248
|
def build_env(gem_dir, build_ruby_dir, make_jobs)
|
|
255
249
|
ruby_bin = File.join(build_ruby_dir, "bin")
|
|
256
250
|
path = [ruby_bin, ENV["PATH"]].compact.reject(&:empty?).join(File::PATH_SEPARATOR)
|
|
@@ -292,6 +286,7 @@ module Scint
|
|
|
292
286
|
|
|
293
287
|
Open3.popen2e(env, *cmd, **opts) do |stdin, out_err, wait_thr|
|
|
294
288
|
stdin.close
|
|
289
|
+
out_err.set_encoding("ASCII-8BIT")
|
|
295
290
|
|
|
296
291
|
out_err.each_line do |line|
|
|
297
292
|
stripped = line.rstrip
|
|
@@ -322,7 +317,7 @@ module Scint
|
|
|
322
317
|
|
|
323
318
|
private_class_method :find_extension_dirs, :compile_extension,
|
|
324
319
|
:compile_extconf, :compile_cmake, :compile_rake,
|
|
325
|
-
:find_rake_executable, :link_extensions,
|
|
320
|
+
:find_rake_executable, :link_extensions,
|
|
326
321
|
:build_env, :run_cmd, :prebuilt_missing_for_ruby?
|
|
327
322
|
end
|
|
328
323
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
require_relative "../fs"
|
|
4
4
|
require_relative "../platform"
|
|
5
5
|
require_relative "../spec_utils"
|
|
6
|
+
require_relative "../cache/layout"
|
|
7
|
+
require_relative "../cache/validity"
|
|
6
8
|
require "pathname"
|
|
7
9
|
|
|
8
10
|
module Scint
|
|
@@ -34,7 +36,7 @@ module Scint
|
|
|
34
36
|
# 1. Link gem files into gems/{full_name}/
|
|
35
37
|
gem_dest = File.join(ruby_dir, "gems", full_name)
|
|
36
38
|
unless Dir.exist?(gem_dest)
|
|
37
|
-
|
|
39
|
+
materialize_gem_dir(prepared_gem, gem_dest)
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# 2. Write gemspec into specifications/
|
|
@@ -60,6 +62,44 @@ module Scint
|
|
|
60
62
|
|
|
61
63
|
# --- private helpers ---
|
|
62
64
|
|
|
65
|
+
def materialize_gem_dir(prepared_gem, gem_dest)
|
|
66
|
+
manifest = cached_manifest_for(prepared_gem)
|
|
67
|
+
if manifest && manifest["files"].is_a?(Array)
|
|
68
|
+
FS.materialize_from_manifest(prepared_gem.extracted_path, gem_dest, manifest["files"])
|
|
69
|
+
else
|
|
70
|
+
FS.clone_tree(prepared_gem.extracted_path, gem_dest)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def cached_manifest_for(prepared_gem)
|
|
75
|
+
return nil unless prepared_gem.from_cache
|
|
76
|
+
|
|
77
|
+
layout = cache_layout_for(prepared_gem)
|
|
78
|
+
cached_path = layout.cached_path(prepared_gem.spec)
|
|
79
|
+
return nil unless File.expand_path(prepared_gem.extracted_path) == File.expand_path(cached_path)
|
|
80
|
+
|
|
81
|
+
manifest = Cache::Validity.read_manifest(layout.cached_manifest_path(prepared_gem.spec))
|
|
82
|
+
return nil unless manifest
|
|
83
|
+
return nil unless Cache::Validity.manifest_matches?(manifest, prepared_gem.spec, Platform.abi_key, layout)
|
|
84
|
+
|
|
85
|
+
manifest
|
|
86
|
+
rescue StandardError
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def cache_layout_for(prepared_gem)
|
|
91
|
+
extracted = File.expand_path(prepared_gem.extracted_path)
|
|
92
|
+
abi_dir = File.dirname(extracted)
|
|
93
|
+
cached_dir = File.dirname(abi_dir)
|
|
94
|
+
root = File.dirname(cached_dir)
|
|
95
|
+
|
|
96
|
+
if File.basename(abi_dir) == Platform.abi_key && File.basename(cached_dir) == "cached"
|
|
97
|
+
Cache::Layout.new(root: root)
|
|
98
|
+
else
|
|
99
|
+
Cache::Layout.new
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
63
103
|
def write_gemspec(prepared_gem, ruby_dir, full_name)
|
|
64
104
|
spec_dir = File.join(ruby_dir, "specifications")
|
|
65
105
|
FS.mkdir_p(spec_dir)
|
|
@@ -201,7 +241,8 @@ module Scint
|
|
|
201
241
|
RUBY
|
|
202
242
|
end
|
|
203
243
|
|
|
204
|
-
private_class_method :
|
|
244
|
+
private_class_method :materialize_gem_dir, :cached_manifest_for, :cache_layout_for,
|
|
245
|
+
:write_gemspec, :write_binstubs_impl, :write_ruby_bin_stub,
|
|
205
246
|
:write_bundle_bin_wrapper, :extract_executables,
|
|
206
247
|
:detect_executables_from_files, :augment_executable_metadata, :infer_bindir,
|
|
207
248
|
:minimal_gemspec
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "extension_builder"
|
|
4
4
|
require_relative "../platform"
|
|
5
|
+
require_relative "../cache/validity"
|
|
5
6
|
|
|
6
7
|
module Scint
|
|
7
8
|
module Installer
|
|
@@ -12,16 +13,16 @@ module Scint
|
|
|
12
13
|
# Compare resolved specs against what's already installed.
|
|
13
14
|
# Returns an Array of PlanEntry with action set to one of:
|
|
14
15
|
# :skip — already installed in bundle_path
|
|
15
|
-
# :link —
|
|
16
|
+
# :link — cached in global cache, just needs linking
|
|
16
17
|
# :download — needs downloading from remote
|
|
17
18
|
# :build_ext — has native extensions that need compiling
|
|
18
19
|
#
|
|
19
20
|
# Download entries are sorted largest-first so big gems start early,
|
|
20
21
|
# keeping the pipeline saturated while small gems fill in gaps.
|
|
21
|
-
def plan(resolved_specs, bundle_path, cache_layout)
|
|
22
|
+
def plan(resolved_specs, bundle_path, cache_layout, telemetry: nil)
|
|
22
23
|
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
23
24
|
entries = resolved_specs.map do |spec|
|
|
24
|
-
plan_one(spec, ruby_dir, cache_layout)
|
|
25
|
+
plan_one(spec, ruby_dir, cache_layout, telemetry: telemetry)
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Keep built-ins first, then downloads (big->small), then the rest.
|
|
@@ -32,7 +33,7 @@ module Scint
|
|
|
32
33
|
builtins + downloads + rest
|
|
33
34
|
end
|
|
34
35
|
|
|
35
|
-
def plan_one(spec, ruby_dir, cache_layout)
|
|
36
|
+
def plan_one(spec, ruby_dir, cache_layout, telemetry: nil)
|
|
36
37
|
full = cache_layout.full_name(spec)
|
|
37
38
|
gem_path = File.join(ruby_dir, "gems", full)
|
|
38
39
|
spec_path = File.join(ruby_dir, "specifications", "#{full}.gemspec")
|
|
@@ -48,10 +49,12 @@ module Scint
|
|
|
48
49
|
|
|
49
50
|
# Already installed? Require both gem files and specification.
|
|
50
51
|
if Dir.exist?(gem_path) && File.exist?(spec_path)
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
cache_source = Cache::Validity.source_path_for(spec, cache_layout, telemetry: telemetry)
|
|
53
|
+
if extension_link_missing?(spec, ruby_dir, cache_layout, cache_source)
|
|
53
54
|
action = ExtensionBuilder.cached_build_available?(spec, cache_layout) ? :link : :build_ext
|
|
54
|
-
return PlanEntry.new(spec: spec, action: action, cached_path:
|
|
55
|
+
return PlanEntry.new(spec: spec, action: action, cached_path: cache_source, gem_path: gem_path) if cache_source
|
|
56
|
+
|
|
57
|
+
return PlanEntry.new(spec: spec, action: :download, cached_path: nil, gem_path: gem_path)
|
|
55
58
|
end
|
|
56
59
|
|
|
57
60
|
return PlanEntry.new(spec: spec, action: :skip, cached_path: nil, gem_path: gem_path)
|
|
@@ -64,39 +67,32 @@ module Scint
|
|
|
64
67
|
return PlanEntry.new(spec: spec, action: action, cached_path: local_source, gem_path: gem_path)
|
|
65
68
|
end
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
return PlanEntry.new(spec: spec, action: action, cached_path: extracted, gem_path: gem_path)
|
|
70
|
+
cache_source = Cache::Validity.source_path_for(spec, cache_layout, telemetry: telemetry)
|
|
71
|
+
if cache_source
|
|
72
|
+
action = needs_ext_build?(spec, cache_layout, cache_source) ? :build_ext : :link
|
|
73
|
+
return PlanEntry.new(spec: spec, action: action, cached_path: cache_source, gem_path: gem_path)
|
|
72
74
|
end
|
|
73
75
|
|
|
74
76
|
# Needs downloading
|
|
75
77
|
PlanEntry.new(spec: spec, action: :download, cached_path: nil, gem_path: gem_path)
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
def needs_ext_build?(spec, cache_layout)
|
|
79
|
-
|
|
80
|
-
return false unless ExtensionBuilder.needs_build?(spec,
|
|
80
|
+
def needs_ext_build?(spec, cache_layout, source_dir)
|
|
81
|
+
return false unless source_dir
|
|
82
|
+
return false unless ExtensionBuilder.needs_build?(spec, source_dir)
|
|
81
83
|
|
|
82
84
|
!ExtensionBuilder.cached_build_available?(spec, cache_layout)
|
|
83
85
|
end
|
|
84
86
|
|
|
85
|
-
def extension_link_missing?(spec, ruby_dir, cache_layout)
|
|
86
|
-
|
|
87
|
-
return false unless
|
|
88
|
-
return false unless ExtensionBuilder.needs_build?(spec, extracted)
|
|
87
|
+
def extension_link_missing?(spec, ruby_dir, cache_layout, source_dir)
|
|
88
|
+
return false unless source_dir
|
|
89
|
+
return false unless ExtensionBuilder.needs_build?(spec, source_dir)
|
|
89
90
|
|
|
90
91
|
full = cache_layout.full_name(spec)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
Platform.extension_api_version,
|
|
96
|
-
full,
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
!Dir.exist?(ext_install_dir)
|
|
92
|
+
gem_dir = File.join(ruby_dir, "gems", full)
|
|
93
|
+
marker = File.join(gem_dir, ExtensionBuilder::BUILD_MARKER)
|
|
94
|
+
|
|
95
|
+
!File.exist?(marker)
|
|
100
96
|
end
|
|
101
97
|
|
|
102
98
|
# Rough size estimate for download ordering.
|