scint 0.1.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.
- checksums.yaml +4 -4
- data/README.md +90 -41
- data/VERSION +1 -0
- data/bin/scint +9 -0
- data/lib/bundler.rb +106 -0
- data/lib/scint/cache/layout.rb +16 -14
- data/lib/scint/cache/metadata_store.rb +4 -11
- data/lib/scint/cli/cache.rb +2 -1
- data/lib/scint/cli/exec.rb +12 -24
- data/lib/scint/cli/install.rb +1214 -134
- data/lib/scint/credentials.rb +78 -15
- data/lib/scint/debug/io_trace.rb +26 -7
- data/lib/scint/downloader/fetcher.rb +25 -1
- data/lib/scint/downloader/pool.rb +67 -15
- data/lib/scint/errors.rb +10 -0
- data/lib/scint/fs.rb +90 -3
- data/lib/scint/gemfile/parser.rb +31 -4
- data/lib/scint/index/client.rb +1 -1
- data/lib/scint/installer/extension_builder.rb +95 -30
- data/lib/scint/installer/linker.rb +9 -25
- data/lib/scint/installer/planner.rb +37 -13
- data/lib/scint/installer/preparer.rb +2 -9
- data/lib/scint/lockfile/parser.rb +2 -1
- data/lib/scint/lockfile/writer.rb +78 -35
- data/lib/scint/platform.rb +8 -0
- data/lib/scint/progress.rb +128 -73
- data/lib/scint/resolver/provider.rb +71 -7
- data/lib/scint/runtime/exec.rb +52 -26
- data/lib/scint/runtime/setup.rb +29 -1
- data/lib/scint/scheduler.rb +6 -1
- data/lib/scint/spec_utils.rb +58 -0
- data/lib/scint/vendor/pub_grub/version.rb +5 -1
- data/lib/scint/version.rb +5 -0
- data/lib/scint.rb +3 -2
- metadata +5 -7
- data/bin/bundler-vs-scint +0 -233
- data/bin/scint-io-summary +0 -46
- data/bin/scint-syscall-trace +0 -41
data/lib/scint/progress.rb
CHANGED
|
@@ -18,15 +18,22 @@ module Scint
|
|
|
18
18
|
}.freeze
|
|
19
19
|
BUILD_TAIL_MAX = 6
|
|
20
20
|
BUILD_TAIL_PREVIEW_LINES = 4
|
|
21
|
-
RENDER_HZ =
|
|
21
|
+
RENDER_HZ = 10
|
|
22
22
|
RENDER_INTERVAL = 1.0 / RENDER_HZ
|
|
23
23
|
MAX_DETAIL_ROWS_PER_PHASE = 4
|
|
24
24
|
MAX_LINE_LEN = 220
|
|
25
25
|
MIN_RENDER_WIDTH = 40
|
|
26
26
|
MAX_PANEL_ROWS = 14
|
|
27
|
+
SLOW_OPERATION_THRESHOLD_SECONDS = 1.0
|
|
27
28
|
SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
28
|
-
IDLE_MARK = "
|
|
29
|
-
PANEL_PHASE_ORDER = %i[download extract build_ext
|
|
29
|
+
IDLE_MARK = "•".freeze
|
|
30
|
+
PANEL_PHASE_ORDER = %i[download extract link build_ext].freeze
|
|
31
|
+
COMPLETION_LOG_TYPES = {
|
|
32
|
+
download: true,
|
|
33
|
+
extract: true,
|
|
34
|
+
link: true,
|
|
35
|
+
build_ext: true,
|
|
36
|
+
}.freeze
|
|
30
37
|
|
|
31
38
|
PHASE_LABELS = {
|
|
32
39
|
fetch_index: "Fetching index",
|
|
@@ -43,6 +50,12 @@ module Scint
|
|
|
43
50
|
link: "Installing",
|
|
44
51
|
build_ext: "Compiling",
|
|
45
52
|
}.freeze
|
|
53
|
+
COMPLETION_LABELS = {
|
|
54
|
+
download: "Downloaded",
|
|
55
|
+
extract: "Extracted",
|
|
56
|
+
link: "Installed",
|
|
57
|
+
build_ext: "Compiled",
|
|
58
|
+
}.freeze
|
|
46
59
|
|
|
47
60
|
def initialize(output: $stderr)
|
|
48
61
|
@output = output
|
|
@@ -55,6 +68,7 @@ module Scint
|
|
|
55
68
|
@failed = Hash.new(0)
|
|
56
69
|
@total = Hash.new(0)
|
|
57
70
|
@active_jobs = {}
|
|
71
|
+
@job_started_at = {}
|
|
58
72
|
@build_tail = []
|
|
59
73
|
@build_tail_by_name = {}
|
|
60
74
|
@spinner_idx = 0
|
|
@@ -67,6 +81,7 @@ module Scint
|
|
|
67
81
|
@phase_reserved_detail_rows = Hash.new(0)
|
|
68
82
|
@setup_lines_printed = false
|
|
69
83
|
@setup_gap_printed = false
|
|
84
|
+
@pending_log_lines = []
|
|
70
85
|
@render_stop = false
|
|
71
86
|
@render_thread = nil
|
|
72
87
|
end
|
|
@@ -90,8 +105,9 @@ module Scint
|
|
|
90
105
|
|
|
91
106
|
@mutex.synchronize do
|
|
92
107
|
begin
|
|
108
|
+
flush_pending_logs_locked
|
|
93
109
|
mark_completed_phase_elapsed_locked
|
|
94
|
-
render_live_locked if
|
|
110
|
+
render_live_locked if any_active_stream_jobs?
|
|
95
111
|
if @interactive && @live_rows.positive?
|
|
96
112
|
move_cursor_down(@live_rows - 1)
|
|
97
113
|
@output.print "\r\n"
|
|
@@ -119,11 +135,16 @@ module Scint
|
|
|
119
135
|
|
|
120
136
|
@mutex.synchronize do
|
|
121
137
|
@active_jobs[job_id] = { type: type, name: name }
|
|
138
|
+
@job_started_at[job_id] = Process.clock_gettime(Process::CLOCK_MONOTONIC) if completion_log_type?(type)
|
|
122
139
|
@started += 1
|
|
123
140
|
emit_setup_gap_if_needed(type)
|
|
124
141
|
@phase_started_at[type] ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) if stream_type?(type)
|
|
125
|
-
if @interactive
|
|
142
|
+
if @interactive
|
|
126
143
|
start_render_thread_locked
|
|
144
|
+
unless stream_type?(type)
|
|
145
|
+
clear_live_block_locked
|
|
146
|
+
emit_setup_or_log_line(type, name)
|
|
147
|
+
end
|
|
127
148
|
else
|
|
128
149
|
clear_live_block_locked
|
|
129
150
|
emit_setup_or_log_line(type, name)
|
|
@@ -136,6 +157,8 @@ module Scint
|
|
|
136
157
|
@completed[type] += 1
|
|
137
158
|
active = @active_jobs.delete(job_id)
|
|
138
159
|
@build_tail_by_name.delete(active[:name]) if active
|
|
160
|
+
elapsed = consume_job_elapsed(job_id)
|
|
161
|
+
emit_task_completion_locked(type, name, elapsed) if @interactive
|
|
139
162
|
if !@interactive || !stream_type?(type)
|
|
140
163
|
emit_phase_completion_locked(type)
|
|
141
164
|
end
|
|
@@ -147,11 +170,22 @@ module Scint
|
|
|
147
170
|
|
|
148
171
|
@mutex.synchronize do
|
|
149
172
|
active = @active_jobs.delete(job_id)
|
|
173
|
+
elapsed = consume_job_elapsed(job_id)
|
|
150
174
|
@build_tail_by_name.delete(active[:name]) if active
|
|
151
175
|
@failed[type] += 1
|
|
152
|
-
clear_live_block_locked
|
|
153
176
|
label = PHASE_LABELS[type] || type.to_s
|
|
154
|
-
|
|
177
|
+
failed_timing = if elapsed && elapsed >= SLOW_OPERATION_THRESHOLD_SECONDS
|
|
178
|
+
" #{DIM}#{format_phase_elapsed(elapsed)}#{RESET}"
|
|
179
|
+
else
|
|
180
|
+
""
|
|
181
|
+
end
|
|
182
|
+
failed_line = "#{RED}FAILED#{RESET} #{label} #{BOLD}#{name}#{RESET}: #{error.message}#{failed_timing}"
|
|
183
|
+
if @interactive
|
|
184
|
+
queue_log_line_locked(failed_line)
|
|
185
|
+
else
|
|
186
|
+
clear_live_block_locked
|
|
187
|
+
@output.puts failed_line
|
|
188
|
+
end
|
|
155
189
|
if !@interactive
|
|
156
190
|
emit_phase_completion_locked(type)
|
|
157
191
|
end
|
|
@@ -197,9 +231,68 @@ module Scint
|
|
|
197
231
|
HIDDEN_TYPES[type] == true
|
|
198
232
|
end
|
|
199
233
|
|
|
234
|
+
def completion_log_type?(type)
|
|
235
|
+
COMPLETION_LOG_TYPES[type] == true
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def consume_job_elapsed(job_id)
|
|
239
|
+
started_at = @job_started_at.delete(job_id)
|
|
240
|
+
return nil unless started_at
|
|
241
|
+
|
|
242
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def emit_task_completion_locked(type, name, elapsed)
|
|
246
|
+
return unless completion_log_type?(type)
|
|
247
|
+
|
|
248
|
+
label = COMPLETION_LABELS[type] || (PHASE_LABELS[type] || type.to_s)
|
|
249
|
+
timing = if elapsed && elapsed >= SLOW_OPERATION_THRESHOLD_SECONDS
|
|
250
|
+
" #{DIM}#{format_phase_elapsed(elapsed)}#{RESET}"
|
|
251
|
+
else
|
|
252
|
+
""
|
|
253
|
+
end
|
|
254
|
+
line = "#{GREEN}#{IDLE_MARK}#{RESET} #{DIM}#{label} #{BOLD}#{name}#{RESET}#{timing}"
|
|
255
|
+
|
|
256
|
+
queue_log_line_locked(line)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def queue_log_line_locked(line)
|
|
260
|
+
@pending_log_lines << line
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def flush_pending_logs_locked
|
|
264
|
+
return if @pending_log_lines.empty?
|
|
265
|
+
|
|
266
|
+
lines = @pending_log_lines.dup
|
|
267
|
+
@pending_log_lines.clear
|
|
268
|
+
lines.each_with_index do |line, idx|
|
|
269
|
+
write_scrollback_line_locked(line, flush: idx == lines.length - 1)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def write_scrollback_line_locked(line, flush: true)
|
|
274
|
+
rendered = fit_line(line, @render_width)
|
|
275
|
+
|
|
276
|
+
unless @interactive && @live_rows.positive?
|
|
277
|
+
@output.print "\r"
|
|
278
|
+
@output.puts rendered
|
|
279
|
+
@output.flush if flush && @output.respond_to?(:flush)
|
|
280
|
+
return
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Insert one scrollback line above the active block without clearing
|
|
284
|
+
# existing rows first. This reduces visible flicker during fast updates.
|
|
285
|
+
move_cursor_up(@live_rows - 1)
|
|
286
|
+
@output.print "\e[1L"
|
|
287
|
+
@output.print "\r#{rendered}\e[K\n"
|
|
288
|
+
move_cursor_down(@live_rows - 1)
|
|
289
|
+
@output.print "\r"
|
|
290
|
+
@output.flush if flush && @output.respond_to?(:flush)
|
|
291
|
+
end
|
|
292
|
+
|
|
200
293
|
def render_live_locked
|
|
201
294
|
return unless @interactive
|
|
202
|
-
return unless
|
|
295
|
+
return unless any_active_stream_jobs? || any_active_setup_jobs?
|
|
203
296
|
|
|
204
297
|
mark_completed_phase_elapsed_locked
|
|
205
298
|
|
|
@@ -207,7 +300,11 @@ module Scint
|
|
|
207
300
|
spinner = SPINNER_FRAMES[@spinner_idx % SPINNER_FRAMES.length]
|
|
208
301
|
@spinner_idx += 1
|
|
209
302
|
|
|
210
|
-
|
|
303
|
+
if any_active_stream_jobs?
|
|
304
|
+
lines.concat(phase_lines(spinner))
|
|
305
|
+
else
|
|
306
|
+
lines << "#{GREEN}#{spinner}#{RESET} Processing..."
|
|
307
|
+
end
|
|
211
308
|
lines = clamp_panel_rows(lines)
|
|
212
309
|
|
|
213
310
|
redraw_live_block_locked(lines)
|
|
@@ -238,7 +335,8 @@ module Scint
|
|
|
238
335
|
end
|
|
239
336
|
|
|
240
337
|
def phase_lines(spinner)
|
|
241
|
-
PANEL_PHASE_ORDER.
|
|
338
|
+
active_types = PANEL_PHASE_ORDER.select { |type| !active_stream_jobs_for(type).empty? }
|
|
339
|
+
active_types.flat_map do |type|
|
|
242
340
|
phase_rows_for(type, spinner)
|
|
243
341
|
end
|
|
244
342
|
end
|
|
@@ -252,69 +350,21 @@ module Scint
|
|
|
252
350
|
label = PHASE_SUMMARY_LABELS[type] || (PHASE_LABELS[type] || type.to_s)
|
|
253
351
|
line = phase_status_line(type, active, label, completed, total, spinner)
|
|
254
352
|
detail_rows = phase_detail_rows_for(type, active)
|
|
255
|
-
[line, *
|
|
353
|
+
[line, *detail_rows.first(MAX_DETAIL_ROWS_PER_PHASE)]
|
|
256
354
|
end
|
|
257
355
|
|
|
258
356
|
def phase_status_line(type, active_jobs, label, completed, total, spinner)
|
|
259
357
|
base = "#{label}... (#{completed}/#{total})"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
elapsed_text = elapsed ? " · #{format_phase_elapsed(elapsed)}" : ""
|
|
263
|
-
"#{DIM}#{IDLE_MARK} #{base}#{elapsed_text}#{RESET}"
|
|
264
|
-
else
|
|
265
|
-
name = active_jobs.first[:name]
|
|
266
|
-
"#{spinner} #{base} · #{BOLD}#{name}#{RESET}"
|
|
267
|
-
end
|
|
358
|
+
name = active_jobs.first[:name]
|
|
359
|
+
"#{GREEN}#{spinner}#{RESET} #{base} · #{BOLD}#{name}#{RESET}"
|
|
268
360
|
end
|
|
269
361
|
|
|
270
362
|
def phase_detail_rows_for(type, active_jobs)
|
|
271
363
|
return [] if active_jobs.empty?
|
|
272
364
|
|
|
273
|
-
if type == :build_ext
|
|
274
|
-
compile_tail_rows_for(active_jobs.first[:name])
|
|
275
|
-
else
|
|
276
|
-
active_rows_for(type, active_jobs)
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def reserve_phase_detail_space(type, details, total, completed, active_jobs)
|
|
281
|
-
if phase_complete?(type, total, completed, active_jobs)
|
|
282
|
-
@phase_reserved_detail_rows[type] = 0
|
|
283
|
-
return []
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
desired = [details.length, MAX_DETAIL_ROWS_PER_PHASE].min
|
|
287
|
-
@phase_reserved_detail_rows[type] = [@phase_reserved_detail_rows[type], desired].max
|
|
288
|
-
reserved = @phase_reserved_detail_rows[type]
|
|
365
|
+
return compile_tail_rows_for(active_jobs.first[:name]) if type == :build_ext
|
|
289
366
|
|
|
290
|
-
|
|
291
|
-
while rows.length < reserved
|
|
292
|
-
rows << "#{DIM} #{RESET}"
|
|
293
|
-
end
|
|
294
|
-
rows
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
def phase_complete?(type, total, completed, active_jobs)
|
|
298
|
-
return false if total.zero?
|
|
299
|
-
|
|
300
|
-
if completed >= total && active_jobs.empty?
|
|
301
|
-
mark_phase_elapsed_locked(type)
|
|
302
|
-
return true
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
false
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def active_rows_for(_type, active_jobs)
|
|
309
|
-
return [] if active_jobs.empty?
|
|
310
|
-
if active_jobs.length <= MAX_DETAIL_ROWS_PER_PHASE
|
|
311
|
-
return active_jobs.map { |job| " #{BOLD}#{job[:name]}#{RESET}" }
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
rows = active_jobs.first(MAX_DETAIL_ROWS_PER_PHASE - 1).map { |job| " #{BOLD}#{job[:name]}#{RESET}" }
|
|
315
|
-
remaining = active_jobs.length - rows.length
|
|
316
|
-
rows << "#{DIM} +#{remaining} more#{RESET}"
|
|
317
|
-
rows
|
|
367
|
+
[]
|
|
318
368
|
end
|
|
319
369
|
|
|
320
370
|
def compile_tail_rows_for(name)
|
|
@@ -330,14 +380,21 @@ module Scint
|
|
|
330
380
|
@active_jobs.values.select { |job| job[:type] == type }
|
|
331
381
|
end
|
|
332
382
|
|
|
383
|
+
def any_active_stream_jobs?
|
|
384
|
+
STREAM_TYPES.keys.any? { |type| !active_stream_jobs_for(type).empty? }
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def any_active_setup_jobs?
|
|
388
|
+
@active_jobs.values.any? { |job| SETUP_TYPES[job[:type]] == true }
|
|
389
|
+
end
|
|
390
|
+
|
|
333
391
|
def clear_live_block_locked
|
|
334
392
|
return unless @interactive
|
|
335
393
|
return if @live_rows.zero?
|
|
336
394
|
|
|
337
395
|
move_cursor_up(@live_rows - 1)
|
|
338
396
|
@live_rows.times do |idx|
|
|
339
|
-
|
|
340
|
-
@output.print "\r#{' ' * prev_width}"
|
|
397
|
+
@output.print "\r\e[K"
|
|
341
398
|
@output.print "\n" if idx < (@live_rows - 1)
|
|
342
399
|
end
|
|
343
400
|
move_cursor_up(@live_rows - 1)
|
|
@@ -350,7 +407,6 @@ module Scint
|
|
|
350
407
|
def redraw_live_block_locked(lines)
|
|
351
408
|
hide_cursor_locked
|
|
352
409
|
previous_rows = @live_rows
|
|
353
|
-
previous_widths = @rendered_widths.dup
|
|
354
410
|
|
|
355
411
|
move_cursor_up(previous_rows - 1) if previous_rows.positive?
|
|
356
412
|
|
|
@@ -359,12 +415,8 @@ module Scint
|
|
|
359
415
|
|
|
360
416
|
max_rows.times do |idx|
|
|
361
417
|
line = idx < lines.length ? fit_line(lines[idx], @render_width) : ""
|
|
362
|
-
prev_width = previous_widths[idx] || 0
|
|
363
418
|
line_width = visible_width(line)
|
|
364
|
-
|
|
365
|
-
pad = pad_len.positive? ? (" " * pad_len) : ""
|
|
366
|
-
|
|
367
|
-
@output.print "\r#{line}#{pad}"
|
|
419
|
+
@output.print "\r#{line}\e[K"
|
|
368
420
|
@output.print "\n" if idx < (max_rows - 1)
|
|
369
421
|
new_widths << line_width
|
|
370
422
|
end
|
|
@@ -405,7 +457,8 @@ module Scint
|
|
|
405
457
|
@mutex.synchronize do
|
|
406
458
|
should_stop = @render_stop
|
|
407
459
|
unless should_stop
|
|
408
|
-
|
|
460
|
+
flush_pending_logs_locked
|
|
461
|
+
render_live_locked if any_active_stream_jobs?
|
|
409
462
|
should_stop = render_done_locked?
|
|
410
463
|
end
|
|
411
464
|
end
|
|
@@ -502,7 +555,9 @@ module Scint
|
|
|
502
555
|
end
|
|
503
556
|
|
|
504
557
|
def render_done_locked?
|
|
505
|
-
|
|
558
|
+
!any_active_stream_jobs? && !any_active_setup_jobs? &&
|
|
559
|
+
@pending_log_lines.empty? &&
|
|
560
|
+
(!any_stream_activity? || !stream_active_or_pending?)
|
|
506
561
|
end
|
|
507
562
|
|
|
508
563
|
def format_phase_elapsed(seconds)
|
|
@@ -65,8 +65,9 @@ module Scint
|
|
|
65
65
|
else
|
|
66
66
|
entries = info_for(name)
|
|
67
67
|
versions = {}
|
|
68
|
-
entries.each do |_name, version, platform, _deps,
|
|
68
|
+
entries.each do |_name, version, platform, _deps, reqs|
|
|
69
69
|
next unless platform_match?(platform)
|
|
70
|
+
next unless requirements_match?(reqs)
|
|
70
71
|
ver = Gem::Version.new(version)
|
|
71
72
|
versions[version] ||= ver
|
|
72
73
|
end
|
|
@@ -85,7 +86,7 @@ module Scint
|
|
|
85
86
|
pg = @path_gems[name]
|
|
86
87
|
deps = {}
|
|
87
88
|
(pg[:dependencies] || []).each do |dep_name, dep_req_str|
|
|
88
|
-
deps[dep_name] =
|
|
89
|
+
deps[dep_name] = build_requirement(dep_req_str)
|
|
89
90
|
end
|
|
90
91
|
deps
|
|
91
92
|
else
|
|
@@ -93,11 +94,12 @@ module Scint
|
|
|
93
94
|
entries = info_for(name)
|
|
94
95
|
deps = {}
|
|
95
96
|
|
|
96
|
-
entries.each do |_name, ver, platform, dep_hash,
|
|
97
|
+
entries.each do |_name, ver, platform, dep_hash, reqs|
|
|
97
98
|
next unless ver == version_str && platform_match?(platform)
|
|
99
|
+
next unless requirements_match?(reqs)
|
|
98
100
|
dep_hash.each do |dep_name, dep_req_str|
|
|
99
101
|
# Merge constraints from all matching platform entries
|
|
100
|
-
req =
|
|
102
|
+
req = build_requirement(dep_req_str)
|
|
101
103
|
deps[dep_name] = if deps[dep_name]
|
|
102
104
|
merge_requirements(deps[dep_name], req)
|
|
103
105
|
else
|
|
@@ -119,8 +121,9 @@ module Scint
|
|
|
119
121
|
version_str = version.to_s
|
|
120
122
|
entries = info_for(name)
|
|
121
123
|
has_platform_specific = false
|
|
122
|
-
entries.each do |_name, ver, platform, _deps,
|
|
124
|
+
entries.each do |_name, ver, platform, _deps, reqs|
|
|
123
125
|
next unless ver == version_str
|
|
126
|
+
next unless requirements_match?(reqs)
|
|
124
127
|
has_platform_specific = true if platform != "ruby"
|
|
125
128
|
end
|
|
126
129
|
has_platform_specific
|
|
@@ -132,8 +135,8 @@ module Scint
|
|
|
132
135
|
return "ruby" if @path_gems.key?(name)
|
|
133
136
|
|
|
134
137
|
version_str = version.to_s
|
|
135
|
-
entries = info_for(name).select do |_n, ver, platform, _deps,
|
|
136
|
-
ver == version_str && platform_match?(platform)
|
|
138
|
+
entries = info_for(name).select do |_n, ver, platform, _deps, reqs|
|
|
139
|
+
ver == version_str && platform_match?(platform) && requirements_match?(reqs)
|
|
137
140
|
end
|
|
138
141
|
return "ruby" if entries.empty?
|
|
139
142
|
|
|
@@ -225,6 +228,67 @@ module Scint
|
|
|
225
228
|
combined = req1.requirements + req2.requirements
|
|
226
229
|
Gem::Requirement.new(combined.map { |op, v| "#{op} #{v}" })
|
|
227
230
|
end
|
|
231
|
+
|
|
232
|
+
def requirements_match?(reqs)
|
|
233
|
+
return true unless reqs.is_a?(Hash)
|
|
234
|
+
|
|
235
|
+
ruby_req = reqs["ruby"] || reqs[:ruby]
|
|
236
|
+
return false unless requirement_satisfied?(ruby_req, Gem::Version.new(RUBY_VERSION), ignore_upper: ignore_ruby_upper_bounds?) if ruby_req
|
|
237
|
+
|
|
238
|
+
rubygems_req = reqs["rubygems"] || reqs[:rubygems]
|
|
239
|
+
return false unless requirement_satisfied?(rubygems_req, Gem::Version.new(Gem::VERSION), ignore_upper: false) if rubygems_req
|
|
240
|
+
|
|
241
|
+
true
|
|
242
|
+
rescue StandardError
|
|
243
|
+
# Keep resolver tolerant of malformed compact-index requirement fields.
|
|
244
|
+
true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def ignore_ruby_upper_bounds?
|
|
248
|
+
raw = ENV["SCINT_IGNORE_RUBY_UPPER_BOUNDS"]
|
|
249
|
+
return true if raw.nil?
|
|
250
|
+
|
|
251
|
+
!%w[0 false no off].include?(raw.strip.downcase)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def requirement_satisfied?(requirement_str, current_version, ignore_upper:)
|
|
255
|
+
req = normalize_requirement(requirement_str, ignore_upper: ignore_upper)
|
|
256
|
+
req.satisfied_by?(current_version)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def normalize_requirement(requirement_str, ignore_upper:)
|
|
260
|
+
parts = requirement_str.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
261
|
+
req = Gem::Requirement.new(*(parts.empty? ? [">= 0"] : parts))
|
|
262
|
+
return req unless ignore_upper
|
|
263
|
+
|
|
264
|
+
filtered = req.requirements.filter_map do |op, version|
|
|
265
|
+
case op
|
|
266
|
+
when "<", "<="
|
|
267
|
+
nil
|
|
268
|
+
when "~>"
|
|
269
|
+
[">=", version]
|
|
270
|
+
else
|
|
271
|
+
[op, version]
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
return Gem::Requirement.default if filtered.empty?
|
|
276
|
+
|
|
277
|
+
Gem::Requirement.new(filtered.map { |op, version| "#{op} #{version}" })
|
|
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
|
|
228
292
|
end
|
|
229
293
|
end
|
|
230
294
|
end
|
data/lib/scint/runtime/exec.rb
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
data/lib/scint/runtime/setup.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/scint/scheduler.rb
CHANGED
|
@@ -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
|
-
|
|
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
|