scint 0.1.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.
- checksums.yaml +7 -0
- data/FEATURES.md +13 -0
- data/README.md +216 -0
- data/bin/bundler-vs-scint +233 -0
- data/bin/scint +35 -0
- data/bin/scint-io-summary +46 -0
- data/bin/scint-syscall-trace +41 -0
- data/lib/bundler/setup.rb +5 -0
- data/lib/bundler.rb +168 -0
- data/lib/scint/cache/layout.rb +131 -0
- data/lib/scint/cache/metadata_store.rb +75 -0
- data/lib/scint/cache/prewarm.rb +192 -0
- data/lib/scint/cli/add.rb +85 -0
- data/lib/scint/cli/cache.rb +316 -0
- data/lib/scint/cli/exec.rb +150 -0
- data/lib/scint/cli/install.rb +1047 -0
- data/lib/scint/cli/remove.rb +60 -0
- data/lib/scint/cli.rb +77 -0
- data/lib/scint/commands/exec.rb +17 -0
- data/lib/scint/commands/install.rb +17 -0
- data/lib/scint/credentials.rb +153 -0
- data/lib/scint/debug/io_trace.rb +218 -0
- data/lib/scint/debug/sampler.rb +138 -0
- data/lib/scint/downloader/fetcher.rb +113 -0
- data/lib/scint/downloader/pool.rb +112 -0
- data/lib/scint/errors.rb +63 -0
- data/lib/scint/fs.rb +119 -0
- data/lib/scint/gem/extractor.rb +86 -0
- data/lib/scint/gem/package.rb +62 -0
- data/lib/scint/gemfile/dependency.rb +30 -0
- data/lib/scint/gemfile/editor.rb +93 -0
- data/lib/scint/gemfile/parser.rb +275 -0
- data/lib/scint/index/cache.rb +166 -0
- data/lib/scint/index/client.rb +301 -0
- data/lib/scint/index/parser.rb +142 -0
- data/lib/scint/installer/extension_builder.rb +264 -0
- data/lib/scint/installer/linker.rb +226 -0
- data/lib/scint/installer/planner.rb +140 -0
- data/lib/scint/installer/preparer.rb +207 -0
- data/lib/scint/lockfile/parser.rb +251 -0
- data/lib/scint/lockfile/writer.rb +178 -0
- data/lib/scint/platform.rb +71 -0
- data/lib/scint/progress.rb +579 -0
- data/lib/scint/resolver/provider.rb +230 -0
- data/lib/scint/resolver/resolver.rb +249 -0
- data/lib/scint/runtime/exec.rb +141 -0
- data/lib/scint/runtime/setup.rb +45 -0
- data/lib/scint/scheduler.rb +392 -0
- data/lib/scint/source/base.rb +46 -0
- data/lib/scint/source/git.rb +92 -0
- data/lib/scint/source/path.rb +70 -0
- data/lib/scint/source/rubygems.rb +79 -0
- data/lib/scint/vendor/pub_grub/assignment.rb +20 -0
- data/lib/scint/vendor/pub_grub/basic_package_source.rb +169 -0
- data/lib/scint/vendor/pub_grub/failure_writer.rb +182 -0
- data/lib/scint/vendor/pub_grub/incompatibility.rb +150 -0
- data/lib/scint/vendor/pub_grub/package.rb +43 -0
- data/lib/scint/vendor/pub_grub/partial_solution.rb +121 -0
- data/lib/scint/vendor/pub_grub/rubygems.rb +45 -0
- data/lib/scint/vendor/pub_grub/solve_failure.rb +19 -0
- data/lib/scint/vendor/pub_grub/static_package_source.rb +61 -0
- data/lib/scint/vendor/pub_grub/strategy.rb +42 -0
- data/lib/scint/vendor/pub_grub/term.rb +105 -0
- data/lib/scint/vendor/pub_grub/version.rb +3 -0
- data/lib/scint/vendor/pub_grub/version_constraint.rb +129 -0
- data/lib/scint/vendor/pub_grub/version_range.rb +423 -0
- data/lib/scint/vendor/pub_grub/version_solver.rb +236 -0
- data/lib/scint/vendor/pub_grub/version_union.rb +178 -0
- data/lib/scint/vendor/pub_grub.rb +32 -0
- data/lib/scint/worker_pool.rb +114 -0
- data/lib/scint.rb +87 -0
- metadata +116 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Scint
|
|
4
|
+
module Resolver
|
|
5
|
+
# Bridges compact index data and the PubGrub resolver.
|
|
6
|
+
# Converts compact index info entries into version lists and dependency hashes.
|
|
7
|
+
#
|
|
8
|
+
# Supports multiple sources: each gem can be routed to a specific
|
|
9
|
+
# Index::Client based on source_map (gem_name => source_uri).
|
|
10
|
+
class Provider
|
|
11
|
+
# default_client: Index::Client for the default source (rubygems.org)
|
|
12
|
+
# clients: hash { source_uri_string => Index::Client } for all sources
|
|
13
|
+
# source_map: hash { gem_name => source_uri_string } for gems with explicit sources
|
|
14
|
+
# path_gems: hash { gem_name => { version:, dependencies: } } for path/git gems
|
|
15
|
+
# locked_specs: hash { name => version_string } for locked version preferences
|
|
16
|
+
# platforms: array of platform strings to match against
|
|
17
|
+
def initialize(default_client, clients: {}, source_map: {}, path_gems: {},
|
|
18
|
+
locked_specs: {}, platforms: nil)
|
|
19
|
+
@default_client = default_client
|
|
20
|
+
@clients = clients
|
|
21
|
+
@source_map = source_map
|
|
22
|
+
@path_gems = path_gems
|
|
23
|
+
@locked_specs = locked_specs
|
|
24
|
+
@platforms = platforms || default_platforms
|
|
25
|
+
@versions_cache = {}
|
|
26
|
+
@deps_cache = {}
|
|
27
|
+
@info_cache = {}
|
|
28
|
+
@mutex = Thread::Mutex.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# For backward compat and use in resolver (e.g. source_uri on resolved specs)
|
|
32
|
+
def index_client
|
|
33
|
+
@default_client
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the source URI for a gem name.
|
|
37
|
+
def source_uri_for(name)
|
|
38
|
+
if @path_gems.key?(name)
|
|
39
|
+
@path_gems[name][:source] || "path"
|
|
40
|
+
elsif @source_map.key?(name)
|
|
41
|
+
@source_map[name]
|
|
42
|
+
else
|
|
43
|
+
@default_client.source_uri
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the Index::Client for a given gem name.
|
|
48
|
+
def client_for(name)
|
|
49
|
+
source_uri = @source_map[name]
|
|
50
|
+
if source_uri
|
|
51
|
+
@clients[source_uri] || @default_client
|
|
52
|
+
else
|
|
53
|
+
@default_client
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns sorted array of Gem::Version for a given gem name,
|
|
58
|
+
# filtered to matching platforms.
|
|
59
|
+
def versions_for(name)
|
|
60
|
+
@versions_cache[name] ||= begin
|
|
61
|
+
# Path/git gems: return the single known version
|
|
62
|
+
if @path_gems.key?(name)
|
|
63
|
+
pg = @path_gems[name]
|
|
64
|
+
[Gem::Version.new(pg[:version] || "0")]
|
|
65
|
+
else
|
|
66
|
+
entries = info_for(name)
|
|
67
|
+
versions = {}
|
|
68
|
+
entries.each do |_name, version, platform, _deps, _reqs|
|
|
69
|
+
next unless platform_match?(platform)
|
|
70
|
+
ver = Gem::Version.new(version)
|
|
71
|
+
versions[version] ||= ver
|
|
72
|
+
end
|
|
73
|
+
versions.values.sort
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns dependency hash for a specific gem name + version.
|
|
79
|
+
# { "dep_name" => Gem::Requirement, ... }
|
|
80
|
+
def dependencies_for(name, version)
|
|
81
|
+
key = "#{name}-#{version}"
|
|
82
|
+
@deps_cache[key] ||= begin
|
|
83
|
+
# Path/git gems: return their declared dependencies
|
|
84
|
+
if @path_gems.key?(name)
|
|
85
|
+
pg = @path_gems[name]
|
|
86
|
+
deps = {}
|
|
87
|
+
(pg[:dependencies] || []).each do |dep_name, dep_req_str|
|
|
88
|
+
deps[dep_name] = Gem::Requirement.new(dep_req_str || ">= 0")
|
|
89
|
+
end
|
|
90
|
+
deps
|
|
91
|
+
else
|
|
92
|
+
version_str = version.to_s
|
|
93
|
+
entries = info_for(name)
|
|
94
|
+
deps = {}
|
|
95
|
+
|
|
96
|
+
entries.each do |_name, ver, platform, dep_hash, _reqs|
|
|
97
|
+
next unless ver == version_str && platform_match?(platform)
|
|
98
|
+
dep_hash.each do |dep_name, dep_req_str|
|
|
99
|
+
# Merge constraints from all matching platform entries
|
|
100
|
+
req = Gem::Requirement.new(dep_req_str.split(", "))
|
|
101
|
+
deps[dep_name] = if deps[dep_name]
|
|
102
|
+
merge_requirements(deps[dep_name], req)
|
|
103
|
+
else
|
|
104
|
+
req
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
deps
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Check if a gem version has native extensions.
|
|
115
|
+
# Approximated by checking if there's a platform-specific variant.
|
|
116
|
+
def has_extensions?(name, version)
|
|
117
|
+
return false if @path_gems.key?(name)
|
|
118
|
+
|
|
119
|
+
version_str = version.to_s
|
|
120
|
+
entries = info_for(name)
|
|
121
|
+
has_platform_specific = false
|
|
122
|
+
entries.each do |_name, ver, platform, _deps, _reqs|
|
|
123
|
+
next unless ver == version_str
|
|
124
|
+
has_platform_specific = true if platform != "ruby"
|
|
125
|
+
end
|
|
126
|
+
has_platform_specific
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Choose the most appropriate platform for a resolved version,
|
|
130
|
+
# preferring local binary gems over ruby source gems when available.
|
|
131
|
+
def preferred_platform_for(name, version)
|
|
132
|
+
return "ruby" if @path_gems.key?(name)
|
|
133
|
+
|
|
134
|
+
version_str = version.to_s
|
|
135
|
+
entries = info_for(name).select do |_n, ver, platform, _deps, _reqs|
|
|
136
|
+
ver == version_str && platform_match?(platform)
|
|
137
|
+
end
|
|
138
|
+
return "ruby" if entries.empty?
|
|
139
|
+
|
|
140
|
+
local_platforms = @platforms.reject { |p| p == "ruby" }
|
|
141
|
+
non_ruby = entries.map { |e| e[2] }.compact.reject { |p| p == "ruby" }.uniq
|
|
142
|
+
|
|
143
|
+
preferred = nil
|
|
144
|
+
best_score = nil
|
|
145
|
+
non_ruby.each do |platform|
|
|
146
|
+
local_platforms.each_with_index do |local, index|
|
|
147
|
+
spec_plat = platform.is_a?(Gem::Platform) ? platform : Gem::Platform.new(platform)
|
|
148
|
+
local_plat = local.is_a?(Gem::Platform) ? local : Gem::Platform.new(local)
|
|
149
|
+
next unless spec_plat === local_plat
|
|
150
|
+
|
|
151
|
+
exact = spec_plat.to_s == local_plat.to_s ? 1 : 0
|
|
152
|
+
score = [exact, -index]
|
|
153
|
+
next if best_score && score <= best_score
|
|
154
|
+
|
|
155
|
+
best_score = score
|
|
156
|
+
preferred = platform
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
return preferred if preferred
|
|
160
|
+
|
|
161
|
+
entries.any? { |e| e[2] == "ruby" } ? "ruby" : entries.first[2].to_s
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns the locked version for a gem, if any.
|
|
165
|
+
def locked_version(name)
|
|
166
|
+
v = @locked_specs[name]
|
|
167
|
+
v ? Gem::Version.new(v) : nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Prefetch info for a batch of gem names, routing to the correct client.
|
|
171
|
+
def prefetch(names)
|
|
172
|
+
# Filter to names we haven't cached yet, skip path/git gems
|
|
173
|
+
uncached = names.reject { |n| @info_cache.key?(n) || @path_gems.key?(n) }
|
|
174
|
+
return if uncached.empty?
|
|
175
|
+
|
|
176
|
+
# Group by client
|
|
177
|
+
by_client = Hash.new { |h, k| h[k] = [] }
|
|
178
|
+
uncached.each do |name|
|
|
179
|
+
by_client[client_for(name)] << name
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
by_client.each do |client, client_names|
|
|
183
|
+
results = client.prefetch(client_names)
|
|
184
|
+
next unless results
|
|
185
|
+
|
|
186
|
+
@mutex.synchronize do
|
|
187
|
+
results.each do |name, data|
|
|
188
|
+
@info_cache[name] = data
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Returns true if the gem is a path or git source gem.
|
|
195
|
+
def path_or_git_gem?(name)
|
|
196
|
+
@path_gems.key?(name)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def info_for(name)
|
|
202
|
+
@info_cache[name] ||= client_for(name).fetch_info(name)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def default_platforms
|
|
206
|
+
local = Platform.local_platform.to_s
|
|
207
|
+
platforms = ["ruby"]
|
|
208
|
+
platforms << local unless local == "ruby"
|
|
209
|
+
platforms
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def platform_match?(spec_platform)
|
|
213
|
+
return true if spec_platform.nil? || spec_platform == "ruby"
|
|
214
|
+
spec_plat = spec_platform.is_a?(Gem::Platform) ? spec_platform : Gem::Platform.new(spec_platform)
|
|
215
|
+
@platforms.any? do |plat|
|
|
216
|
+
next false if plat == "ruby"
|
|
217
|
+
|
|
218
|
+
local_plat = plat.is_a?(Gem::Platform) ? plat : Gem::Platform.new(plat)
|
|
219
|
+
spec_plat === local_plat
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def merge_requirements(req1, req2)
|
|
224
|
+
# Combine requirement constraints
|
|
225
|
+
combined = req1.requirements + req2.requirements
|
|
226
|
+
Gem::Requirement.new(combined.map { |op, v| "#{op} #{v}" })
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../vendor/pub_grub"
|
|
4
|
+
require_relative "provider"
|
|
5
|
+
|
|
6
|
+
module Scint
|
|
7
|
+
module Resolver
|
|
8
|
+
# ResolvedSpec: the output of resolution.
|
|
9
|
+
ResolvedSpec = Struct.new(
|
|
10
|
+
:name, :version, :platform, :dependencies,
|
|
11
|
+
:source, :has_extensions, :remote_uri, :checksum,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# PubGrub-based dependency resolver.
|
|
16
|
+
# Implements the source interface that PubGrub::VersionSolver expects.
|
|
17
|
+
class Resolver
|
|
18
|
+
attr_reader :provider
|
|
19
|
+
|
|
20
|
+
# provider: Resolver::Provider instance
|
|
21
|
+
# dependencies: array of Gemfile::Dependency (top-level requirements)
|
|
22
|
+
# locked_specs: hash { name => version_string } for preferring locked versions
|
|
23
|
+
def initialize(provider:, dependencies:, locked_specs: {})
|
|
24
|
+
@provider = provider
|
|
25
|
+
@dependencies = dependencies
|
|
26
|
+
@locked_specs = locked_specs
|
|
27
|
+
@packages = {} # name => PubGrub::Package
|
|
28
|
+
|
|
29
|
+
@root_package = Scint::PubGrub::Package.root
|
|
30
|
+
@root_version = Scint::PubGrub::Package.root_version
|
|
31
|
+
|
|
32
|
+
# MUST be ascending — PubGrub uses binary search in select_versions.
|
|
33
|
+
@sorted_versions = Hash.new do |h, k|
|
|
34
|
+
if k == @root_package
|
|
35
|
+
h[k] = [@root_version]
|
|
36
|
+
else
|
|
37
|
+
h[k] = all_versions_for(k).sort
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@cached_dependencies = Hash.new do |packages, package|
|
|
42
|
+
if package == @root_package
|
|
43
|
+
packages[package] = { @root_version => root_dependencies }
|
|
44
|
+
else
|
|
45
|
+
packages[package] = Hash.new do |versions, version|
|
|
46
|
+
versions[version] = dependencies_for(package, version)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run resolution. Returns array of ResolvedSpec.
|
|
53
|
+
def resolve
|
|
54
|
+
# Prefetch all known gem info before resolution
|
|
55
|
+
prefetch_all
|
|
56
|
+
|
|
57
|
+
solver = Scint::PubGrub::VersionSolver.new(
|
|
58
|
+
source: self,
|
|
59
|
+
root: @root_package,
|
|
60
|
+
logger: NullLogger.new
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
result = solver.solve
|
|
64
|
+
|
|
65
|
+
result.filter_map do |package, version|
|
|
66
|
+
next if Scint::PubGrub::Package.root?(package)
|
|
67
|
+
build_resolved_spec(package, version)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# --- PubGrub source interface ---
|
|
72
|
+
|
|
73
|
+
def versions_for(package, range = Scint::PubGrub::VersionRange.any)
|
|
74
|
+
range.select_versions(@sorted_versions[package])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def incompatibilities_for(package, version)
|
|
78
|
+
package_deps = @cached_dependencies[package]
|
|
79
|
+
sorted_versions = @sorted_versions[package]
|
|
80
|
+
package_deps[version].map do |dep_package, dep_constraint|
|
|
81
|
+
low = high = sorted_versions.index(version)
|
|
82
|
+
|
|
83
|
+
# find version low such that all >= low share the same dep
|
|
84
|
+
while low > 0 && package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint
|
|
85
|
+
low -= 1
|
|
86
|
+
end
|
|
87
|
+
low = low == 0 ? nil : sorted_versions[low]
|
|
88
|
+
|
|
89
|
+
# find version high such that all < high share the same dep
|
|
90
|
+
while high < sorted_versions.length && package_deps[sorted_versions[high]][dep_package] == dep_constraint
|
|
91
|
+
high += 1
|
|
92
|
+
end
|
|
93
|
+
high = high == sorted_versions.length ? nil : sorted_versions[high]
|
|
94
|
+
|
|
95
|
+
range = Scint::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?)
|
|
96
|
+
self_constraint = Scint::PubGrub::VersionConstraint.new(package, range: range)
|
|
97
|
+
|
|
98
|
+
dep_term = Scint::PubGrub::Term.new(dep_constraint, false)
|
|
99
|
+
self_term = Scint::PubGrub::Term.new(self_constraint, true)
|
|
100
|
+
|
|
101
|
+
Scint::PubGrub::Incompatibility.new([self_term, dep_term], cause: :dependency)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def no_versions_incompatibility_for(_package, unsatisfied_term)
|
|
106
|
+
cause = Scint::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term)
|
|
107
|
+
Scint::PubGrub::Incompatibility.new([unsatisfied_term], cause: cause)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Public: PubGrub Strategy calls this to build version preference index.
|
|
111
|
+
# Returns newest-first so index 0 = newest = most preferred.
|
|
112
|
+
# Locked versions are promoted to the front (most preferred).
|
|
113
|
+
def all_versions_for(package)
|
|
114
|
+
name = package.name
|
|
115
|
+
versions = @provider.versions_for(name).reverse # newest first
|
|
116
|
+
|
|
117
|
+
locked = @locked_specs[name]
|
|
118
|
+
if locked
|
|
119
|
+
locked_ver = Gem::Version.new(locked)
|
|
120
|
+
# Move locked version to front if present
|
|
121
|
+
if (idx = versions.index(locked_ver))
|
|
122
|
+
versions.delete_at(idx)
|
|
123
|
+
versions.unshift(locked_ver)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
versions
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def package_for(name)
|
|
133
|
+
@packages[name] ||= Scint::PubGrub::Package.new(name)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def root_dependencies
|
|
137
|
+
deps = {}
|
|
138
|
+
@dependencies.each do |dep|
|
|
139
|
+
pkg = package_for(dep.name)
|
|
140
|
+
req = Gem::Requirement.new(dep.version_reqs)
|
|
141
|
+
range = requirement_to_range(req)
|
|
142
|
+
constraint = Scint::PubGrub::VersionConstraint.new(pkg, range: range)
|
|
143
|
+
|
|
144
|
+
deps[pkg] = if deps[pkg]
|
|
145
|
+
deps[pkg].intersect(constraint)
|
|
146
|
+
else
|
|
147
|
+
constraint
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
deps
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def dependencies_for(package, version)
|
|
154
|
+
name = package.name
|
|
155
|
+
dep_hash = @provider.dependencies_for(name, version)
|
|
156
|
+
result = {}
|
|
157
|
+
|
|
158
|
+
dep_hash.each do |dep_name, dep_req|
|
|
159
|
+
dep_package = package_for(dep_name)
|
|
160
|
+
range = requirement_to_range(dep_req)
|
|
161
|
+
constraint = Scint::PubGrub::VersionConstraint.new(dep_package, range: range)
|
|
162
|
+
result[dep_package] = constraint
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
result
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def requirement_to_range(requirement)
|
|
169
|
+
ranges = requirement.requirements.map do |(op, version)|
|
|
170
|
+
case op
|
|
171
|
+
when "~>"
|
|
172
|
+
name = "~> #{version}"
|
|
173
|
+
bump = Gem::Version.new(version.bump.to_s + ".A")
|
|
174
|
+
Scint::PubGrub::VersionRange.new(name: name, min: version, max: bump, include_min: true)
|
|
175
|
+
when ">"
|
|
176
|
+
Scint::PubGrub::VersionRange.new(min: version)
|
|
177
|
+
when ">="
|
|
178
|
+
Scint::PubGrub::VersionRange.new(min: version, include_min: true)
|
|
179
|
+
when "<"
|
|
180
|
+
Scint::PubGrub::VersionRange.new(max: version)
|
|
181
|
+
when "<="
|
|
182
|
+
Scint::PubGrub::VersionRange.new(max: version, include_max: true)
|
|
183
|
+
when "="
|
|
184
|
+
Scint::PubGrub::VersionRange.new(min: version, max: version, include_min: true, include_max: true)
|
|
185
|
+
when "!="
|
|
186
|
+
Scint::PubGrub::VersionRange.new(min: version, max: version, include_min: true, include_max: true).invert
|
|
187
|
+
else
|
|
188
|
+
raise ResolveError, "bad version specifier: #{op}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
ranges.inject(&:intersect)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Prefetch compact index info for all known gem names.
|
|
196
|
+
def prefetch_all
|
|
197
|
+
names = Set.new
|
|
198
|
+
@dependencies.each { |d| names << d.name }
|
|
199
|
+
|
|
200
|
+
# Also include locked spec names and their transitive deps
|
|
201
|
+
@locked_specs.each_key { |n| names << n }
|
|
202
|
+
|
|
203
|
+
# Fetch versions for each unique client (populates checksums).
|
|
204
|
+
# Skip path/git gems -- they don't use the compact index.
|
|
205
|
+
fetched_clients = Set.new
|
|
206
|
+
names.each do |name|
|
|
207
|
+
next if @provider.path_or_git_gem?(name)
|
|
208
|
+
client = @provider.client_for(name)
|
|
209
|
+
unless fetched_clients.include?(client.source_uri)
|
|
210
|
+
client.fetch_versions
|
|
211
|
+
fetched_clients << client.source_uri
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Prefetch all info in parallel (provider routes to correct client)
|
|
216
|
+
@provider.prefetch(names.to_a)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def build_resolved_spec(package, version)
|
|
220
|
+
name = package.name
|
|
221
|
+
deps = @provider.dependencies_for(name, version)
|
|
222
|
+
dep_list = deps.map { |n, r| { name: n, version_reqs: [r.to_s] } }
|
|
223
|
+
platform = @provider.preferred_platform_for(name, version)
|
|
224
|
+
|
|
225
|
+
source = @provider.source_uri_for(name)
|
|
226
|
+
|
|
227
|
+
Scint::ResolvedSpec.new(
|
|
228
|
+
name: name,
|
|
229
|
+
version: version.to_s,
|
|
230
|
+
platform: platform,
|
|
231
|
+
dependencies: dep_list,
|
|
232
|
+
source: source,
|
|
233
|
+
has_extensions: (platform == "ruby") && @provider.has_extensions?(name, version),
|
|
234
|
+
remote_uri: nil,
|
|
235
|
+
checksum: nil,
|
|
236
|
+
)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Minimal logger that discards output (PubGrub requires a logger).
|
|
240
|
+
class NullLogger
|
|
241
|
+
def info(&block) = nil
|
|
242
|
+
def debug(&block) = nil
|
|
243
|
+
def warn(&block) = nil
|
|
244
|
+
def error(&block) = nil
|
|
245
|
+
def level=(v); end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "setup"
|
|
4
|
+
require_relative "../fs"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "pathname"
|
|
7
|
+
|
|
8
|
+
module Scint
|
|
9
|
+
module Runtime
|
|
10
|
+
module Exec
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Execute a command with the bundled environment.
|
|
14
|
+
# Reads Marshal'd runtime config, sets RUBYLIB so the child process
|
|
15
|
+
# has all gem load paths, then Kernel.exec replaces the process.
|
|
16
|
+
#
|
|
17
|
+
# command: the program to run (e.g. "rails")
|
|
18
|
+
# args: array of arguments
|
|
19
|
+
# lock_path: path to .bundle/scint.lock.marshal
|
|
20
|
+
def exec(command, args, lock_path)
|
|
21
|
+
original_env = ENV.to_hash
|
|
22
|
+
lock_data = Setup.load_lock(lock_path)
|
|
23
|
+
|
|
24
|
+
bundle_dir = File.dirname(lock_path)
|
|
25
|
+
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
|
|
35
|
+
end
|
|
36
|
+
|
|
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
|
+
ENV["SCINT_RUNTIME_LOCK"] = lock_path
|
|
48
|
+
ENV["GEM_HOME"] = ruby_dir
|
|
49
|
+
ENV["GEM_PATH"] = ruby_dir
|
|
50
|
+
ENV["BUNDLE_PATH"] = bundle_dir
|
|
51
|
+
ENV["BUNDLE_APP_CONFIG"] = bundle_dir
|
|
52
|
+
ENV["BUNDLE_GEMFILE"] = find_gemfile(bundle_dir)
|
|
53
|
+
ENV["PATH"] = prepend_path(File.join(ruby_dir, "bin"), ENV["PATH"])
|
|
54
|
+
ENV["PATH"] = prepend_path(File.join(bundle_dir, "bin"), ENV["PATH"])
|
|
55
|
+
prepend_rubyopt("-rbundler/setup")
|
|
56
|
+
export_original_env(original_env)
|
|
57
|
+
|
|
58
|
+
command = resolve_command(command, bundle_dir, ruby_dir)
|
|
59
|
+
|
|
60
|
+
# Kernel.exec replaces the process
|
|
61
|
+
Kernel.exec(command, *args)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def find_gemfile(bundle_dir)
|
|
65
|
+
project_root = File.dirname(bundle_dir)
|
|
66
|
+
gemfile = File.join(project_root, "Gemfile")
|
|
67
|
+
File.exist?(gemfile) ? gemfile : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def prepend_rubyopt(flag)
|
|
71
|
+
parts = ENV["RUBYOPT"].to_s.split(/\s+/).reject(&:empty?)
|
|
72
|
+
return if parts.include?(flag)
|
|
73
|
+
|
|
74
|
+
ENV["RUBYOPT"] = ([flag] + parts).join(" ")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def prepend_path(prefix, current_path)
|
|
78
|
+
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}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def export_original_env(original_env)
|
|
85
|
+
ENV["SCINT_ORIGINAL_ENV"] ||= Base64.strict_encode64(Marshal.dump(original_env))
|
|
86
|
+
rescue StandardError
|
|
87
|
+
# Non-fatal: shim can fallback to current ENV.
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def resolve_command(command, bundle_dir, ruby_dir)
|
|
91
|
+
return command if command.include?(File::SEPARATOR)
|
|
92
|
+
|
|
93
|
+
bundle_bin = File.join(bundle_dir, "bin")
|
|
94
|
+
ruby_bin = File.join(ruby_dir, "bin")
|
|
95
|
+
FS.mkdir_p(bundle_bin)
|
|
96
|
+
|
|
97
|
+
bundle_candidate = File.join(bundle_bin, command)
|
|
98
|
+
return bundle_candidate if File.executable?(bundle_candidate)
|
|
99
|
+
|
|
100
|
+
ruby_candidate = File.join(ruby_bin, command)
|
|
101
|
+
return ruby_candidate if File.executable?(ruby_candidate)
|
|
102
|
+
|
|
103
|
+
gem_exec = find_gem_executable(ruby_dir, command)
|
|
104
|
+
return command unless gem_exec
|
|
105
|
+
|
|
106
|
+
write_bundle_exec_wrapper(bundle_candidate, gem_exec, bundle_bin)
|
|
107
|
+
bundle_candidate
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_gem_executable(ruby_dir, command)
|
|
111
|
+
gems_dir = File.join(ruby_dir, "gems")
|
|
112
|
+
return nil unless Dir.exist?(gems_dir)
|
|
113
|
+
|
|
114
|
+
Dir.glob(File.join(gems_dir, "*")).sort.each do |gem_dir|
|
|
115
|
+
%w[exe bin].each do |subdir|
|
|
116
|
+
candidate = File.join(gem_dir, subdir, command)
|
|
117
|
+
return candidate if File.file?(candidate)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def write_bundle_exec_wrapper(wrapper_path, target_path, bundle_bin)
|
|
125
|
+
relative = Pathname.new(target_path).relative_path_from(Pathname.new(bundle_bin)).to_s
|
|
126
|
+
content = <<~RUBY
|
|
127
|
+
#!/usr/bin/env ruby
|
|
128
|
+
# frozen_string_literal: true
|
|
129
|
+
load File.expand_path("#{relative}", __dir__)
|
|
130
|
+
RUBY
|
|
131
|
+
FS.atomic_write(wrapper_path, content)
|
|
132
|
+
File.chmod(0o755, wrapper_path)
|
|
133
|
+
rescue StandardError
|
|
134
|
+
# If wrapper creation fails, we'll still fall back to PATH lookup.
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private_class_method :find_gemfile, :prepend_rubyopt, :prepend_path, :export_original_env,
|
|
138
|
+
:resolve_command, :find_gem_executable, :write_bundle_exec_wrapper
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Scint
|
|
4
|
+
module Runtime
|
|
5
|
+
module Setup
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Load and return the Marshal'd lock data without modifying $LOAD_PATH.
|
|
9
|
+
def load_lock(lock_path)
|
|
10
|
+
unless File.exist?(lock_path)
|
|
11
|
+
raise LoadError, "Runtime lock not found: #{lock_path}. Run `scint install` first."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Marshal.load(File.binread(lock_path)) # rubocop:disable Security/MarshalLoad
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Set up $LOAD_PATH from a Marshal'd lock data file.
|
|
18
|
+
# This is the fast path for in-process setup — <10ms.
|
|
19
|
+
def setup(lock_path)
|
|
20
|
+
lock_data = load_lock(lock_path)
|
|
21
|
+
|
|
22
|
+
paths = []
|
|
23
|
+
lock_data.each_value do |info|
|
|
24
|
+
Array(info[:load_paths]).each do |p|
|
|
25
|
+
paths << p if File.directory?(p)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
$LOAD_PATH.unshift(*paths)
|
|
30
|
+
|
|
31
|
+
ENV["BUNDLE_GEMFILE"] ||= find_gemfile(File.dirname(lock_path))
|
|
32
|
+
|
|
33
|
+
lock_data
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_gemfile(bundle_dir)
|
|
37
|
+
project_root = File.dirname(bundle_dir)
|
|
38
|
+
gemfile = File.join(project_root, "Gemfile")
|
|
39
|
+
File.exist?(gemfile) ? gemfile : nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private_class_method :find_gemfile
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|