scint 0.6.0 → 0.7.0

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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../fs"
4
4
  require_relative "../platform"
5
+ require_relative "../spec_utils"
5
6
  require "pathname"
6
7
 
7
8
  module Scint
@@ -20,7 +21,7 @@ module Scint
20
21
  # Link gem files + gemspec only (no binstubs).
21
22
  # This allows binstubs to be scheduled as a separate DAG task.
22
23
  def link_files(prepared_gem, bundle_path)
23
- ruby_dir = ruby_install_dir(bundle_path)
24
+ ruby_dir = Platform.ruby_install_dir(bundle_path)
24
25
  link_files_to_ruby_dir(prepared_gem, ruby_dir)
25
26
  end
26
27
 
@@ -28,7 +29,7 @@ module Scint
28
29
  # This is used for the install-time hermetic build environment.
29
30
  def link_files_to_ruby_dir(prepared_gem, ruby_dir)
30
31
  spec = prepared_gem.spec
31
- full_name = spec_full_name(spec)
32
+ full_name = SpecUtils.full_name(spec)
32
33
 
33
34
  # 1. Link gem files into gems/{full_name}/
34
35
  gem_dest = File.join(ruby_dir, "gems", full_name)
@@ -42,9 +43,9 @@ module Scint
42
43
 
43
44
  # Write binstubs for one already-linked gem.
44
45
  def write_binstubs(prepared_gem, bundle_path)
45
- ruby_dir = ruby_install_dir(bundle_path)
46
+ ruby_dir = Platform.ruby_install_dir(bundle_path)
46
47
  spec = prepared_gem.spec
47
- full_name = spec_full_name(spec)
48
+ full_name = SpecUtils.full_name(spec)
48
49
  gem_dest = File.join(ruby_dir, "gems", full_name)
49
50
  return unless Dir.exist?(gem_dest)
50
51
 
@@ -101,7 +102,7 @@ module Scint
101
102
  #!/usr/bin/env ruby
102
103
  # frozen_string_literal: true
103
104
  #
104
- # This file was generated by scint for #{spec_full_name(spec)}
105
+ # This file was generated by scint for #{SpecUtils.full_name(spec)}
105
106
  #
106
107
  gem "#{spec.name}", "#{spec.version}"
107
108
  load Gem.bin_path("#{spec.name}", "#{exe_name}", "#{spec.version}")
@@ -193,34 +194,17 @@ module Scint
193
194
  Gem::Specification.new do |s|
194
195
  s.name = #{spec.name.inspect}
195
196
  s.version = #{spec.version.to_s.inspect}
196
- s.platform = #{platform_str(spec).inspect}
197
+ s.platform = #{SpecUtils.platform_str(spec).inspect}
197
198
  s.authors = ["scint"]
198
199
  s.summary = "Installed by scint"
199
200
  end
200
201
  RUBY
201
202
  end
202
203
 
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
204
  private_class_method :write_gemspec, :write_binstubs_impl, :write_ruby_bin_stub,
221
205
  :write_bundle_bin_wrapper, :extract_executables,
222
206
  :detect_executables_from_files, :augment_executable_metadata, :infer_bindir,
223
- :minimal_gemspec, :spec_full_name, :platform_str, :ruby_install_dir
207
+ :minimal_gemspec
224
208
  end
225
209
  end
226
210
  end
@@ -7,6 +7,7 @@ module Scint
7
7
  module Installer
8
8
  module Planner
9
9
  module_function
10
+ PATH_GLOB_DEFAULT = "{,*,*/*}.gemspec"
10
11
 
11
12
  # Compare resolved specs against what's already installed.
12
13
  # Returns an Array of PlanEntry with action set to one of:
@@ -18,7 +19,7 @@ module Scint
18
19
  # Download entries are sorted largest-first so big gems start early,
19
20
  # keeping the pipeline saturated while small gems fill in gaps.
20
21
  def plan(resolved_specs, bundle_path, cache_layout)
21
- ruby_dir = ruby_install_dir(bundle_path)
22
+ ruby_dir = Platform.ruby_install_dir(bundle_path)
22
23
  entries = resolved_specs.map do |spec|
23
24
  plan_one(spec, ruby_dir, cache_layout)
24
25
  end
@@ -98,10 +99,6 @@ module Scint
98
99
  !Dir.exist?(ext_install_dir)
99
100
  end
100
101
 
101
- def ruby_install_dir(bundle_path)
102
- File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
103
- end
104
-
105
102
  # Rough size estimate for download ordering.
106
103
  # If we don't know, use 0 so unknowns sort after large known gems.
107
104
  def estimated_size(spec)
@@ -131,11 +128,37 @@ module Scint
131
128
  return nil if source_str.end_with?(".git") || source_str.include?(".git/")
132
129
 
133
130
  absolute = File.expand_path(source_str, Dir.pwd)
134
- Dir.exist?(absolute) ? absolute : nil
131
+ return nil unless Dir.exist?(absolute)
132
+
133
+ spec_name =
134
+ if spec.respond_to?(:name)
135
+ spec.name.to_s
136
+ else
137
+ spec[:name].to_s
138
+ end
139
+ return absolute if spec_name.empty?
140
+ return absolute if File.exist?(File.join(absolute, "#{spec_name}.gemspec"))
141
+
142
+ glob =
143
+ if source.respond_to?(:glob) && !source.glob.to_s.empty?
144
+ source.glob.to_s
145
+ else
146
+ PATH_GLOB_DEFAULT
147
+ end
148
+
149
+ Dir.glob(File.join(absolute, glob)).each do |path|
150
+ return File.dirname(path) if File.basename(path, ".gemspec") == spec_name
151
+ end
152
+
153
+ Dir.glob(File.join(absolute, "**", "*.gemspec")).each do |path|
154
+ return File.dirname(path) if File.basename(path, ".gemspec") == spec_name
155
+ end
156
+
157
+ absolute
135
158
  end
136
159
 
137
160
  private_class_method :plan_one, :needs_ext_build?, :extension_link_missing?,
138
- :ruby_install_dir, :estimated_size, :local_source_path
161
+ :estimated_size, :local_source_path
139
162
  end
140
163
  end
141
164
  end
@@ -6,6 +6,7 @@ require_relative "../gem/extractor"
6
6
  require_relative "../cache/layout"
7
7
  require_relative "../fs"
8
8
  require_relative "../errors"
9
+ require_relative "../spec_utils"
9
10
 
10
11
  module Scint
11
12
  module Installer
@@ -182,15 +183,7 @@ module Scint
182
183
 
183
184
  def gem_download_uri(entry)
184
185
  spec = entry.spec
185
- name = spec.respond_to?(:name) ? spec.name : spec[:name]
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
186
+ filename = "#{SpecUtils.full_name(spec)}.gem"
194
187
 
195
188
  # Use cached_path if provided, otherwise construct from source
196
189
  if entry.cached_path
@@ -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 = platform == "ruby" ? "#{name}-#{version}" : "#{name}-#{version}-#{platform}"
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)
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../spec_utils"
4
+
3
5
  module Scint
4
6
  module Lockfile
5
7
  # Writes a standard Gemfile.lock file from structured data.
6
8
  # Produces output compatible with stock bundler.
7
9
  #
8
10
  # Sections in order: source blocks (GEM/GIT/PATH), PLATFORMS,
9
- # DEPENDENCIES, CHECKSUMS (if present), RUBY VERSION.
11
+ # DEPENDENCIES, CHECKSUMS (if present), RUBY VERSION, BUNDLED WITH.
10
12
  class Writer
11
13
  def self.write(lockfile_data)
12
14
  new(lockfile_data).generate
@@ -24,6 +26,7 @@ module Scint
24
26
  add_dependencies(out)
25
27
  add_checksums(out)
26
28
  add_ruby_version(out)
29
+ add_bundled_with(out)
27
30
 
28
31
  out
29
32
  end
@@ -32,67 +35,95 @@ module Scint
32
35
 
33
36
  def add_sources(out)
34
37
  # Group specs by source, preserving source order.
35
- # Specs store source as a URI string; sources are Source objects.
36
- # Match by checking if the spec's source URI matches any remote.
37
38
  specs_by_source = {}
38
39
  @data.sources.each { |s| specs_by_source[s] = [] }
39
40
 
40
41
  @data.specs.each do |spec|
41
- spec_src = spec.is_a?(Hash) ? spec[:source] : spec.source
42
- spec_uri = normalize_source_uri(spec_src)
43
-
44
- matched = @data.sources.find do |source|
45
- if source.respond_to?(:remotes)
46
- source.remotes.any? { |r| normalize_source_uri(r) == spec_uri }
47
- elsif source.respond_to?(:uri)
48
- normalize_source_uri(source.uri) == spec_uri
49
- else
50
- source == spec_src
51
- end
52
- end
42
+ target = match_source_for_spec(spec) || @data.sources.first
43
+ next unless target
53
44
 
54
- target = matched || @data.sources.first
55
45
  specs_by_source[target] ||= []
56
46
  specs_by_source[target] << spec
57
47
  end
58
48
 
59
- first = true
49
+ emitted = false
60
50
  @data.sources.each do |source|
61
- out << "\n" unless first
62
- first = false
51
+ source_specs = specs_by_source[source] || []
52
+ next if source_specs.empty?
53
+
54
+ out << "\n" if emitted
55
+ emitted = true
63
56
 
64
57
  out << source.to_lock
65
- add_specs(out, specs_by_source[source] || [])
58
+ add_specs(out, source_specs)
66
59
  end
67
60
  end
68
61
 
69
- def normalize_source_uri(uri)
70
- s = uri.to_s.chomp("/")
71
- s.sub(%r{^https?://}, "").downcase
62
+ def match_source_for_spec(spec)
63
+ spec_source = spec.is_a?(Hash) ? spec[:source] : spec.source
64
+ return nil unless spec_source
65
+
66
+ @data.sources.find { |source| source_matches?(source, spec_source) }
67
+ end
68
+
69
+ def source_matches?(source, spec_source)
70
+ return true if source.equal?(spec_source)
71
+ return true if source == spec_source
72
+
73
+ spec_key = normalize_source_key(spec_source)
74
+ return false unless spec_key
75
+
76
+ if source.respond_to?(:remotes)
77
+ source.remotes.any? { |remote| normalize_source_key(remote) == spec_key }
78
+ elsif source.respond_to?(:uri)
79
+ normalize_source_key(source.uri) == spec_key
80
+ else
81
+ normalize_source_key(source) == spec_key
82
+ end
83
+ end
84
+
85
+ def normalize_source_key(source_ref)
86
+ raw =
87
+ if source_ref.respond_to?(:uri)
88
+ source_ref.uri.to_s
89
+ elsif source_ref.respond_to?(:path)
90
+ source_ref.path.to_s
91
+ else
92
+ source_ref.to_s
93
+ end
94
+ return nil if raw.empty?
95
+
96
+ if raw.match?(%r{\Ahttps?://}i)
97
+ raw = raw.sub(%r{\Ahttps?://}i, "")
98
+ raw = raw.sub(%r{\.git/?\z}i, "")
99
+ raw.chomp("/").downcase
100
+ elsif raw.start_with?("/") || raw.start_with?(".")
101
+ File.expand_path(raw)
102
+ else
103
+ raw.sub(%r{\.git/?\z}i, "").chomp("/").downcase
104
+ end
72
105
  end
73
106
 
74
107
  def add_specs(out, specs)
75
108
  # Sort by full name (name-version-platform) for consistency
76
109
  sorted = specs.sort_by do |s|
77
110
  if s.is_a?(Hash)
78
- n = s[:name]
79
- v = s[:version]
80
- p = s[:platform]
81
- p == "ruby" ? "#{n}-#{v}" : "#{n}-#{v}-#{p}"
111
+ SpecUtils.full_name_for(s[:name], s[:version], s[:platform])
82
112
  else
83
- "#{s.name}-#{s.version}#{"-#{s.platform}" if s.platform != "ruby"}"
113
+ SpecUtils.full_name_for(s.name, s.version, s.platform)
84
114
  end
85
115
  end
86
116
 
87
117
  sorted.each do |spec|
88
- name, version, platform, deps = if spec.is_a?(Hash)
89
- [spec[:name], spec[:version], spec[:platform], spec[:dependencies] || []]
118
+ name, version, deps = if spec.is_a?(Hash)
119
+ [spec[:name], spec[:version], spec[:dependencies] || []]
90
120
  else
91
- [spec.name, spec.version, spec.platform, spec.dependencies || []]
121
+ [spec.name, spec.version, spec.dependencies || []]
92
122
  end
93
123
 
94
124
  # Format: " name (version)" or " name (version-platform)"
95
- version_str = platform && platform != "ruby" ? "#{version}-#{platform}" : version.to_s
125
+ platform_str = SpecUtils.platform_str(spec)
126
+ version_str = platform_str == "ruby" ? version.to_s : "#{version}-#{platform_str}"
96
127
  out << " #{name} (#{version_str})\n"
97
128
 
98
129
  # Dependencies of this spec (6-space indent)
@@ -152,19 +183,37 @@ module Scint
152
183
  return unless @data.checksums
153
184
  out << "\nCHECKSUMS\n"
154
185
 
155
- @data.checksums.sort.each do |key, values|
186
+ @data.checksums.each do |key, values|
187
+ rendered_key = format_checksum_key(key)
156
188
  if values && !values.empty?
157
- out << " #{key} #{values.join(",")}\n"
189
+ out << " #{rendered_key} #{values.join(",")}\n"
158
190
  else
159
- out << " #{key}\n"
191
+ out << " #{rendered_key}\n"
160
192
  end
161
193
  end
162
194
  end
163
195
 
196
+ def format_checksum_key(key)
197
+ match = key.to_s.match(/\A(.+)-(\d[^-]*)(?:-(.+))?\z/)
198
+ return key unless match
199
+
200
+ name = match[1]
201
+ version = match[2]
202
+ platform = match[3]
203
+ version_str = platform ? "#{version}-#{platform}" : version
204
+ "#{name} (#{version_str})"
205
+ end
206
+
164
207
  def add_ruby_version(out)
165
208
  return unless @data.ruby_version
166
209
  out << "\nRUBY VERSION\n"
167
- out << " #{@data.ruby_version}\n"
210
+ out << " #{@data.ruby_version}\n"
211
+ end
212
+
213
+ def add_bundled_with(out)
214
+ return unless @data.bundler_version
215
+ out << "\nBUNDLED WITH\n"
216
+ out << " #{@data.bundler_version}\n"
168
217
  end
169
218
 
170
219
  end
@@ -40,6 +40,14 @@ module Scint
40
40
  RUBY_VERSION
41
41
  end
42
42
 
43
+ def ruby_minor_version_dir
44
+ @ruby_minor_version_dir ||= RUBY_VERSION.split(".")[0, 2].join(".") + ".0"
45
+ end
46
+
47
+ def ruby_install_dir(base)
48
+ File.join(base, "ruby", ruby_minor_version_dir)
49
+ end
50
+
43
51
  def extension_api_version
44
52
  ::Gem.extension_api_version
45
53
  end
@@ -86,7 +86,7 @@ module Scint
86
86
  pg = @path_gems[name]
87
87
  deps = {}
88
88
  (pg[:dependencies] || []).each do |dep_name, dep_req_str|
89
- deps[dep_name] = Gem::Requirement.new(dep_req_str || ">= 0")
89
+ deps[dep_name] = build_requirement(dep_req_str)
90
90
  end
91
91
  deps
92
92
  else
@@ -99,7 +99,7 @@ module Scint
99
99
  next unless requirements_match?(reqs)
100
100
  dep_hash.each do |dep_name, dep_req_str|
101
101
  # Merge constraints from all matching platform entries
102
- req = Gem::Requirement.new(dep_req_str.split(", "))
102
+ req = build_requirement(dep_req_str)
103
103
  deps[dep_name] = if deps[dep_name]
104
104
  merge_requirements(deps[dep_name], req)
105
105
  else
@@ -276,6 +276,19 @@ module Scint
276
276
 
277
277
  Gem::Requirement.new(filtered.map { |op, version| "#{op} #{version}" })
278
278
  end
279
+
280
+ def build_requirement(value)
281
+ case value
282
+ when Gem::Requirement
283
+ value
284
+ when Array
285
+ parts = value.flatten.compact.map(&:to_s).reject(&:empty?)
286
+ Gem::Requirement.new(*(parts.empty? ? [">= 0"] : parts))
287
+ else
288
+ parts = value.to_s.split(",").map(&:strip).reject(&:empty?)
289
+ Gem::Requirement.new(*(parts.empty? ? [">= 0"] : parts))
290
+ end
291
+ end
279
292
  end
280
293
  end
281
294
  end
@@ -2,8 +2,10 @@
2
2
 
3
3
  require_relative "setup"
4
4
  require_relative "../fs"
5
+ require_relative "../platform"
5
6
  require "base64"
6
7
  require "pathname"
8
+ require "rbconfig"
7
9
 
8
10
  module Scint
9
11
  module Runtime
@@ -19,40 +21,40 @@ module Scint
19
21
  # lock_path: path to .bundle/scint.lock.marshal
20
22
  def exec(command, args, lock_path)
21
23
  original_env = ENV.to_hash
22
- lock_data = Setup.load_lock(lock_path)
24
+ Setup.load_lock(lock_path)
25
+ command, args = rewrite_bundle_exec(command, args)
26
+ passthrough_bundle = bundle_command?(command)
23
27
 
24
28
  bundle_dir = File.dirname(lock_path)
25
29
  scint_lib_dir = File.expand_path("../..", __dir__)
26
- ruby_dir = File.join(bundle_dir, "ruby",
27
- RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
28
-
29
- # Collect all load paths from the runtime config
30
- paths = []
31
- lock_data.each_value do |info|
32
- Array(info[:load_paths]).each do |p|
33
- paths << p if File.directory?(p)
34
- end
30
+ ruby_dir = Platform.ruby_install_dir(bundle_dir)
31
+
32
+ # Set RUBYLIB to make our Bundler shim loadable. We intentionally avoid
33
+ # injecting all gem load paths here because large apps can exceed exec
34
+ # argument/environment limits when RUBYLIB gets too long.
35
+ # Gem load paths are still activated via Scint::Runtime::Setup from
36
+ # `-rbundler/setup`.
37
+ unless passthrough_bundle
38
+ existing = ENV["RUBYLIB"]
39
+ rubylib = scint_lib_dir
40
+ rubylib = "#{rubylib}#{File::PATH_SEPARATOR}#{existing}" if existing && !existing.empty?
41
+ ENV["RUBYLIB"] = rubylib
35
42
  end
36
43
 
37
- # Ensure our bundler shim wins over global bundler.
38
- # Order matters: scint lib first, then gem load paths.
39
- paths.unshift(scint_lib_dir)
40
-
41
- # Set RUBYLIB so the child process inherits load paths.
42
- existing = ENV["RUBYLIB"]
43
- rubylib = paths.join(File::PATH_SEPARATOR)
44
- rubylib = "#{rubylib}#{File::PATH_SEPARATOR}#{existing}" if existing && !existing.empty?
45
- ENV["RUBYLIB"] = rubylib
46
-
47
44
  ENV["SCINT_RUNTIME_LOCK"] = lock_path
48
45
  ENV["GEM_HOME"] = ruby_dir
49
- ENV["GEM_PATH"] = ruby_dir
46
+ ENV["GEM_PATH"] = build_gem_path(ruby_dir, original_env["GEM_PATH"])
50
47
  ENV["BUNDLE_PATH"] = bundle_dir
51
48
  ENV["BUNDLE_APP_CONFIG"] = bundle_dir
52
49
  ENV["BUNDLE_GEMFILE"] = find_gemfile(bundle_dir)
53
- ENV["PATH"] = prepend_path(File.join(ruby_dir, "bin"), ENV["PATH"])
50
+ ruby_interpreter_bin = File.dirname(RbConfig.ruby)
51
+
52
+ # Keep interpreter/bin ahead of .bundle/bin so `#!/usr/bin/env ruby`
53
+ # resolves to the interpreter, not a gem-provided "ruby" executable.
54
54
  ENV["PATH"] = prepend_path(File.join(bundle_dir, "bin"), ENV["PATH"])
55
- prepend_rubyopt("-rbundler/setup")
55
+ ENV["PATH"] = prepend_path(File.join(ruby_dir, "bin"), ENV["PATH"])
56
+ ENV["PATH"] = prepend_path(ruby_interpreter_bin, ENV["PATH"])
57
+ prepend_rubyopt("-rbundler/setup") unless passthrough_bundle
56
58
  export_original_env(original_env)
57
59
 
58
60
  command = resolve_command(command, bundle_dir, ruby_dir)
@@ -76,9 +78,9 @@ module Scint
76
78
 
77
79
  def prepend_path(prefix, current_path)
78
80
  return prefix unless current_path && !current_path.empty?
79
- return current_path if current_path.split(File::PATH_SEPARATOR).include?(prefix)
80
-
81
- "#{prefix}#{File::PATH_SEPARATOR}#{current_path}"
81
+ parts = current_path.split(File::PATH_SEPARATOR).reject(&:empty?)
82
+ parts.delete(prefix)
83
+ ([prefix] + parts).join(File::PATH_SEPARATOR)
82
84
  end
83
85
 
84
86
  def export_original_env(original_env)
@@ -87,6 +89,29 @@ module Scint
87
89
  # Non-fatal: shim can fallback to current ENV.
88
90
  end
89
91
 
92
+ def build_gem_path(bundle_ruby_dir, original_gem_path)
93
+ paths = [bundle_ruby_dir]
94
+ if defined?(Gem) && Gem.respond_to?(:default_path)
95
+ paths.concat(Array(Gem.default_path))
96
+ end
97
+ if original_gem_path && !original_gem_path.empty?
98
+ paths.concat(original_gem_path.split(File::PATH_SEPARATOR))
99
+ end
100
+ paths.reject(&:empty?).uniq.join(File::PATH_SEPARATOR)
101
+ end
102
+
103
+ def bundle_command?(command)
104
+ File.basename(command.to_s) == "bundle"
105
+ end
106
+
107
+ def rewrite_bundle_exec(command, args)
108
+ return [command, args] unless bundle_command?(command)
109
+ return [command, args] unless args.first == "exec"
110
+ return [command, args] if args.length < 2
111
+
112
+ [args[1], args[2..] || []]
113
+ end
114
+
90
115
  def resolve_command(command, bundle_dir, ruby_dir)
91
116
  return command if command.include?(File::SEPARATOR)
92
117
 
@@ -135,6 +160,7 @@ module Scint
135
160
  end
136
161
 
137
162
  private_class_method :find_gemfile, :prepend_rubyopt, :prepend_path, :export_original_env,
163
+ :bundle_command?, :rewrite_bundle_exec, :build_gem_path,
138
164
  :resolve_command, :find_gem_executable, :write_bundle_exec_wrapper
139
165
  end
140
166
  end
@@ -29,6 +29,7 @@ module Scint
29
29
  $LOAD_PATH.unshift(*paths)
30
30
 
31
31
  ENV["BUNDLE_GEMFILE"] ||= find_gemfile(File.dirname(lock_path))
32
+ hydrate_loaded_specs(lock_data)
32
33
 
33
34
  lock_data
34
35
  end
@@ -39,7 +40,34 @@ module Scint
39
40
  File.exist?(gemfile) ? gemfile : nil
40
41
  end
41
42
 
42
- private_class_method :find_gemfile
43
+ def hydrate_loaded_specs(lock_data)
44
+ return unless defined?(Gem) && Gem.respond_to?(:loaded_specs)
45
+
46
+ lock_data.each do |name, info|
47
+ gem_name = name.to_s
48
+ next if gem_name.empty? || Gem.loaded_specs[gem_name]
49
+
50
+ version = info.is_a?(Hash) ? info[:version] : nil
51
+ spec = find_installed_spec(gem_name, version)
52
+ Gem.loaded_specs[gem_name] = spec if spec
53
+ end
54
+ rescue StandardError
55
+ # Best-effort compatibility with gems expecting Gem.loaded_specs.
56
+ end
57
+
58
+ def find_installed_spec(gem_name, version)
59
+ version_req = version.to_s.strip
60
+ if !version_req.empty?
61
+ exact = Gem::Specification.find_all_by_name(gem_name, version_req)
62
+ return exact.find { |spec| spec.version.to_s == version_req } || exact.first
63
+ end
64
+
65
+ Gem::Specification.find_all_by_name(gem_name).max_by(&:version)
66
+ rescue StandardError
67
+ nil
68
+ end
69
+
70
+ private_class_method :find_gemfile, :hydrate_loaded_specs, :find_installed_spec
43
71
  end
44
72
  end
45
73
  end
@@ -329,7 +329,12 @@ module Scint
329
329
  job.error = error
330
330
  @failed[job.id] = job
331
331
  @errors << { job_id: job.id, type: job.type, name: job.name, error: error }
332
- @aborted = true if @fail_fast
332
+ if @fail_fast
333
+ @aborted = true
334
+ # Drop queued work immediately; wait_all will return once current
335
+ # in-flight jobs drain.
336
+ @pending.clear
337
+ end
333
338
  else
334
339
  job.state = :completed
335
340
  @completed[job.id] = job
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scint
4
+ module SpecUtils
5
+ module_function
6
+
7
+ def name(spec)
8
+ spec.respond_to?(:name) ? spec.name : spec[:name]
9
+ end
10
+
11
+ def version(spec)
12
+ spec.respond_to?(:version) ? spec.version : spec[:version]
13
+ end
14
+
15
+ def platform(spec)
16
+ return spec.platform if spec.respond_to?(:platform)
17
+ return spec[:platform] if spec.is_a?(Hash)
18
+ return nil unless spec.respond_to?(:[])
19
+
20
+ spec[:platform]
21
+ rescue NameError, ArgumentError
22
+ nil
23
+ end
24
+
25
+ def platform_str(spec)
26
+ platform_value(platform(spec))
27
+ end
28
+
29
+ def platform_value(platform)
30
+ value = platform.nil? ? "ruby" : platform.to_s
31
+ value.empty? ? "ruby" : value
32
+ end
33
+
34
+ def full_name(spec)
35
+ base = "#{name(spec)}-#{version(spec)}"
36
+ plat = platform_str(spec)
37
+ return base if plat == "ruby"
38
+
39
+ "#{base}-#{plat}"
40
+ end
41
+
42
+ def full_name_for(name, version, platform = "ruby")
43
+ base = "#{name}-#{version}"
44
+ plat = platform_value(platform)
45
+ return base if plat == "ruby"
46
+
47
+ "#{base}-#{plat}"
48
+ end
49
+
50
+ def full_key(spec)
51
+ full_key_for(name(spec), version(spec), platform(spec))
52
+ end
53
+
54
+ def full_key_for(name, version, platform = "ruby")
55
+ "#{name}-#{version}-#{platform_value(platform)}"
56
+ end
57
+ end
58
+ end