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
data/lib/scint/gemfile/parser.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "dependency"
|
|
4
|
+
require_relative "../source/path"
|
|
4
5
|
|
|
5
6
|
module Scint
|
|
6
7
|
module Gemfile
|
|
7
8
|
# Result of parsing a Gemfile.
|
|
8
|
-
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)
|
|
9
10
|
|
|
10
11
|
# Evaluates a Gemfile using instance_eval, just like stock bundler.
|
|
11
12
|
# Supports the full Gemfile DSL: source, gem, group, platforms, git_source,
|
|
@@ -19,14 +20,16 @@ module Scint
|
|
|
19
20
|
sources: parser.parsed_sources.uniq,
|
|
20
21
|
ruby_version: parser.parsed_ruby_version,
|
|
21
22
|
platforms: parser.parsed_platforms,
|
|
23
|
+
optional_groups: parser.parsed_optional_groups,
|
|
22
24
|
)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
# Accessors that don't collide with DSL method names
|
|
26
|
-
def parsed_dependencies;
|
|
27
|
-
def parsed_sources;
|
|
28
|
-
def parsed_ruby_version;
|
|
29
|
-
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
|
|
30
33
|
|
|
31
34
|
def initialize(gemfile_path)
|
|
32
35
|
@gemfile_path = File.expand_path(gemfile_path)
|
|
@@ -38,6 +41,7 @@ module Scint
|
|
|
38
41
|
@current_source_options = {}
|
|
39
42
|
@ruby_version = nil
|
|
40
43
|
@declared_platforms = []
|
|
44
|
+
@optional_groups = []
|
|
41
45
|
|
|
42
46
|
add_default_git_sources
|
|
43
47
|
end
|
|
@@ -134,6 +138,11 @@ module Scint
|
|
|
134
138
|
source_opts[:path] = path_val
|
|
135
139
|
end
|
|
136
140
|
|
|
141
|
+
# Internal/source metadata used by lockfile generation.
|
|
142
|
+
source_opts[:glob] = options.delete(:glob) if options.key?(:glob)
|
|
143
|
+
source_opts[:gemspec_generated] = options.delete(:gemspec_generated) if options.key?(:gemspec_generated)
|
|
144
|
+
source_opts[:gemspec_primary] = options.delete(:gemspec_primary) if options.key?(:gemspec_primary)
|
|
145
|
+
|
|
137
146
|
if options[:source]
|
|
138
147
|
source_opts[:source] = options.delete(:source)
|
|
139
148
|
end
|
|
@@ -161,7 +170,11 @@ module Scint
|
|
|
161
170
|
|
|
162
171
|
def group(*names, **opts, &blk)
|
|
163
172
|
old_groups = @current_groups.dup
|
|
164
|
-
|
|
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
|
|
165
178
|
yield
|
|
166
179
|
ensure
|
|
167
180
|
@current_groups = old_groups
|
|
@@ -213,24 +226,39 @@ module Scint
|
|
|
213
226
|
instance_eval(contents, expanded, 1)
|
|
214
227
|
end
|
|
215
228
|
|
|
216
|
-
def ruby(
|
|
217
|
-
|
|
229
|
+
def ruby(*versions, **opts)
|
|
230
|
+
version_parts = versions.flatten.compact.map(&:to_s)
|
|
231
|
+
@ruby_version = version_parts.join(", ") unless version_parts.empty?
|
|
218
232
|
end
|
|
219
233
|
|
|
220
234
|
def gemspec(opts = {})
|
|
221
235
|
path = opts[:path] || "."
|
|
222
236
|
name = opts[:name]
|
|
237
|
+
glob = opts[:glob] || Scint::Source::Path::DEFAULT_GLOB
|
|
223
238
|
dir = File.expand_path(path, File.dirname(@gemfile_path))
|
|
224
|
-
gemspecs = Dir.glob(File.join(dir,
|
|
239
|
+
gemspecs = Dir.glob(File.join(dir, glob)).sort
|
|
225
240
|
# Just record we have a gemspec source -- full spec loading is
|
|
226
241
|
# deferred to the resolver/installer.
|
|
227
242
|
gemspecs.each do |gs|
|
|
228
243
|
spec_name = File.basename(gs, ".gemspec")
|
|
229
244
|
next if name && spec_name != name
|
|
230
|
-
gem(
|
|
245
|
+
gem(
|
|
246
|
+
spec_name,
|
|
247
|
+
path: File.dirname(gs),
|
|
248
|
+
glob: glob,
|
|
249
|
+
gemspec_generated: true,
|
|
250
|
+
gemspec_primary: File.expand_path(File.dirname(gs)) == dir,
|
|
251
|
+
)
|
|
231
252
|
end
|
|
232
253
|
end
|
|
233
254
|
|
|
255
|
+
def install_if(*conditions, &blk)
|
|
256
|
+
raise GemfileError, "install_if requires a block" unless block_given?
|
|
257
|
+
return unless conditions.all? { |condition| condition_truthy?(condition) }
|
|
258
|
+
|
|
259
|
+
yield
|
|
260
|
+
end
|
|
261
|
+
|
|
234
262
|
# Silently ignore plugin declarations
|
|
235
263
|
def plugin(*args); end
|
|
236
264
|
|
|
@@ -247,6 +275,12 @@ module Scint
|
|
|
247
275
|
|
|
248
276
|
private
|
|
249
277
|
|
|
278
|
+
def condition_truthy?(condition)
|
|
279
|
+
return condition.call if condition.respond_to?(:call)
|
|
280
|
+
|
|
281
|
+
!!condition
|
|
282
|
+
end
|
|
283
|
+
|
|
250
284
|
def add_default_git_sources
|
|
251
285
|
git_source(:github) do |repo_name|
|
|
252
286
|
if repo_name =~ %r{\Ahttps://github\.com/([^/]+/[^/]+)/pull/(\d+)\z}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../fs"
|
|
4
4
|
require_relative "../platform"
|
|
5
5
|
require_relative "../errors"
|
|
6
|
+
require_relative "../spec_utils"
|
|
6
7
|
require "open3"
|
|
7
8
|
|
|
8
9
|
module Scint
|
|
@@ -10,65 +11,63 @@ module Scint
|
|
|
10
11
|
module ExtensionBuilder
|
|
11
12
|
module_function
|
|
12
13
|
|
|
14
|
+
BUILD_MARKER = ".scint.build_complete"
|
|
15
|
+
|
|
13
16
|
# Build native extensions for a prepared gem.
|
|
14
17
|
# prepared_gem: PreparedGem struct
|
|
15
18
|
# bundle_path: .bundle/ root
|
|
16
19
|
# abi_key: e.g. "ruby-3.3.0-arm64-darwin24" (defaults to Platform.abi_key)
|
|
17
20
|
def build(prepared_gem, bundle_path, cache_layout, abi_key: Platform.abi_key, compile_slots: 1, output_tail: nil)
|
|
18
21
|
spec = prepared_gem.spec
|
|
19
|
-
ruby_dir = ruby_install_dir(bundle_path)
|
|
20
22
|
build_ruby_dir = cache_layout.install_ruby_dir
|
|
21
|
-
|
|
22
|
-
# Check global extension cache first
|
|
23
|
-
cached_ext = cache_layout.ext_path(spec, abi_key)
|
|
24
|
-
if Dir.exist?(cached_ext) && File.exist?(File.join(cached_ext, "gem.build_complete"))
|
|
25
|
-
link_extensions(cached_ext, ruby_dir, spec, abi_key)
|
|
26
|
-
return true
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Build in a temp dir, then cache
|
|
30
23
|
src_dir = prepared_gem.extracted_path
|
|
31
|
-
ext_dirs = find_extension_dirs(src_dir)
|
|
32
|
-
raise ExtensionBuildError, "No extension directories found for #{spec.name}" if ext_dirs.empty?
|
|
33
24
|
|
|
25
|
+
marker = build_marker_path(src_dir)
|
|
26
|
+
return true if File.exist?(marker)
|
|
27
|
+
|
|
28
|
+
# Build in a temp dir, then sync artifacts into the source tree.
|
|
34
29
|
FS.with_tempdir("scint-ext") do |tmpdir|
|
|
35
|
-
|
|
30
|
+
# Stage the full gem source tree in an isolated workspace.
|
|
31
|
+
# Many extconf scripts use paths like ../../vendor relative to ext/,
|
|
32
|
+
# which only work when the full gem layout is preserved.
|
|
33
|
+
staged_src_dir = File.join(tmpdir, "source")
|
|
34
|
+
FS.clone_tree(src_dir, staged_src_dir)
|
|
35
|
+
|
|
36
|
+
ext_dirs = find_extension_dirs(staged_src_dir)
|
|
37
|
+
raise ExtensionBuildError, "No extension directories found for #{spec.name}" if ext_dirs.empty?
|
|
38
|
+
|
|
39
|
+
build_root = File.join(tmpdir, "build")
|
|
36
40
|
install_dir = File.join(tmpdir, "install")
|
|
37
|
-
FS.mkdir_p(
|
|
41
|
+
FS.mkdir_p(build_root)
|
|
38
42
|
FS.mkdir_p(install_dir)
|
|
39
43
|
|
|
40
|
-
ext_dirs.
|
|
41
|
-
|
|
44
|
+
ext_dirs.each_with_index do |ext_dir, idx|
|
|
45
|
+
# Keep isolated build trees per extension directory. Some gems
|
|
46
|
+
# invoke multiple CMake projects under ext/ and CMake caches are
|
|
47
|
+
# source-tree specific.
|
|
48
|
+
ext_build_dir = File.join(build_root, idx.to_s)
|
|
49
|
+
FS.mkdir_p(ext_build_dir)
|
|
50
|
+
compile_extension(ext_dir, ext_build_dir, install_dir, staged_src_dir, spec, build_ruby_dir, compile_slots, output_tail)
|
|
42
51
|
end
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
File.write(File.join(install_dir, "gem.build_complete"), "")
|
|
46
|
-
|
|
47
|
-
# Cache globally
|
|
48
|
-
FS.mkdir_p(File.dirname(cached_ext))
|
|
49
|
-
FS.atomic_move(install_dir, cached_ext)
|
|
53
|
+
sync_extensions_into_gem(install_dir, src_dir)
|
|
50
54
|
end
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
File.write(marker, "")
|
|
53
57
|
true
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
# True when a completed global extension build exists for this spec + ABI.
|
|
57
61
|
def cached_build_available?(spec, cache_layout, abi_key: Platform.abi_key)
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
cached_dir = cache_layout.cached_path(spec, abi_key)
|
|
63
|
+
File.exist?(build_marker_path(cached_dir))
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
# Link already-compiled extensions from
|
|
63
|
-
# Returns true when cache
|
|
64
|
-
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)
|
|
65
69
|
spec = prepared_gem.spec
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
ruby_dir = ruby_install_dir(bundle_path)
|
|
69
|
-
cached_ext = cache_layout.ext_path(spec, abi_key)
|
|
70
|
-
link_extensions(cached_ext, ruby_dir, spec, abi_key)
|
|
71
|
-
true
|
|
70
|
+
cached_build_available?(spec, cache_layout, abi_key: abi_key)
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
# True when a gem has native extension sources that need compiling.
|
|
@@ -120,9 +119,18 @@ module Scint
|
|
|
120
119
|
dirs << File.dirname(path)
|
|
121
120
|
end
|
|
122
121
|
|
|
123
|
-
# CMakeLists.txt in ext
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
# CMakeLists.txt in ext/. Keep only top-level CMake roots, so vendored
|
|
123
|
+
# subprojects (e.g. deps/*) are not built standalone.
|
|
124
|
+
cmake_dirs = Dir.glob(File.join(gem_dir, "ext", "**", "CMakeLists.txt"))
|
|
125
|
+
.map { |path| File.dirname(path) }
|
|
126
|
+
.uniq
|
|
127
|
+
.sort_by { |dir| [dir.length, dir] }
|
|
128
|
+
cmake_roots = []
|
|
129
|
+
cmake_dirs.each do |dir|
|
|
130
|
+
next if cmake_roots.any? { |root| dir.start_with?("#{root}/") }
|
|
131
|
+
cmake_roots << dir
|
|
132
|
+
end
|
|
133
|
+
cmake_roots.each do |dir|
|
|
126
134
|
dirs << dir unless dirs.include?(dir)
|
|
127
135
|
end
|
|
128
136
|
|
|
@@ -141,7 +149,7 @@ module Scint
|
|
|
141
149
|
env = build_env(gem_dir, build_ruby_dir, make_jobs)
|
|
142
150
|
|
|
143
151
|
if File.exist?(File.join(ext_dir, "extconf.rb"))
|
|
144
|
-
compile_extconf(ext_dir, build_dir, install_dir, env, make_jobs, output_tail)
|
|
152
|
+
compile_extconf(ext_dir, gem_dir, build_dir, install_dir, env, make_jobs, output_tail)
|
|
145
153
|
elsif File.exist?(File.join(ext_dir, "CMakeLists.txt"))
|
|
146
154
|
compile_cmake(ext_dir, build_dir, install_dir, env, make_jobs, output_tail)
|
|
147
155
|
elsif File.exist?(File.join(ext_dir, "Rakefile"))
|
|
@@ -151,13 +159,18 @@ module Scint
|
|
|
151
159
|
end
|
|
152
160
|
end
|
|
153
161
|
|
|
154
|
-
def compile_extconf(ext_dir, build_dir, install_dir, env, make_jobs, output_tail = nil)
|
|
162
|
+
def compile_extconf(ext_dir, gem_dir, build_dir, install_dir, env, make_jobs, output_tail = nil)
|
|
163
|
+
# Build in-place within the staged ext directory so extconf scripts
|
|
164
|
+
# that navigate relative paths (../../vendor, ../..) behave like
|
|
165
|
+
# Bundler's install layout.
|
|
166
|
+
_ = gem_dir
|
|
167
|
+
_ = build_dir
|
|
155
168
|
run_cmd(env, RbConfig.ruby, File.join(ext_dir, "extconf.rb"),
|
|
156
169
|
"--with-opt-dir=#{RbConfig::CONFIG["prefix"]}",
|
|
157
|
-
chdir:
|
|
158
|
-
run_cmd(env, "make", "-j#{make_jobs}", "-C",
|
|
170
|
+
chdir: ext_dir, output_tail: output_tail)
|
|
171
|
+
run_cmd(env, "make", "-j#{make_jobs}", "-C", ext_dir, output_tail: output_tail)
|
|
159
172
|
run_cmd(env, "make", "install", "DESTDIR=", "sitearchdir=#{install_dir}", "sitelibdir=#{install_dir}",
|
|
160
|
-
chdir:
|
|
173
|
+
chdir: ext_dir, output_tail: output_tail)
|
|
161
174
|
end
|
|
162
175
|
|
|
163
176
|
def compile_cmake(ext_dir, build_dir, install_dir, env, make_jobs, output_tail = nil)
|
|
@@ -208,9 +221,28 @@ module Scint
|
|
|
208
221
|
ext_install_dir = File.join(ruby_dir, "extensions",
|
|
209
222
|
Platform.gem_arch, Platform.extension_api_version,
|
|
210
223
|
spec_full_name(spec))
|
|
211
|
-
|
|
224
|
+
FS.clone_tree(cached_ext, ext_install_dir) unless Dir.exist?(ext_install_dir)
|
|
225
|
+
gem_dir = File.join(ruby_dir, "gems", spec_full_name(spec))
|
|
226
|
+
sync_extensions_into_gem(cached_ext, gem_dir)
|
|
227
|
+
end
|
|
212
228
|
|
|
213
|
-
|
|
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)
|
|
233
|
+
lib_dir = File.join(gem_dir, "lib")
|
|
234
|
+
FS.mkdir_p(lib_dir)
|
|
235
|
+
|
|
236
|
+
Dir.glob(File.join(cached_ext, "**", "*.{so,bundle,dll,dylib}")).each do |artifact|
|
|
237
|
+
rel = artifact.delete_prefix("#{cached_ext}/")
|
|
238
|
+
dest = File.join(lib_dir, rel)
|
|
239
|
+
FS.mkdir_p(File.dirname(dest))
|
|
240
|
+
FS.clonefile(artifact, dest)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def build_marker_path(gem_dir)
|
|
245
|
+
File.join(gem_dir, BUILD_MARKER)
|
|
214
246
|
end
|
|
215
247
|
|
|
216
248
|
def build_env(gem_dir, build_ruby_dir, make_jobs)
|
|
@@ -254,6 +286,7 @@ module Scint
|
|
|
254
286
|
|
|
255
287
|
Open3.popen2e(env, *cmd, **opts) do |stdin, out_err, wait_thr|
|
|
256
288
|
stdin.close
|
|
289
|
+
out_err.set_encoding("ASCII-8BIT")
|
|
257
290
|
|
|
258
291
|
out_err.each_line do |line|
|
|
259
292
|
stripped = line.rstrip
|
|
@@ -279,21 +312,13 @@ module Scint
|
|
|
279
312
|
end
|
|
280
313
|
|
|
281
314
|
def spec_full_name(spec)
|
|
282
|
-
|
|
283
|
-
version = spec.version
|
|
284
|
-
plat = spec.respond_to?(:platform) ? spec.platform : nil
|
|
285
|
-
base = "#{name}-#{version}"
|
|
286
|
-
(plat.nil? || plat.to_s == "ruby" || plat.to_s.empty?) ? base : "#{base}-#{plat}"
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def ruby_install_dir(bundle_path)
|
|
290
|
-
File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
315
|
+
SpecUtils.full_name(spec)
|
|
291
316
|
end
|
|
292
317
|
|
|
293
318
|
private_class_method :find_extension_dirs, :compile_extension,
|
|
294
319
|
:compile_extconf, :compile_cmake, :compile_rake,
|
|
295
|
-
:find_rake_executable, :link_extensions,
|
|
296
|
-
:
|
|
320
|
+
:find_rake_executable, :link_extensions,
|
|
321
|
+
:build_env, :run_cmd, :prebuilt_missing_for_ruby?
|
|
297
322
|
end
|
|
298
323
|
end
|
|
299
324
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../fs"
|
|
4
4
|
require_relative "../platform"
|
|
5
|
+
require_relative "../spec_utils"
|
|
6
|
+
require_relative "../cache/layout"
|
|
7
|
+
require_relative "../cache/validity"
|
|
5
8
|
require "pathname"
|
|
6
9
|
|
|
7
10
|
module Scint
|
|
@@ -20,7 +23,7 @@ module Scint
|
|
|
20
23
|
# Link gem files + gemspec only (no binstubs).
|
|
21
24
|
# This allows binstubs to be scheduled as a separate DAG task.
|
|
22
25
|
def link_files(prepared_gem, bundle_path)
|
|
23
|
-
ruby_dir = ruby_install_dir(bundle_path)
|
|
26
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
24
27
|
link_files_to_ruby_dir(prepared_gem, ruby_dir)
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -28,12 +31,12 @@ module Scint
|
|
|
28
31
|
# This is used for the install-time hermetic build environment.
|
|
29
32
|
def link_files_to_ruby_dir(prepared_gem, ruby_dir)
|
|
30
33
|
spec = prepared_gem.spec
|
|
31
|
-
full_name =
|
|
34
|
+
full_name = SpecUtils.full_name(spec)
|
|
32
35
|
|
|
33
36
|
# 1. Link gem files into gems/{full_name}/
|
|
34
37
|
gem_dest = File.join(ruby_dir, "gems", full_name)
|
|
35
38
|
unless Dir.exist?(gem_dest)
|
|
36
|
-
|
|
39
|
+
materialize_gem_dir(prepared_gem, gem_dest)
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
# 2. Write gemspec into specifications/
|
|
@@ -42,9 +45,9 @@ module Scint
|
|
|
42
45
|
|
|
43
46
|
# Write binstubs for one already-linked gem.
|
|
44
47
|
def write_binstubs(prepared_gem, bundle_path)
|
|
45
|
-
ruby_dir = ruby_install_dir(bundle_path)
|
|
48
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
46
49
|
spec = prepared_gem.spec
|
|
47
|
-
full_name =
|
|
50
|
+
full_name = SpecUtils.full_name(spec)
|
|
48
51
|
gem_dest = File.join(ruby_dir, "gems", full_name)
|
|
49
52
|
return unless Dir.exist?(gem_dest)
|
|
50
53
|
|
|
@@ -59,6 +62,44 @@ module Scint
|
|
|
59
62
|
|
|
60
63
|
# --- private helpers ---
|
|
61
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
|
+
|
|
62
103
|
def write_gemspec(prepared_gem, ruby_dir, full_name)
|
|
63
104
|
spec_dir = File.join(ruby_dir, "specifications")
|
|
64
105
|
FS.mkdir_p(spec_dir)
|
|
@@ -101,7 +142,7 @@ module Scint
|
|
|
101
142
|
#!/usr/bin/env ruby
|
|
102
143
|
# frozen_string_literal: true
|
|
103
144
|
#
|
|
104
|
-
# This file was generated by scint for #{
|
|
145
|
+
# This file was generated by scint for #{SpecUtils.full_name(spec)}
|
|
105
146
|
#
|
|
106
147
|
gem "#{spec.name}", "#{spec.version}"
|
|
107
148
|
load Gem.bin_path("#{spec.name}", "#{exe_name}", "#{spec.version}")
|
|
@@ -193,34 +234,18 @@ module Scint
|
|
|
193
234
|
Gem::Specification.new do |s|
|
|
194
235
|
s.name = #{spec.name.inspect}
|
|
195
236
|
s.version = #{spec.version.to_s.inspect}
|
|
196
|
-
s.platform = #{platform_str(spec).inspect}
|
|
237
|
+
s.platform = #{SpecUtils.platform_str(spec).inspect}
|
|
197
238
|
s.authors = ["scint"]
|
|
198
239
|
s.summary = "Installed by scint"
|
|
199
240
|
end
|
|
200
241
|
RUBY
|
|
201
242
|
end
|
|
202
243
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
version = spec.version
|
|
206
|
-
plat = platform_str(spec)
|
|
207
|
-
base = "#{name}-#{version}"
|
|
208
|
-
(plat == "ruby" || plat.empty?) ? base : "#{base}-#{plat}"
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
def platform_str(spec)
|
|
212
|
-
p = spec.respond_to?(:platform) ? spec.platform : nil
|
|
213
|
-
p.nil? ? "ruby" : p.to_s
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def ruby_install_dir(bundle_path)
|
|
217
|
-
File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
private_class_method :write_gemspec, :write_binstubs_impl, :write_ruby_bin_stub,
|
|
244
|
+
private_class_method :materialize_gem_dir, :cached_manifest_for, :cache_layout_for,
|
|
245
|
+
:write_gemspec, :write_binstubs_impl, :write_ruby_bin_stub,
|
|
221
246
|
:write_bundle_bin_wrapper, :extract_executables,
|
|
222
247
|
:detect_executables_from_files, :augment_executable_metadata, :infer_bindir,
|
|
223
|
-
:minimal_gemspec
|
|
248
|
+
:minimal_gemspec
|
|
224
249
|
end
|
|
225
250
|
end
|
|
226
251
|
end
|
|
@@ -2,25 +2,27 @@
|
|
|
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
|
|
8
9
|
module Planner
|
|
9
10
|
module_function
|
|
11
|
+
PATH_GLOB_DEFAULT = "{,*,*/*}.gemspec"
|
|
10
12
|
|
|
11
13
|
# Compare resolved specs against what's already installed.
|
|
12
14
|
# Returns an Array of PlanEntry with action set to one of:
|
|
13
15
|
# :skip — already installed in bundle_path
|
|
14
|
-
# :link —
|
|
16
|
+
# :link — cached in global cache, just needs linking
|
|
15
17
|
# :download — needs downloading from remote
|
|
16
18
|
# :build_ext — has native extensions that need compiling
|
|
17
19
|
#
|
|
18
20
|
# Download entries are sorted largest-first so big gems start early,
|
|
19
21
|
# keeping the pipeline saturated while small gems fill in gaps.
|
|
20
|
-
def plan(resolved_specs, bundle_path, cache_layout)
|
|
21
|
-
ruby_dir = ruby_install_dir(bundle_path)
|
|
22
|
+
def plan(resolved_specs, bundle_path, cache_layout, telemetry: nil)
|
|
23
|
+
ruby_dir = Platform.ruby_install_dir(bundle_path)
|
|
22
24
|
entries = resolved_specs.map do |spec|
|
|
23
|
-
plan_one(spec, ruby_dir, cache_layout)
|
|
25
|
+
plan_one(spec, ruby_dir, cache_layout, telemetry: telemetry)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
# Keep built-ins first, then downloads (big->small), then the rest.
|
|
@@ -31,7 +33,7 @@ module Scint
|
|
|
31
33
|
builtins + downloads + rest
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def plan_one(spec, ruby_dir, cache_layout)
|
|
36
|
+
def plan_one(spec, ruby_dir, cache_layout, telemetry: nil)
|
|
35
37
|
full = cache_layout.full_name(spec)
|
|
36
38
|
gem_path = File.join(ruby_dir, "gems", full)
|
|
37
39
|
spec_path = File.join(ruby_dir, "specifications", "#{full}.gemspec")
|
|
@@ -47,10 +49,12 @@ module Scint
|
|
|
47
49
|
|
|
48
50
|
# Already installed? Require both gem files and specification.
|
|
49
51
|
if Dir.exist?(gem_path) && File.exist?(spec_path)
|
|
50
|
-
|
|
51
|
-
|
|
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)
|
|
52
54
|
action = ExtensionBuilder.cached_build_available?(spec, cache_layout) ? :link : :build_ext
|
|
53
|
-
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)
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
return PlanEntry.new(spec: spec, action: :skip, cached_path: nil, gem_path: gem_path)
|
|
@@ -63,43 +67,32 @@ module Scint
|
|
|
63
67
|
return PlanEntry.new(spec: spec, action: action, cached_path: local_source, gem_path: gem_path)
|
|
64
68
|
end
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
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)
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
# Needs downloading
|
|
74
77
|
PlanEntry.new(spec: spec, action: :download, cached_path: nil, gem_path: gem_path)
|
|
75
78
|
end
|
|
76
79
|
|
|
77
|
-
def needs_ext_build?(spec, cache_layout)
|
|
78
|
-
|
|
79
|
-
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)
|
|
80
83
|
|
|
81
84
|
!ExtensionBuilder.cached_build_available?(spec, cache_layout)
|
|
82
85
|
end
|
|
83
86
|
|
|
84
|
-
def extension_link_missing?(spec, ruby_dir, cache_layout)
|
|
85
|
-
|
|
86
|
-
return false unless
|
|
87
|
-
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)
|
|
88
90
|
|
|
89
91
|
full = cache_layout.full_name(spec)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"extensions",
|
|
93
|
-
Platform.gem_arch,
|
|
94
|
-
Platform.extension_api_version,
|
|
95
|
-
full,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
!Dir.exist?(ext_install_dir)
|
|
99
|
-
end
|
|
92
|
+
gem_dir = File.join(ruby_dir, "gems", full)
|
|
93
|
+
marker = File.join(gem_dir, ExtensionBuilder::BUILD_MARKER)
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
95
|
+
!File.exist?(marker)
|
|
103
96
|
end
|
|
104
97
|
|
|
105
98
|
# Rough size estimate for download ordering.
|
|
@@ -131,11 +124,37 @@ module Scint
|
|
|
131
124
|
return nil if source_str.end_with?(".git") || source_str.include?(".git/")
|
|
132
125
|
|
|
133
126
|
absolute = File.expand_path(source_str, Dir.pwd)
|
|
134
|
-
Dir.exist?(absolute)
|
|
127
|
+
return nil unless Dir.exist?(absolute)
|
|
128
|
+
|
|
129
|
+
spec_name =
|
|
130
|
+
if spec.respond_to?(:name)
|
|
131
|
+
spec.name.to_s
|
|
132
|
+
else
|
|
133
|
+
spec[:name].to_s
|
|
134
|
+
end
|
|
135
|
+
return absolute if spec_name.empty?
|
|
136
|
+
return absolute if File.exist?(File.join(absolute, "#{spec_name}.gemspec"))
|
|
137
|
+
|
|
138
|
+
glob =
|
|
139
|
+
if source.respond_to?(:glob) && !source.glob.to_s.empty?
|
|
140
|
+
source.glob.to_s
|
|
141
|
+
else
|
|
142
|
+
PATH_GLOB_DEFAULT
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
Dir.glob(File.join(absolute, glob)).each do |path|
|
|
146
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == spec_name
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
Dir.glob(File.join(absolute, "**", "*.gemspec")).each do |path|
|
|
150
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == spec_name
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
absolute
|
|
135
154
|
end
|
|
136
155
|
|
|
137
156
|
private_class_method :plan_one, :needs_ext_build?, :extension_link_missing?,
|
|
138
|
-
:
|
|
157
|
+
:estimated_size, :local_source_path
|
|
139
158
|
end
|
|
140
159
|
end
|
|
141
160
|
end
|