#!/usr/bin/env ruby # frozen_string_literal: true # !!!!!!! # No gems can be required in this file until we invoke bundler setup except inside the forked process that sets up the # composed bundle # !!!!!!! setup_error = nil install_error = nil # Read the initialize request before even starting the server. We need to do this to figure out the workspace URI. # Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need # to ensure that we're setting up the bundle in the right place $stdin.binmode headers = $stdin.gets("\r\n\r\n") content_length = headers[/Content-Length: (\d+)/i, 1].to_i raw_initialize = $stdin.read(content_length) # Compose the Ruby LSP bundle in a forked process so that we can require gems without polluting the main process # `$LOAD_PATH` and `Gem.loaded_specs`. Windows doesn't support forking, so we need a separate path to support it pid = if Gem.win_platform? # Since we can't fork on Windows and spawn won't carry over the existing load paths, we need to explicitly pass that # down to the child process or else requiring gems during composing the bundle will fail load_path = $LOAD_PATH.flat_map do |path| ["-I", File.expand_path(path)] end Process.spawn( Gem.ruby, *load_path, File.expand_path("../lib/ruby_lsp/scripts/compose_bundle_windows.rb", __dir__), raw_initialize, ) else fork do require_relative "../lib/ruby_lsp/scripts/compose_bundle" compose(raw_initialize) end end begin # Wait until the composed Bundle is finished Process.wait(pid) rescue Errno::ECHILD # In theory, the child process can finish before we even get to the wait call, but that is not an error end begin bundle_env_path = File.join(".ruby-lsp", "bundle_env") # We can't require `bundler/setup` because that file prematurely exits the process if setup fails. However, we can't # simply require bundler either because the version required might conflict with the one locked in the composed # bundle. We need the composed bundle sub-process to inform us of the locked Bundler version, so that we can then # activate the right spec and require the exact Bundler version required by the app if File.exist?(bundle_env_path) env = File.readlines(bundle_env_path).to_h { |line| line.chomp.split("=", 2) } ENV.merge!(env) if env["BUNDLER_VERSION"] Gem::Specification.find_by_name("bundler", env["BUNDLER_VERSION"]).activate end require "bundler" Bundler.ui.level = :silent # This Marshal load can only happen after requiring Bundler because it will load a custom error class from Bundler # itself. If we try to load before requiring, the class will not be defined and loading will fail error_path = File.join(".ruby-lsp", "install_error") install_error = if File.exist?(error_path) Marshal.load(File.read(error_path)) end Bundler.setup $stderr.puts("Composed Bundle set up successfully") end rescue StandardError => e # If installing gems failed for any reason, we don't want to exit the process prematurely. We can still provide most # features in a degraded mode. We simply save the error so that we can report to the user that certain gems might be # missing, but we respect the LSP life cycle. # # If an install error occurred and one of the gems is not installed, Bundler.setup is guaranteed to fail with # `Bundler::GemNotFound`. We don't set `setup_error` here because that would essentially report the same problem twice # to telemetry, which is not useful unless install_error && e.is_a?(Bundler::GemNotFound) setup_error = e $stderr.puts("Failed to set up composed Bundle\n#{e.full_message}") end # If Bundler.setup fails, we need to restore the original $LOAD_PATH so that we can still require the Ruby LSP server # in degraded mode $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) end # Now that the bundle is set up, we can begin actually launching the server. Note that `Bundler.setup` will have already # configured the load path using the version of the Ruby LSP present in the composed bundle. Do not push any Ruby LSP # paths into the load path manually or we may end up requiring the wrong version of the gem require "ruby_lsp/load_sorbet" require "ruby_lsp/internal" T::Utils.run_all_sig_blocks if ARGV.include?("--debug") if ["x64-mingw-ucrt", "x64-mingw32"].include?(RUBY_PLATFORM) $stderr.puts "Debugging is not supported on Windows" else begin ENV.delete("RUBY_DEBUG_IRB_CONSOLE") require "debug/open_nonstop" rescue LoadError $stderr.puts("You need to install the debug gem to use the --debug flag") end end end # Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device. $> = $stderr initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize begin RubyLsp::Server.new( install_error: install_error, setup_error: setup_error, initialize_request: initialize_request, ).start rescue ArgumentError # If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat # and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize # request manually and then start the main loop server = RubyLsp::Server.new server.process_message(initialize_request) server.start end