Class: Lich::BundlerRecovery
- Inherits:
-
Object
- Object
- Lich::BundlerRecovery
- Defined in:
- documented/bundler_recovery.rb
Overview
Recovers missing non-GTK runtime gems on macOS. Bundler resolves and builds in Lich's temporary directory first. A detached helper then promotes only the missing gems and their unavailable runtime dependencies into Gem.dir, rolling back every touched gem if validation fails.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- LOG_FILENAME =
'lich5-bundler-recovery.log'- LOCK_FILENAME =
'.lich-bundler-recovery.lock'- EXCLUDED_GROUPS =
Bundler dependency groups excluded from Bundler recovery staging.
%w[gtk development vscode profanity].freeze
- NATIVE_DEFAULT_GEMS =
Native gems requiring build tools and development headers during recovery.
%w[ox sqlite3].freeze
Class Method Summary collapse
-
.log_helper_failure(payload_or_path, error) ⇒ void
private
Logs a failure from the macOS gem promotion helper process to the recovery log.
-
.run_macos_replacement(payload_path) ⇒ Object
Runs after the Lich process exits.
-
.supported? ⇒ Boolean
private
Returns true if the current Ruby platform is macOS.
Instance Method Summary collapse
-
#initialize(lich_dir:, gem_home: Gem.dir, helper_launcher: nil, installer: nil) ⇒ void
constructor
Creates a new recovery manager for staging and promoting gems on macOS.
-
#preflight(missing) ⇒ String?
Validates prerequisites for macOS Bundler recovery.
-
#recover(missing) ⇒ Object
Stages a complete resolver result, but promotes only the requested gems and runtime dependencies not already satisfiable in the real Gem.dir.
Constructor Details
#initialize(lich_dir:, gem_home: Gem.dir, helper_launcher: nil, installer: nil) ⇒ void
Creates a new recovery manager for staging and promoting gems on macOS.
104 105 106 107 108 109 |
# File 'documented/bundler_recovery.rb', line 104 def initialize(lich_dir:, gem_home: Gem.dir, helper_launcher: nil, installer: nil) @lich_dir = File.(lich_dir) @gem_home = File.(gem_home) @helper_launcher = helper_launcher || method(:launch_macos_helper) @installer = installer || method(:install_gem) end |
Class Method Details
.log_helper_failure(payload_or_path, error) ⇒ void
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
This method returns an undefined value.
Logs a failure from the macOS gem promotion helper process to the recovery log.
Gracefully handles missing or unparseable payload data by falling back to a default temporary directory. If logging fails, the error is silently ignored.
70 71 72 73 74 75 76 |
# File 'documented/bundler_recovery.rb', line 70 def log_helper_failure(payload_or_path, error) payload = helper_payload(payload_or_path) path = File.join(payload.fetch('temp_dir', helper_temp_dir(payload_or_path)), LOG_FILENAME) File.open(path, 'a') { |file| file.puts("[#{Time.now}] macOS gem promotion failed\n #{error.class}: #{error.}\n") } rescue StandardError nil end |
.run_macos_replacement(payload_path) ⇒ Object
Runs after the Lich process exits. The helper owns promotion, rollback, cleanup, and restart so the running Ruby never changes its own gems.
50 51 52 53 54 55 56 57 58 59 |
# File 'documented/bundler_recovery.rb', line 50 def run_macos_replacement(payload_path) payload = nil payload = JSON.parse(File.read(payload_path)) new(lich_dir: payload.fetch('lich_dir'), gem_home: payload.fetch('gem_home')) .send(:run_macos_replacement!, payload, payload_path: payload_path) true rescue StandardError => e log_helper_failure(payload || payload_path, e) false end |
.supported? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns true if the current Ruby platform is macOS.
44 45 46 |
# File 'documented/bundler_recovery.rb', line 44 def supported? RUBY_PLATFORM.match?(/darwin/) end |
Instance Method Details
#preflight(missing) ⇒ String?
Validates prerequisites for macOS Bundler recovery.
Checks for macOS runtime support, required files and executables, and (when promoting native gems) development headers and build tools. Returns an error message if any check fails, or nil if all checks pass.
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'documented/bundler_recovery.rb', line 123 def preflight(missing) return 'macOS Bundler recovery is not supported by this Ruby runtime' unless self.class.supported? return "Gemfile not found at #{gemfile}" unless File.file?(gemfile) return "Ruby executable is not available at #{Gem.ruby}" unless File.executable?(Gem.ruby) return 'Bundler is not available from this Ruby runtime' unless command_available?(Gem.ruby, '-S', 'bundle', '--version') return nil unless (Array(missing) & NATIVE_DEFAULT_GEMS).any? return "Ruby development headers are not available at #{ruby_headers}" unless File.directory?(ruby_headers) return 'Xcode Command Line Tools are required to build native Ruby gems' unless command_available?('xcrun', '--find', 'clang') return 'make is required to build native Ruby gems' unless command_available?('make', '--version') nil end |
#recover(missing) ⇒ Object
Stages a complete resolver result, but promotes only the requested gems and runtime dependencies not already satisfiable in the real Gem.dir.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'documented/bundler_recovery.rb', line 139 def recover(missing) reason = preflight(missing) return failure(reason) if reason FileUtils.mkdir_p(temp_dir) work_dir = Dir.mktmpdir('lich-bundler-recovery-', temp_dir) staging_path = File.join(work_dir, 'staging') staged_gemfile, frozen = stage_gemfile_files(staging_path) stdout, stderr, status = Open3.capture3(bundler_environment(staging_path, staged_gemfile, frozen: frozen), Gem.ruby, '-S', 'bundle', 'install', chdir: @lich_dir) transcript = "#{stdout}#{stderr}" write_transcript(transcript, status.success?) return failure("Bundler install failed (see #{log_path})") unless status.success? packages = promotion_packages(missing, staging_path) payload_path = File.join(work_dir, 'macos-gem-promotion.json') File.write(payload_path, JSON.generate(promotion_payload(work_dir, packages))) @helper_launcher.call(payload_path) work_dir = nil # The helper cleans its verified staging data after promotion. Result.new(log_path: log_path, restart_required: true) rescue StandardError => e failure("#{e.class}: #{e.}") ensure FileUtils.rm_rf(work_dir) if defined?(work_dir) && work_dir && Dir.exist?(work_dir) end |