scint 0.1.0 → 0.6.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.
@@ -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 = 5
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 = "".freeze
29
- PANEL_PHASE_ORDER = %i[download extract build_ext link].freeze
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 any_stream_activity?
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 && stream_type?(type)
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
- @output.puts "#{RED}FAILED#{RESET} #{label} #{BOLD}#{name}#{RESET}: #{error.message}"
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 any_stream_activity?
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
- lines.concat(phase_lines(spinner))
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.flat_map do |type|
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, *reserve_phase_detail_space(type, detail_rows, total, completed, active)]
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
- if active_jobs.empty?
261
- elapsed = @phase_elapsed[type]
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
- rows = details.first(reserved)
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
- prev_width = @rendered_widths[idx] || 0
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
- pad_len = prev_width > line_width ? (prev_width - line_width) : 0
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
- render_live_locked
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
- any_stream_activity? && !stream_active_or_pending?
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, _reqs|
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
@@ -93,8 +94,9 @@ module Scint
93
94
  entries = info_for(name)
94
95
  deps = {}
95
96
 
96
- entries.each do |_name, ver, platform, dep_hash, _reqs|
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
102
  req = Gem::Requirement.new(dep_req_str.split(", "))
@@ -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, _reqs|
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, _reqs|
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,54 @@ 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
228
279
  end
229
280
  end
230
281
  end
@@ -1,3 +1,7 @@
1
1
  module Scint::PubGrub
2
- VERSION = "0.5.0"
2
+ VERSION = if defined?(::Scint::VERSION)
3
+ ::Scint::VERSION
4
+ else
5
+ File.read(File.expand_path("../../../../VERSION", __dir__)).strip
6
+ end
3
7
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Scint
4
+ VERSION = File.read(File.expand_path("../../VERSION", __dir__)).strip
5
+ end
data/lib/scint.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Scint
4
- VERSION = "0.1.0"
3
+ require_relative "scint/version"
5
4
 
5
+ module Scint
6
6
  # Color support: respects NO_COLOR (https://no-color.org) and TERM=dumb.
7
7
  COLOR = !ENV.key?("NO_COLOR") && ENV["TERM"] != "dumb" && $stderr.tty?
8
8
 
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
- - Scint Contributors
7
+ - Tobi Lutke
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -16,17 +16,13 @@ email:
16
16
  - maintainers@example.com
17
17
  executables:
18
18
  - scint
19
- - scint-io-summary
20
- - scint-syscall-trace
21
19
  extensions: []
22
20
  extra_rdoc_files: []
23
21
  files:
24
22
  - FEATURES.md
25
23
  - README.md
26
- - bin/bundler-vs-scint
24
+ - VERSION
27
25
  - bin/scint
28
- - bin/scint-io-summary
29
- - bin/scint-syscall-trace
30
26
  - lib/bundler.rb
31
27
  - lib/bundler/setup.rb
32
28
  - lib/scint.rb
@@ -90,6 +86,7 @@ files:
90
86
  - lib/scint/vendor/pub_grub/version_range.rb
91
87
  - lib/scint/vendor/pub_grub/version_solver.rb
92
88
  - lib/scint/vendor/pub_grub/version_union.rb
89
+ - lib/scint/version.rb
93
90
  - lib/scint/worker_pool.rb
94
91
  homepage: https://example.com/scint
95
92
  licenses: