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,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../fs"
|
|
4
|
+
require_relative "../platform"
|
|
5
|
+
require "pathname"
|
|
6
|
+
|
|
7
|
+
module Scint
|
|
8
|
+
module Installer
|
|
9
|
+
module Linker
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Link a single extracted gem into .bundle/ruby/{version}/
|
|
13
|
+
# prepared_gem: PreparedGem struct (spec, extracted_path, gemspec, from_cache)
|
|
14
|
+
# bundle_path: root .bundle/ directory
|
|
15
|
+
def link(prepared_gem, bundle_path)
|
|
16
|
+
link_files(prepared_gem, bundle_path)
|
|
17
|
+
write_binstubs(prepared_gem, bundle_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Link gem files + gemspec only (no binstubs).
|
|
21
|
+
# This allows binstubs to be scheduled as a separate DAG task.
|
|
22
|
+
def link_files(prepared_gem, bundle_path)
|
|
23
|
+
ruby_dir = ruby_install_dir(bundle_path)
|
|
24
|
+
link_files_to_ruby_dir(prepared_gem, ruby_dir)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Link gem files + gemspec into an explicit ruby gem home directory.
|
|
28
|
+
# This is used for the install-time hermetic build environment.
|
|
29
|
+
def link_files_to_ruby_dir(prepared_gem, ruby_dir)
|
|
30
|
+
spec = prepared_gem.spec
|
|
31
|
+
full_name = spec_full_name(spec)
|
|
32
|
+
|
|
33
|
+
# 1. Link gem files into gems/{full_name}/
|
|
34
|
+
gem_dest = File.join(ruby_dir, "gems", full_name)
|
|
35
|
+
unless Dir.exist?(gem_dest)
|
|
36
|
+
FS.hardlink_tree(prepared_gem.extracted_path, gem_dest)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 2. Write gemspec into specifications/
|
|
40
|
+
write_gemspec(prepared_gem, ruby_dir, full_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Write binstubs for one already-linked gem.
|
|
44
|
+
def write_binstubs(prepared_gem, bundle_path)
|
|
45
|
+
ruby_dir = ruby_install_dir(bundle_path)
|
|
46
|
+
spec = prepared_gem.spec
|
|
47
|
+
full_name = spec_full_name(spec)
|
|
48
|
+
gem_dest = File.join(ruby_dir, "gems", full_name)
|
|
49
|
+
return unless Dir.exist?(gem_dest)
|
|
50
|
+
|
|
51
|
+
# 3. Create binstubs for executables
|
|
52
|
+
write_binstubs_impl(prepared_gem, bundle_path, ruby_dir, gem_dest)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Link multiple gems. Thread-safe — each link is independent.
|
|
56
|
+
def link_batch(prepared_gems, bundle_path)
|
|
57
|
+
prepared_gems.each { |pg| link(pg, bundle_path) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# --- private helpers ---
|
|
61
|
+
|
|
62
|
+
def write_gemspec(prepared_gem, ruby_dir, full_name)
|
|
63
|
+
spec_dir = File.join(ruby_dir, "specifications")
|
|
64
|
+
FS.mkdir_p(spec_dir)
|
|
65
|
+
spec_path = File.join(spec_dir, "#{full_name}.gemspec")
|
|
66
|
+
return if File.exist?(spec_path)
|
|
67
|
+
|
|
68
|
+
content = if prepared_gem.gemspec.is_a?(String)
|
|
69
|
+
prepared_gem.gemspec
|
|
70
|
+
elsif prepared_gem.gemspec.respond_to?(:to_ruby)
|
|
71
|
+
augment_executable_metadata(prepared_gem.gemspec, prepared_gem.extracted_path).to_ruby
|
|
72
|
+
else
|
|
73
|
+
minimal_gemspec(prepared_gem.spec, full_name)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
FS.atomic_write(spec_path, content)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def write_binstubs_impl(prepared_gem, bundle_path, ruby_dir, gem_dir)
|
|
80
|
+
# Look for executables declared in the gemspec
|
|
81
|
+
executables = extract_executables(prepared_gem, gem_dir)
|
|
82
|
+
return if executables.empty?
|
|
83
|
+
|
|
84
|
+
ruby_bin_dir = File.join(ruby_dir, "bin")
|
|
85
|
+
bundle_bin_dir = File.join(bundle_path, "bin")
|
|
86
|
+
FS.mkdir_p(ruby_bin_dir)
|
|
87
|
+
FS.mkdir_p(bundle_bin_dir)
|
|
88
|
+
|
|
89
|
+
executables.each do |exe_name|
|
|
90
|
+
write_ruby_bin_stub(prepared_gem, ruby_bin_dir, exe_name)
|
|
91
|
+
write_bundle_bin_wrapper(bundle_path, ruby_bin_dir, bundle_bin_dir, exe_name)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def write_ruby_bin_stub(prepared_gem, ruby_bin_dir, exe_name)
|
|
96
|
+
binstub_path = File.join(ruby_bin_dir, exe_name)
|
|
97
|
+
return if File.exist?(binstub_path)
|
|
98
|
+
|
|
99
|
+
spec = prepared_gem.spec
|
|
100
|
+
content = <<~RUBY
|
|
101
|
+
#!/usr/bin/env ruby
|
|
102
|
+
# frozen_string_literal: true
|
|
103
|
+
#
|
|
104
|
+
# This file was generated by scint for #{spec_full_name(spec)}
|
|
105
|
+
#
|
|
106
|
+
gem "#{spec.name}", "#{spec.version}"
|
|
107
|
+
load Gem.bin_path("#{spec.name}", "#{exe_name}", "#{spec.version}")
|
|
108
|
+
RUBY
|
|
109
|
+
|
|
110
|
+
FS.atomic_write(binstub_path, content)
|
|
111
|
+
File.chmod(0o755, binstub_path)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def write_bundle_bin_wrapper(bundle_path, ruby_bin_dir, bundle_bin_dir, exe_name)
|
|
115
|
+
wrapper_path = File.join(bundle_bin_dir, exe_name)
|
|
116
|
+
return if File.exist?(wrapper_path)
|
|
117
|
+
|
|
118
|
+
target = File.join(ruby_bin_dir, exe_name)
|
|
119
|
+
relative_target = Pathname.new(target).relative_path_from(Pathname.new(bundle_bin_dir)).to_s
|
|
120
|
+
content = <<~RUBY
|
|
121
|
+
#!/usr/bin/env ruby
|
|
122
|
+
# frozen_string_literal: true
|
|
123
|
+
exec(File.expand_path("#{relative_target}", __dir__), *ARGV)
|
|
124
|
+
RUBY
|
|
125
|
+
|
|
126
|
+
FS.atomic_write(wrapper_path, content)
|
|
127
|
+
File.chmod(0o755, wrapper_path)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def extract_executables(prepared_gem, gem_dir)
|
|
131
|
+
gemspec = prepared_gem.gemspec
|
|
132
|
+
executables = if gemspec.respond_to?(:executables)
|
|
133
|
+
Array(gemspec.executables)
|
|
134
|
+
elsif gemspec.is_a?(Hash) && gemspec[:executables]
|
|
135
|
+
Array(gemspec[:executables])
|
|
136
|
+
else
|
|
137
|
+
[]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if executables.empty?
|
|
141
|
+
executables = detect_executables_from_files(gem_dir)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
executables.map(&:to_s).reject(&:empty?).uniq
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def detect_executables_from_files(gem_dir)
|
|
148
|
+
names = []
|
|
149
|
+
%w[exe bin].each do |subdir|
|
|
150
|
+
dir = File.join(gem_dir, subdir)
|
|
151
|
+
next unless Dir.exist?(dir)
|
|
152
|
+
|
|
153
|
+
Dir.children(dir).each do |entry|
|
|
154
|
+
next if entry.start_with?(".")
|
|
155
|
+
path = File.join(dir, entry)
|
|
156
|
+
names << entry if File.file?(path)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
names
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def augment_executable_metadata(gemspec, gem_dir)
|
|
163
|
+
detected = detect_executables_from_files(gem_dir)
|
|
164
|
+
return gemspec if detected.empty?
|
|
165
|
+
|
|
166
|
+
executables = Array(gemspec.executables).map(&:to_s).reject(&:empty?)
|
|
167
|
+
bindir = gemspec.respond_to?(:bindir) ? gemspec.bindir.to_s : ""
|
|
168
|
+
|
|
169
|
+
selected = executables.empty? ? detected : executables
|
|
170
|
+
inferred_bindir = infer_bindir(gem_dir, selected, bindir)
|
|
171
|
+
needs_execs = executables.empty?
|
|
172
|
+
needs_bindir = !inferred_bindir.nil? && inferred_bindir != bindir
|
|
173
|
+
return gemspec unless needs_execs || needs_bindir
|
|
174
|
+
|
|
175
|
+
patched = gemspec.dup
|
|
176
|
+
patched.executables = selected if needs_execs
|
|
177
|
+
patched.bindir = inferred_bindir if needs_bindir
|
|
178
|
+
patched
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def infer_bindir(gem_dir, executables, current_bindir)
|
|
182
|
+
%w[exe bin].each do |dir|
|
|
183
|
+
next unless executables.all? { |exe| File.file?(File.join(gem_dir, dir, exe)) }
|
|
184
|
+
return dir
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
current_bindir unless current_bindir.empty?
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def minimal_gemspec(spec, full_name)
|
|
191
|
+
<<~RUBY
|
|
192
|
+
# frozen_string_literal: true
|
|
193
|
+
Gem::Specification.new do |s|
|
|
194
|
+
s.name = #{spec.name.inspect}
|
|
195
|
+
s.version = #{spec.version.to_s.inspect}
|
|
196
|
+
s.platform = #{platform_str(spec).inspect}
|
|
197
|
+
s.authors = ["scint"]
|
|
198
|
+
s.summary = "Installed by scint"
|
|
199
|
+
end
|
|
200
|
+
RUBY
|
|
201
|
+
end
|
|
202
|
+
|
|
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,
|
|
221
|
+
:write_bundle_bin_wrapper, :extract_executables,
|
|
222
|
+
:detect_executables_from_files, :augment_executable_metadata, :infer_bindir,
|
|
223
|
+
:minimal_gemspec, :spec_full_name, :platform_str, :ruby_install_dir
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "extension_builder"
|
|
4
|
+
require_relative "../platform"
|
|
5
|
+
|
|
6
|
+
module Scint
|
|
7
|
+
module Installer
|
|
8
|
+
module Planner
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Compare resolved specs against what's already installed.
|
|
12
|
+
# Returns an Array of PlanEntry with action set to one of:
|
|
13
|
+
# :skip — already installed in bundle_path
|
|
14
|
+
# :link — extracted in global cache, just needs linking
|
|
15
|
+
# :download — needs downloading from remote
|
|
16
|
+
# :build_ext — has native extensions that need compiling
|
|
17
|
+
#
|
|
18
|
+
# Download entries are sorted largest-first so big gems start early,
|
|
19
|
+
# 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
|
+
entries = resolved_specs.map do |spec|
|
|
23
|
+
plan_one(spec, ruby_dir, cache_layout)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Stable partition: downloads first (big→small), then everything else
|
|
27
|
+
downloads, rest = entries.partition { |e| e.action == :download }
|
|
28
|
+
downloads.sort_by! { |e| -(estimated_size(e.spec)) }
|
|
29
|
+
|
|
30
|
+
downloads + rest
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def plan_one(spec, ruby_dir, cache_layout)
|
|
34
|
+
full = cache_layout.full_name(spec)
|
|
35
|
+
gem_path = File.join(ruby_dir, "gems", full)
|
|
36
|
+
spec_path = File.join(ruby_dir, "specifications", "#{full}.gemspec")
|
|
37
|
+
|
|
38
|
+
# Built-in gems (scint itself): copy from our own lib tree.
|
|
39
|
+
if spec.name == "scint" && spec.source.to_s.include?("built-in")
|
|
40
|
+
if Dir.exist?(gem_path) && File.exist?(spec_path)
|
|
41
|
+
return PlanEntry.new(spec: spec, action: :skip, cached_path: nil, gem_path: gem_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return PlanEntry.new(spec: spec, action: :builtin, cached_path: nil, gem_path: gem_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Already installed? Require both gem files and specification.
|
|
48
|
+
if Dir.exist?(gem_path) && File.exist?(spec_path)
|
|
49
|
+
if extension_link_missing?(spec, ruby_dir, cache_layout)
|
|
50
|
+
extracted = cache_layout.extracted_path(spec)
|
|
51
|
+
action = ExtensionBuilder.cached_build_available?(spec, cache_layout) ? :link : :build_ext
|
|
52
|
+
return PlanEntry.new(spec: spec, action: action, cached_path: extracted, gem_path: gem_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return PlanEntry.new(spec: spec, action: :skip, cached_path: nil, gem_path: gem_path)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Local path sources are linked directly from their source tree.
|
|
59
|
+
local_source = local_source_path(spec)
|
|
60
|
+
if local_source
|
|
61
|
+
action = ExtensionBuilder.buildable_source_dir?(local_source) ? :build_ext : :link
|
|
62
|
+
return PlanEntry.new(spec: spec, action: action, cached_path: local_source, gem_path: gem_path)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Extracted in global cache?
|
|
66
|
+
extracted = cache_layout.extracted_path(spec)
|
|
67
|
+
if Dir.exist?(extracted)
|
|
68
|
+
action = needs_ext_build?(spec, cache_layout) ? :build_ext : :link
|
|
69
|
+
return PlanEntry.new(spec: spec, action: action, cached_path: extracted, gem_path: gem_path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Needs downloading
|
|
73
|
+
PlanEntry.new(spec: spec, action: :download, cached_path: nil, gem_path: gem_path)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def needs_ext_build?(spec, cache_layout)
|
|
77
|
+
extracted = cache_layout.extracted_path(spec)
|
|
78
|
+
return false unless ExtensionBuilder.buildable_source_dir?(extracted)
|
|
79
|
+
|
|
80
|
+
!ExtensionBuilder.cached_build_available?(spec, cache_layout)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def extension_link_missing?(spec, ruby_dir, cache_layout)
|
|
84
|
+
extracted = cache_layout.extracted_path(spec)
|
|
85
|
+
return false unless Dir.exist?(extracted)
|
|
86
|
+
return false unless ExtensionBuilder.buildable_source_dir?(extracted)
|
|
87
|
+
|
|
88
|
+
full = cache_layout.full_name(spec)
|
|
89
|
+
ext_install_dir = File.join(
|
|
90
|
+
ruby_dir,
|
|
91
|
+
"extensions",
|
|
92
|
+
Platform.gem_arch,
|
|
93
|
+
Platform.extension_api_version,
|
|
94
|
+
full,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
!Dir.exist?(ext_install_dir)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ruby_install_dir(bundle_path)
|
|
101
|
+
File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Rough size estimate for download ordering.
|
|
105
|
+
# If we don't know, use 0 so unknowns sort after large known gems.
|
|
106
|
+
def estimated_size(spec)
|
|
107
|
+
return spec.size if spec.respond_to?(:size) && spec.size
|
|
108
|
+
0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def local_source_path(spec)
|
|
112
|
+
source =
|
|
113
|
+
if spec.respond_to?(:source)
|
|
114
|
+
spec.source
|
|
115
|
+
else
|
|
116
|
+
spec[:source]
|
|
117
|
+
end
|
|
118
|
+
return nil unless source
|
|
119
|
+
|
|
120
|
+
source_str =
|
|
121
|
+
if source.respond_to?(:path)
|
|
122
|
+
source.path.to_s
|
|
123
|
+
elsif source.respond_to?(:uri) && source.class.name.end_with?("::Path")
|
|
124
|
+
source.uri.to_s
|
|
125
|
+
else
|
|
126
|
+
source.to_s
|
|
127
|
+
end
|
|
128
|
+
return nil if source_str.empty?
|
|
129
|
+
return nil if source_str.start_with?("http://", "https://")
|
|
130
|
+
return nil if source_str.end_with?(".git") || source_str.include?(".git/")
|
|
131
|
+
|
|
132
|
+
absolute = File.expand_path(source_str, Dir.pwd)
|
|
133
|
+
Dir.exist?(absolute) ? absolute : nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private_class_method :plan_one, :needs_ext_build?, :extension_link_missing?,
|
|
137
|
+
:ruby_install_dir, :estimated_size, :local_source_path
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../downloader/pool"
|
|
4
|
+
require_relative "../gem/package"
|
|
5
|
+
require_relative "../gem/extractor"
|
|
6
|
+
require_relative "../cache/layout"
|
|
7
|
+
require_relative "../fs"
|
|
8
|
+
require_relative "../errors"
|
|
9
|
+
|
|
10
|
+
module Scint
|
|
11
|
+
module Installer
|
|
12
|
+
# Data structures used by the preparer.
|
|
13
|
+
PlanEntry = Struct.new(:spec, :action, :cached_path, :gem_path, keyword_init: true)
|
|
14
|
+
PreparedGem = Struct.new(:spec, :extracted_path, :gemspec, :from_cache, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
class Preparer
|
|
17
|
+
attr_reader :results
|
|
18
|
+
|
|
19
|
+
# scheduler: optional Scint::Scheduler for enqueuing jobs
|
|
20
|
+
# layout: Cache::Layout instance
|
|
21
|
+
# on_progress: optional callback proc
|
|
22
|
+
def initialize(layout:, scheduler: nil, on_progress: nil)
|
|
23
|
+
@layout = layout
|
|
24
|
+
@scheduler = scheduler
|
|
25
|
+
@on_progress = on_progress
|
|
26
|
+
@download_pool = Downloader::Pool.new(on_progress: on_progress)
|
|
27
|
+
@package = GemPkg::Package.new
|
|
28
|
+
@results = []
|
|
29
|
+
@mutex = Thread::Mutex.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Prepare all plan entries: download + extract as needed.
|
|
33
|
+
# Returns array of PreparedGem.
|
|
34
|
+
def prepare(entries)
|
|
35
|
+
# Sort by estimated size descending (larger gems first for better parallelism).
|
|
36
|
+
# Use version as rough proxy for size if no other info available.
|
|
37
|
+
sorted = entries.sort_by do |e|
|
|
38
|
+
s = e.spec
|
|
39
|
+
name = s.respond_to?(:name) ? s.name : s[:name]
|
|
40
|
+
-(name.length) # Longer names tend to be larger packages
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
to_download = []
|
|
44
|
+
already_cached = []
|
|
45
|
+
|
|
46
|
+
sorted.each do |entry|
|
|
47
|
+
inbound = @layout.inbound_path(entry.spec)
|
|
48
|
+
extracted = @layout.extracted_path(entry.spec)
|
|
49
|
+
|
|
50
|
+
if File.directory?(extracted)
|
|
51
|
+
# Already extracted -- load gemspec from cache or re-read
|
|
52
|
+
gemspec = load_cached_spec(entry.spec) || read_gemspec_from_extracted(extracted, entry.spec)
|
|
53
|
+
already_cached << PreparedGem.new(
|
|
54
|
+
spec: entry.spec,
|
|
55
|
+
extracted_path: extracted,
|
|
56
|
+
gemspec: gemspec,
|
|
57
|
+
from_cache: true,
|
|
58
|
+
)
|
|
59
|
+
elsif File.exist?(inbound)
|
|
60
|
+
# Downloaded but not extracted
|
|
61
|
+
already_cached << extract_gem(entry.spec, inbound)
|
|
62
|
+
else
|
|
63
|
+
# Need to download
|
|
64
|
+
to_download << entry
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
# Batch download everything that's missing
|
|
70
|
+
if to_download.any?
|
|
71
|
+
download_items = to_download.map do |entry|
|
|
72
|
+
spec = entry.spec
|
|
73
|
+
source_uri = gem_download_uri(entry)
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
uri: source_uri,
|
|
77
|
+
dest: @layout.inbound_path(spec),
|
|
78
|
+
spec: spec,
|
|
79
|
+
checksum: nil,
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
download_results = @download_pool.download_batch(download_items)
|
|
84
|
+
|
|
85
|
+
download_results.each do |dr|
|
|
86
|
+
if dr[:error]
|
|
87
|
+
name = dr[:spec].respond_to?(:name) ? dr[:spec].name : dr[:spec][:name]
|
|
88
|
+
raise InstallError, "Failed to download #{name}: #{dr[:error].message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
already_cached << extract_gem(dr[:spec], dr[:path])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
ensure
|
|
95
|
+
@download_pool.close
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
@results = already_cached
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
already_cached
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Prepare a single entry (for use with scheduler).
|
|
106
|
+
def prepare_one(entry)
|
|
107
|
+
inbound = @layout.inbound_path(entry.spec)
|
|
108
|
+
extracted = @layout.extracted_path(entry.spec)
|
|
109
|
+
|
|
110
|
+
if File.directory?(extracted)
|
|
111
|
+
gemspec = load_cached_spec(entry.spec) || read_gemspec_from_extracted(extracted, entry.spec)
|
|
112
|
+
return PreparedGem.new(
|
|
113
|
+
spec: entry.spec,
|
|
114
|
+
extracted_path: extracted,
|
|
115
|
+
gemspec: gemspec,
|
|
116
|
+
from_cache: true,
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
unless File.exist?(inbound)
|
|
121
|
+
uri = gem_download_uri(entry)
|
|
122
|
+
@download_pool.download(uri, inbound)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
extract_gem(entry.spec, inbound)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def extract_gem(spec, gem_path)
|
|
131
|
+
dest = @layout.extracted_path(spec)
|
|
132
|
+
|
|
133
|
+
if File.directory?(dest)
|
|
134
|
+
gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(dest, spec)
|
|
135
|
+
return PreparedGem.new(spec: spec, extracted_path: dest, gemspec: gemspec, from_cache: true)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Extract to temp dir, then atomic move
|
|
139
|
+
tmp_dest = "#{dest}.#{Process.pid}.tmp"
|
|
140
|
+
FileUtils.rm_rf(tmp_dest) if File.exist?(tmp_dest)
|
|
141
|
+
|
|
142
|
+
result = @package.extract(gem_path, tmp_dest)
|
|
143
|
+
FS.atomic_move(tmp_dest, dest)
|
|
144
|
+
|
|
145
|
+
# Cache the gemspec as Marshal for fast future loads
|
|
146
|
+
cache_spec(spec, result[:gemspec])
|
|
147
|
+
|
|
148
|
+
PreparedGem.new(
|
|
149
|
+
spec: spec,
|
|
150
|
+
extracted_path: dest,
|
|
151
|
+
gemspec: result[:gemspec],
|
|
152
|
+
from_cache: false,
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def load_cached_spec(spec)
|
|
157
|
+
path = @layout.spec_cache_path(spec)
|
|
158
|
+
return nil unless File.exist?(path)
|
|
159
|
+
Marshal.load(File.binread(path))
|
|
160
|
+
rescue ArgumentError, TypeError, EOFError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def cache_spec(spec, gemspec)
|
|
165
|
+
path = @layout.spec_cache_path(spec)
|
|
166
|
+
FS.atomic_write(path, Marshal.dump(gemspec))
|
|
167
|
+
rescue StandardError
|
|
168
|
+
# Non-fatal: cache miss on next load
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def read_gemspec_from_extracted(extracted_dir, spec)
|
|
172
|
+
pattern = File.join(extracted_dir, "*.gemspec")
|
|
173
|
+
candidates = Dir.glob(pattern)
|
|
174
|
+
if candidates.any?
|
|
175
|
+
begin
|
|
176
|
+
::Gem::Specification.load(candidates.first)
|
|
177
|
+
rescue StandardError
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def gem_download_uri(entry)
|
|
184
|
+
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
|
|
194
|
+
|
|
195
|
+
# Use cached_path if provided, otherwise construct from source
|
|
196
|
+
if entry.cached_path
|
|
197
|
+
entry.cached_path
|
|
198
|
+
elsif entry.gem_path
|
|
199
|
+
entry.gem_path
|
|
200
|
+
else
|
|
201
|
+
# Default to rubygems.org
|
|
202
|
+
"https://rubygems.org/gems/#{filename}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|