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.
@@ -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; @dependencies; end
27
- def parsed_sources; @sources; end
28
- def parsed_ruby_version; @ruby_version; end
29
- def parsed_platforms; @declared_platforms; end
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
- @current_groups.concat(names.map(&:to_sym))
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(version, **opts)
217
- @ruby_version = version.to_s
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, "{,*}.gemspec"))
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(spec_name, path: dir)
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
- build_dir = File.join(tmpdir, "build")
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(build_dir)
41
+ FS.mkdir_p(build_root)
38
42
  FS.mkdir_p(install_dir)
39
43
 
40
- ext_dirs.each do |ext_dir|
41
- compile_extension(ext_dir, build_dir, install_dir, src_dir, spec, build_ruby_dir, compile_slots, output_tail)
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
- # Write marker
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
- link_extensions(cached_ext, ruby_dir, spec, abi_key)
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
- cached_ext = cache_layout.ext_path(spec, abi_key)
59
- Dir.exist?(cached_ext) && File.exist?(File.join(cached_ext, "gem.build_complete"))
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 global cache into bundle_path.
63
- # Returns true when cache was present and linked, false otherwise.
64
- def link_cached_build(prepared_gem, bundle_path, cache_layout, abi_key: Platform.abi_key)
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
- return false unless cached_build_available?(spec, cache_layout, abi_key: abi_key)
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
- Dir.glob(File.join(gem_dir, "ext", "**", "CMakeLists.txt")).each do |path|
125
- dir = File.dirname(path)
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: build_dir, output_tail: output_tail)
158
- run_cmd(env, "make", "-j#{make_jobs}", "-C", build_dir, output_tail: output_tail)
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: build_dir, output_tail: output_tail)
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
- return if Dir.exist?(ext_install_dir)
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
- FS.clone_tree(cached_ext, ext_install_dir)
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
- name = spec.name
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, :build_env, :run_cmd,
296
- :spec_full_name, :ruby_install_dir, :prebuilt_missing_for_ruby?
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 = spec_full_name(spec)
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
- FS.clone_tree(prepared_gem.extracted_path, gem_dest)
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 = spec_full_name(spec)
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 #{spec_full_name(spec)}
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
- def spec_full_name(spec)
204
- name = spec.name
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, :spec_full_name, :platform_str, :ruby_install_dir
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 — extracted in global cache, just needs linking
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
- if extension_link_missing?(spec, ruby_dir, cache_layout)
51
- extracted = cache_layout.extracted_path(spec)
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: extracted, gem_path: gem_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
- # Extracted in global cache?
67
- extracted = cache_layout.extracted_path(spec)
68
- if Dir.exist?(extracted)
69
- action = needs_ext_build?(spec, cache_layout) ? :build_ext : :link
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
- extracted = cache_layout.extracted_path(spec)
79
- return false unless ExtensionBuilder.needs_build?(spec, extracted)
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
- extracted = cache_layout.extracted_path(spec)
86
- return false unless Dir.exist?(extracted)
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
- ext_install_dir = File.join(
91
- ruby_dir,
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
- def ruby_install_dir(bundle_path)
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) ? absolute : nil
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
- :ruby_install_dir, :estimated_size, :local_source_path
157
+ :estimated_size, :local_source_path
139
158
  end
140
159
  end
141
160
  end