Module: Lich::GemCheck

Defined in:
documented/gemcheck.rb

Overview

Verifies bundled gems are installed at Lich startup and alerts the user via a native OS dialog (with a log-file fallback) when any are missing. Runs once during boot, before scripts load.

Defined Under Namespace

Classes: ConsentError

Constant Summary collapse

WINDOWS_MESSAGE =

User-facing alert message displayed on Windows when required gems are missing.

Instructs users to update Ruby using the Ruby4Lich5 installer.

Returns:

"You're missing required Ruby gems!\n\n" \
"Please update to the latest Ruby version using the\n" \
"Ruby4Lich5 installer."
UNIX_MESSAGE =

User-facing alert message displayed on macOS and Linux when required gems are missing.

Instructs users to run 'bundle install' from their Lich5 folder.

Returns:

"You're missing required Ruby gems!\n\n" \
"Please run 'bundle install' from your Lich5 folder."
TITLE =
'Lich5: Missing Ruby Gems'
RELEASE_URL =

URL to the latest Lich5 release on GitHub, presented to Windows users as a download link.

Returns:

'https://github.com/elanthia-online/lich-5/releases/latest'
LOG_FILENAME =
'lich5-missing-gems.log'
120
BUNDLER_RECOVERY_TIMEOUT_SECONDS =
120
ALERT_TIMEOUT_SECONDS =
120

Class Method Summary collapse

Class Method Details

.alert(missing: [], groups: [:default], error: nil) ⇒ void

This method returns an undefined value.

Parameters:

  • missing (Array<String>) (defaults to: [])

    gem names identified by our detector

  • groups (Array<Symbol>) (defaults to: [:default])

    groups being verified

  • error (Exception, nil) (defaults to: nil)

    the Bundler or require exception, if any



303
304
305
306
307
308
309
310
311
# File 'documented/gemcheck.rb', line 303

def alert(missing: [], groups: [:default], error: nil)
  write_log(missing: missing, groups: groups, error: error)
  body = build_alert_body(missing, error)
  case RUBY_PLATFORM
  when /mswin|mingw|cygwin/ then alert_windows(body)
  when /darwin/             then alert_macos(body)
  else                           alert_linux(body)
  end
end

.alert_linux(body) ⇒ void

This method returns an undefined value.

Parameters:



526
527
528
529
530
531
532
533
534
535
536
# File 'documented/gemcheck.rb', line 526

def alert_linux(body)
  if cmd_available?('zenity')
    run_with_timeout(['zenity', '--info', '--title', TITLE, '--text', body], ALERT_TIMEOUT_SECONDS)
  elsif cmd_available?('kdialog')
    run_with_timeout(['kdialog', '--title', TITLE, '--msgbox', body], ALERT_TIMEOUT_SECONDS)
  elsif cmd_available?('xmessage')
    run_with_timeout(['xmessage', '-center', body], ALERT_TIMEOUT_SECONDS)
  else
    warn "!!ALERT!! #{body}"
  end
end

.alert_macos(body) ⇒ void

This method returns an undefined value.

Parameters:



507
508
509
510
511
512
513
514
515
516
# File 'documented/gemcheck.rb', line 507

def alert_macos(body)
  script = %(display dialog #{macos_dialog_body(body)} ) +
           %(with title #{TITLE.inspect} ) +
           %(buttons {"OK"} default button "OK" with icon caution)
  IO.popen(['osascript', '-'], 'r+') do |io|
    io.write(script)
    io.close_write
    io.read
  end
end

.alert_windows(body) ⇒ void

This method returns an undefined value.

Parameters:



497
498
499
500
501
502
503
# File 'documented/gemcheck.rb', line 497

def alert_windows(body)
  require 'win32ole'
  shell = WIN32OLE.new('WScript.Shell')
  result = shell.Popup("#{body}\nClick OK to open the download page.",
                       ALERT_TIMEOUT_SECONDS, TITLE, 1 + 64) # OK/Cancel + Information icon
  shell.Run(RELEASE_URL) if result == 1
end

.all_groupsArray<Symbol>

Returns all groups declared in the Gemfile.

Returns:

  • (Array<Symbol>)

    all groups declared in the Gemfile



293
294
295
296
297
# File 'documented/gemcheck.rb', line 293

def all_groups
  Bundler.definition.groups
rescue StandardError
  [:default]
end

.build_alert_body(missing, error) ⇒ String

Composes the alert dialog body: platform message + a bulleted list of detected missing gems, plus the underlying error when one was captured. The error is always shown when present: a failed require 'gtk3' may mean a native DLL failed to load even though every gem is installed, so hiding the message behind the missing-gems list misdiagnoses the fault.

Parameters:

  • missing (Array<String>)
  • error (Exception, nil)

Returns:



329
330
331
332
333
334
335
336
337
338
339
# File 'documented/gemcheck.rb', line 329

def build_alert_body(missing, error)
  parts = [message]
  if missing.any?
    parts << "Missing gems:\n  - #{missing.join("\n  - ")}"
  end
  if error
    parts << "Underlying error:\n  #{error.message.lines.first.to_s.strip}"
  end
  parts << "See #{File.join(TEMP_DIR, LOG_FILENAME)} for details." if defined?(TEMP_DIR)
  parts.join("\n\n")
end

.build_recovery_prompt(units) ⇒ String

Parameters:

  • units (Array<Hash>)

    validated manifest recovery units

Returns:



235
236
237
238
239
240
241
242
243
244
# File 'documented/gemcheck.rb', line 235

def build_recovery_prompt(units)
  listed_units = Array(units).map do |unit|
    members = Array(unit['members'])
    details = members.length > 1 ? ": #{members.join(', ')}" : ''
    "  - #{recovery_unit_label(unit)}#{details}"
  end
  "Required Ruby gems are not installed:\n#{listed_units.join("\n")}\n\n" \
    "Lich can download and install the approved, hash-verified packages now.\n\n" \
    'Install now?'
end

.bundler_recovery_supported?(groups) ⇒ Boolean

The initial non-Windows recovery is deliberately macOS-only and only covers default runtime gems. GTK remains outside this Bundler path.

Parameters:

  • groups (Array<Symbol>)

Returns:

  • (Boolean)


115
116
117
# File 'documented/gemcheck.rb', line 115

def bundler_recovery_supported?(groups)
  BundlerRecovery.supported? && Array(groups).map(&:to_sym) == [:default]
end

.cmd_available?(cmd) ⇒ Boolean

Parameters:

  • cmd (String)

    executable name to probe

Returns:

  • (Boolean)


540
541
542
# File 'documented/gemcheck.rb', line 540

def cmd_available?(cmd)
  system('which', cmd, out: File::NULL, err: File::NULL)
end

.configure_gemfile!void

This method returns an undefined value.

Ensures Bundler resolves Lich's Gemfile even when the app is launched from another working directory, such as macOS app launch from /.



95
96
97
98
99
100
101
# File 'documented/gemcheck.rb', line 95

def configure_gemfile!
  return if ENV['BUNDLE_GEMFILE'] && !ENV['BUNDLE_GEMFILE'].empty?
  return unless defined?(LICH_DIR)

  gemfile = File.join(LICH_DIR, 'Gemfile')
  ENV['BUNDLE_GEMFILE'] = gemfile if File.file?(gemfile)
end

.confirm_bundler_recovery(gem_names) ⇒ Symbol

Returns :approved, :declined, :timed_out, or :unavailable.

Parameters:

Returns:

  • (Symbol)

    :approved, :declined, :timed_out, or :unavailable



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'documented/gemcheck.rb', line 211

def confirm_bundler_recovery(gem_names)
  return :unavailable unless BundlerRecovery.supported?

  body = "Required Ruby gems are not installed:\n#{Array(gem_names).map { |name| "  - #{name}" }.join("\n")}\n\n" \
         "Lich can install the non-GTK runtime bundle to its private directory now.\n" \
         "Your Gemfile and Gemfile.lock will not be changed.\n\nInstall now?"
  script = %(display dialog #{macos_dialog_body(body)} with title #{TITLE.inspect} ) +
           %(buttons {"Install", "Cancel"} default button "Install" with icon caution ) +
           "giving up after #{BUNDLER_RECOVERY_TIMEOUT_SECONDS}"
  output = IO.popen(['osascript', '-'], 'r+') do |io|
    io.write(script)
    io.close_write
    io.read
  end
  return :timed_out if output.include?('gave up:true')
  return :approved if output.include?('button returned:Install')

  :declined
rescue StandardError
  :unavailable
end

.confirm_recovery_units(units) ⇒ Symbol

Returns :approved, :declined, or :unavailable.

Parameters:

  • units (Array<Hash>)

    validated manifest recovery units

Returns:

  • (Symbol)

    :approved, :declined, or :unavailable



200
201
202
203
204
205
206
207
# File 'documented/gemcheck.rb', line 200

def confirm_recovery_units(units)
  return :unavailable unless self_healing_supported?

  body = build_recovery_prompt(units)
  confirm_windows(body)
rescue StandardError
  :unavailable
end

.confirm_windows(body) ⇒ Symbol

Returns :approved, :declined, or :timed_out.

Parameters:

Returns:

  • (Symbol)

    :approved, :declined, or :timed_out



458
459
460
461
462
463
464
# File 'documented/gemcheck.rb', line 458

def confirm_windows(body)
  result = windows_popup(body, 4 + 32) # Yes/No buttons + question icon
  return :approved if result == 6 # Yes
  return :timed_out if result == -1 # WScript Popup timeout

  :declined
end

Returns loggable reason for not installing.

Parameters:

  • decision (Symbol)

    consent dialog outcome

Returns:

  • (String)

    loggable reason for not installing



191
192
193
194
195
196
# File 'documented/gemcheck.rb', line 191

def consent_failure_reason(decision)
  return 'user consent not available' if decision == :unavailable
  return 'user consent timed out' if decision == :timed_out

  'user declined installation'
end

.dependency_report(groups) ⇒ String

Builds a per-dependency status line for every gem in the requested groups: name, requirement, and whether it's installed. This is the single most useful piece of debug output when the detector disagrees with Bundler.

Parameters:

  • groups (Array<Symbol>)

Returns:



434
435
436
437
438
439
440
441
442
443
444
445
# File 'documented/gemcheck.rb', line 434

def dependency_report(groups)
  deps = Bundler.definition.current_dependencies.select do |dep|
    (dep.groups & groups).any?
  end
  return '(none)' if deps.empty?

  deps.sort_by(&:name).map do |dep|
    installed = Gem::Specification.find_all_by_name(dep.name, dep.requirement)
    status = installed.any? ? "OK (#{installed.map(&:version).join(', ')})" : 'MISSING'
    "#{dep.name.ljust(24)} #{dep.requirement.to_s.ljust(20)} #{status}"
  end.join("\n")
end

.macos_dialog_body(body) ⇒ String

Returns AppleScript expression retaining each line break.

Parameters:

Returns:

  • (String)

    AppleScript expression retaining each line break



520
521
522
# File 'documented/gemcheck.rb', line 520

def macos_dialog_body(body)
  body.split("\n").map(&:inspect).join(' & return & ')
end

.messageString

Returns the message appropriate for the current platform.

Returns:

  • (String)

    the message appropriate for the current platform



314
315
316
317
318
319
# File 'documented/gemcheck.rb', line 314

def message
  case RUBY_PLATFORM
  when /mswin|mingw|cygwin/ then WINDOWS_MESSAGE
  else                           UNIX_MESSAGE
  end
end

.missing_gems(groups = [:default]) ⇒ Array<String>

Names of gems declared in the Gemfile that are not installed at any version satisfying the declared requirement, scoped to the given groups. Relies on installed Gem::Specifications; git/path-sourced gems are not detected here (the current Gemfile has none).

Parameters:

  • groups (Array<Symbol>) (defaults to: [:default])

    groups being verified

Returns:

  • (Array<String>)

    sorted, unique gem names



282
283
284
285
286
287
288
289
290
# File 'documented/gemcheck.rb', line 282

def missing_gems(groups = [:default])
  Bundler.definition.current_dependencies.select do |dep|
    (dep.groups & groups).any?
  end.reject do |dep|
    Gem::Specification.find_all_by_name(dep.name, dep.requirement).any?
  end.map(&:name).sort.uniq
rescue StandardError
  []
end

.notice_windows(body) ⇒ void

This method returns an undefined value.

Parameters:



490
491
492
493
# File 'documented/gemcheck.rb', line 490

def notice_windows(body)
  require 'win32ole'
  WIN32OLE.new('WScript.Shell').Popup(body, CONSENT_TIMEOUT_SECONDS, TITLE, 64) # OK + information icon
end

.recover_with_bundler_consent!(gem_names, groups: [:default]) ⇒ BundlerRecovery::Result?

Runs a consented Bundler repair for the default non-GTK runtime gems on macOS. The recovery object uses a shipped lockfile when present, or resolves only within staging before making the private RubyGems home active.

Parameters:

  • gem_names (Array<String>)
  • groups (Array<Symbol>) (defaults to: [:default])

Returns:



158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'documented/gemcheck.rb', line 158

def recover_with_bundler_consent!(gem_names, groups: [:default])
  recovery = BundlerRecovery.new(lich_dir: LICH_DIR)
  if (reason = recovery.preflight(gem_names))
    return BundlerRecovery::Result.new(error: reason)
  end

  decision = confirm_bundler_recovery(gem_names)
  unless decision == :approved
    report_bundler_consent_failure(gem_names, groups, consent_failure_reason(decision))
    return nil
  end

  recovery.recover(gem_names)
end

.recover_with_consent!(gem_names, force: false, groups: [:default]) ⇒ DependencyRecovery::Result?

Fetches and validates the manifest, requests consent for each affected recovery unit, then performs the download and installation only after all units were approved. Returns nil when consent was declined or unavailable; that case is already logged and presented to the user.

Parameters:

  • gem_names (Array<String>)

    names to recover

  • force (Boolean) (defaults to: false)

    reinstall an already registered manifest package

  • groups (Array<Symbol>) (defaults to: [:default])

    dependency groups being recovered

Returns:



141
142
143
144
145
146
147
148
149
# File 'documented/gemcheck.rb', line 141

def recover_with_consent!(gem_names, force: false, groups: [:default])
  recovery = DependencyRecovery.new
  plan = recovery.recovery_plan(gem_names)
  return DependencyRecovery::Result.new(installed_gems: [], error: plan.error) unless plan.success?
  return nil unless recovery_units_approved?(plan.units, groups)

  write_recovery_log(missing: gem_names, groups: groups, units: plan.units)
  recovery.recover(gem_names, force: force, plan: plan)
end

.recovery_unit_label(unit) ⇒ String

Parameters:

  • unit (Hash)

    validated manifest recovery unit

Returns:



248
249
250
251
252
253
254
# File 'documented/gemcheck.rb', line 248

def recovery_unit_label(unit)
  members = Array(unit['members'])
  return "#{members.first} gem" if members.length == 1
  return 'GTK3 runtime bundle' if unit['id'] == 'gtk3-runtime'

  "#{unit.fetch('id').tr('-', ' ')} bundle"
end

.recovery_units_approved?(units, groups) ⇒ Boolean

Requests one consent decision for every affected unit before any artifact download or installation begins. This prevents a declined bundle from leaving an earlier approved unit partially installed.

Parameters:

  • units (Array<Hash>)

    validated manifest units

  • groups (Array<Symbol>)

    dependency groups being recovered

Returns:

  • (Boolean)


180
181
182
183
184
185
186
187
# File 'documented/gemcheck.rb', line 180

def recovery_units_approved?(units, groups)
  decision = confirm_recovery_units(units)
  return true if decision == :approved

  reason = consent_failure_reason(decision)
  report_consent_failure(units, groups, reason)
  false
end

This method returns an undefined value.

Parameters:

  • missing (Array<String>)
  • groups (Array<Symbol>)
  • reason (String)


271
272
273
274
# File 'documented/gemcheck.rb', line 271

def report_bundler_consent_failure(missing, groups, reason)
  write_log(missing: missing, groups: groups, error: ConsentError.new(reason))
  show_notice("Required gem#{'s' if missing.length != 1} #{missing.join(', ')} not installed. Exiting.")
end

This method returns an undefined value.

Parameters:

  • units (Array<Hash>)

    recovery units the user did not approve

  • groups (Array<Symbol>)

    dependency groups being recovered

  • reason (String)

    user-decision or UI-availability reason



260
261
262
263
264
265
# File 'documented/gemcheck.rb', line 260

def report_consent_failure(units, groups, reason)
  error = ConsentError.new(reason)
  missing = units.flat_map { |unit| Array(unit['members']) }.uniq
  write_log(missing: missing, groups: groups, error: error)
  show_notice("Required gem#{'s' if missing.length != 1} #{missing.join(', ')} not installed. Exiting.")
end

.run_with_timeout(cmd, timeout) ⇒ Boolean

Runs an external command that would otherwise block indefinitely (a GUI dialog with no one present to dismiss it, e.g. a dead or forwarded X display) and forcibly reclaims control once the timeout elapses. Wrapping a blocking Kernel#system call in Timeout can't do this safely, since the spawned child keeps running as an orphan; spawning it ourselves lets us kill it directly.

Parameters:

  • cmd (Array<String>)

    command and arguments for Process.spawn

  • timeout (Integer)

    seconds to wait before killing the process

Returns:

  • (Boolean)

    whether the command exited on its own within the timeout



553
554
555
556
557
558
559
560
561
# File 'documented/gemcheck.rb', line 553

def run_with_timeout(cmd, timeout)
  pid = Process.spawn(*cmd, out: File::NULL, err: File::NULL)
  Timeout.timeout(timeout) { Process.wait(pid) }
  true
rescue Timeout::Error
  Process.kill('TERM', pid)
  Process.wait(pid)
  false
end

.safe_call { ... } ⇒ Object, String

Wraps a block, returning its result or a placeholder string on error.

Yields:

  • the value to compute

Returns:



450
451
452
453
454
# File 'documented/gemcheck.rb', line 450

def safe_call
  yield
rescue StandardError => e
  "(unavailable: #{e.class}: #{e.message})"
end

.self_healing_supported?Boolean

Ruby4Lich5 currently publishes and validates recovery artifacts only for the Windows runtime. Other platforms retain the ordinary missing-gem warning and never fetch the recovery manifest.

Returns:

  • (Boolean)


107
108
109
# File 'documented/gemcheck.rb', line 107

def self_healing_supported?
  Gem.win_platform?
end

.show_notice(body) ⇒ void

This method returns an undefined value.

Shows an error without the normal missing-gem alert's release-page link.

Parameters:



478
479
480
481
482
483
484
485
486
# File 'documented/gemcheck.rb', line 478

def show_notice(body)
  case RUBY_PLATFORM
  when /mswin|mingw|cygwin/ then notice_windows(body)
  when /darwin/             then alert_macos(body)
  else                           alert_linux(body)
  end
rescue StandardError
  warn "!!ALERT!! #{body}"
end

.startup_groups(argv = ARGV) ⇒ Array<Symbol>

Chooses the dependency groups that must be present before normal startup. Only Windows verifies GTK here, because Ruby4Lich5 publishes a Windows recovery unit for it. On every platform, init.rb treats GTK as required unless the user explicitly passes --no-gtk or --no-gui.

Parameters:

  • argv (Array<String>) (defaults to: ARGV)

    command-line arguments

Returns:

  • (Array<Symbol>)


126
127
128
129
130
# File 'documented/gemcheck.rb', line 126

def startup_groups(argv = ARGV)
  groups = [:default]
  groups << :gtk if self_healing_supported? && !Array(argv).any? { |arg| arg.match?(/^--no-(?:gtk|gui)$/i) }
  groups
end

.verify!(*groups) ⇒ void

This method returns an undefined value.

Verifies every gem required by the requested Bundler groups is installed, alerting the user (native OS dialog, with a log-file fallback) and exiting when any remain missing after one manifest-backed recovery attempt. This remains a presence check only: it does not call Bundler.setup or lock the load path, leaving scripts free to require gems they install at runtime.

Parameters:

  • groups (Array<Symbol>)

    Bundler groups to verify



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'documented/gemcheck.rb', line 51

def verify!(*groups)
  groups = [:default] if groups.empty?
  configure_gemfile!

  missing = missing_gems(groups)
  return if missing.empty?

  if self_healing_supported?
    result = recover_with_consent!(missing, groups: groups)
    exit 1 unless result
    exit 0 if result.restart_required

    if result.success?
      missing = missing_gems(groups)
      return if missing.empty?

      alert(missing: missing, groups: groups)
    else
      alert(missing: missing, groups: groups,
            error: DependencyRecovery::Error.new(result.error))
    end
    exit 1
  end

  if bundler_recovery_supported?(groups)
    result = recover_with_bundler_consent!(missing, groups: groups)
    exit 1 unless result

    if result.success?
      write_bundler_recovery_log(missing: missing, groups: groups, result: result)
      exit(result.restart_required ? 0 : 1)
    end

    alert(missing: missing, groups: groups, error: DependencyRecovery::Error.new(result.error))
    exit 1
  else
    alert(missing: missing, groups: groups)
    exit 1
  end
end

.windows_popup(body, flags) ⇒ Integer

Returns WScript Popup result code.

Parameters:

  • body (String)
  • flags (Integer)

    WScript Popup button and icon flags

Returns:

  • (Integer)

    WScript Popup result code



469
470
471
472
473
# File 'documented/gemcheck.rb', line 469

def windows_popup(body, flags)
  require 'win32ole'
  shell = WIN32OLE.new('WScript.Shell')
  shell.Popup(body, CONSENT_TIMEOUT_SECONDS, TITLE, flags)
end

.write_bundler_recovery_log(missing:, groups:, result:) ⇒ void

This method returns an undefined value.

Parameters:



355
356
357
358
# File 'documented/gemcheck.rb', line 355

def write_bundler_recovery_log(missing:, groups:, result:)
  write_log(missing: missing, groups: groups, event: 'Bundler recovery',
            recovery_note: "Private non-GTK bundle staged and activated. Details: #{result.log_path}")
end

.write_log(missing: [], groups: [:default], error: nil, event: 'failure', recovery_units: nil, recovery_note: nil) ⇒ void

This method returns an undefined value.

Parameters:

  • missing (Array<String>) (defaults to: [])
  • groups (Array<Symbol>) (defaults to: [:default])
  • error (Exception, nil) (defaults to: nil)
  • event (String) (defaults to: 'failure')

    log event name

  • recovery_units (Array<Hash>, nil) (defaults to: nil)

    approved manifest units

  • recovery_note (String, nil) (defaults to: nil)

    additional approved recovery detail



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'documented/gemcheck.rb', line 367

def write_log(missing: [], groups: [:default], error: nil, event: 'failure', recovery_units: nil, recovery_note: nil)
  log_path = File.join(TEMP_DIR, LOG_FILENAME)
  # verify! can run before init.rb creates TEMP_DIR (fresh install), so
  # ensure the directory exists or the alert would cite a log we never wrote.
  Dir.mkdir(TEMP_DIR) unless File.exist?(TEMP_DIR)
  File.open(log_path, 'a') do |f|
    f.puts "[#{Time.now}] Lich5 GemCheck #{event}"
    f.puts message.gsub(/^/, '  ') if event == 'failure'
    f.puts

    if recovery_units
      f.puts '  Approved manifest recovery units:'
      recovery_units.each do |unit|
        f.puts "    - #{recovery_unit_label(unit)}: #{Array(unit['members']).join(', ')}"
      end
      f.puts
    end

    if recovery_note
      f.puts "  Recovery: #{recovery_note}"
      f.puts
    end

    f.puts '  Diagnostics:'
    f.puts "    Ruby:            #{RUBY_DESCRIPTION}"
    f.puts "    Bundler:         #{safe_call { Bundler::VERSION }}"
    f.puts "    Gemfile:         #{safe_call { Bundler.default_gemfile }}"
    f.puts "    Lockfile:        #{safe_call { Bundler.default_lockfile }}"
    f.puts "    Working dir:     #{Dir.pwd}"
    f.puts "    Groups checked:  #{groups.inspect}"
    f.puts "    All groups:      #{all_groups.inspect}"
    f.puts

    if missing.any?
      f.puts '  Missing gems (detected):'
      missing.each { |name| f.puts "    - #{name}" }
    else
      f.puts '  Missing gems (detected): none identified by GemCheck'
    end
    f.puts

    if error
      f.puts '  Bundler error:'
      f.puts "    Class:   #{error.class}"
      error.message.each_line { |line| f.puts "    #{line.chomp}" }
      f.puts
    end

    f.puts '  Declared dependencies in requested groups:'
    safe_call { dependency_report(groups) }.to_s.each_line do |line|
      f.puts "    #{line.chomp}"
    end
    f.puts

    f.puts "  Download: #{RELEASE_URL}" if event == 'failure' && RUBY_PLATFORM =~ /mswin|mingw|cygwin/
    f.puts
  end
rescue StandardError
  # Filesystem write failed; continue to GUI attempt.
end

.write_recovery_log(missing:, groups:, units:) ⇒ void

This method returns an undefined value.

Records a successful user-approved recovery attempt so self-healing is observable even when Lich subsequently starts without an error dialog.

Parameters:

  • missing (Array<String>)
  • groups (Array<Symbol>)
  • units (Array<Hash>)


347
348
349
# File 'documented/gemcheck.rb', line 347

def write_recovery_log(missing:, groups:, units:)
  write_log(missing: missing, groups: groups, event: 'recovery', recovery_units: units)
end