Skip to content

Commit

Permalink
Fix launcher file paths on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jan 27, 2025
1 parent cd85897 commit 7f629ac
Show file tree
Hide file tree
Showing 12 changed files with 372 additions and 210 deletions.
14 changes: 11 additions & 3 deletions exe/ruby-lsp
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,17 @@ if ENV["BUNDLE_GEMFILE"].nil?
# which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to
# install rather than failing to boot the server completely
if options[:launcher]
command = +"#{Gem.ruby} #{File.expand_path("ruby-lsp-launcher", __dir__)}"
command << " --debug" if options[:debug]
exit exec(command)
# Run `/path/to/ruby /path/to/exe/ruby-lsp-launcher` and ensuring that the Windows long format path is normalized
# for exec
ruby_path = Gem.ruby
ruby_path = ruby_path.gsub("/", "\\") if ruby_path.start_with?("//?/")

launcher_path = File.expand_path("ruby-lsp-launcher", __dir__)
launcher_path.gsub!("/", "\\") if launcher_path.start_with?("//?/")

flags = []
flags << " --debug" if options[:debug]
exit exec(ruby_path, launcher_path, *flags)
end

require_relative "../lib/ruby_lsp/setup_bundler"
Expand Down
2 changes: 1 addition & 1 deletion exe/ruby-lsp-launcher
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ setup_error = nil
install_error = nil
reboot = false

workspace_uri = ARGV.first
workspace_uri = ARGV.first.strip

raw_initialize = if workspace_uri && !workspace_uri.start_with?("--")
# If there's an argument without `--`, then it's the server asking to compose the bundle and passing to this
Expand Down
10 changes: 7 additions & 3 deletions vscode/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,14 @@ function collectClientOptions(

ruby.gemPath.forEach((gemPath) => {
supportedSchemes.forEach((scheme) => {
// On Windows, gem paths may be using backslashes, but those are not valid as a glob pattern. We need to ensure
// that we're using forward slashes for the document selectors
const pathAsGlobPattern = gemPath.replace(/\\/g, "/");

documentSelector.push({
scheme,
language: "ruby",
pattern: `${gemPath}/**/*`,
pattern: `${pathAsGlobPattern}/**/*`,
});

// Because of how default gems are installed, the gemPath location is actually not exactly where the files are
Expand All @@ -193,11 +197,11 @@ function collectClientOptions(
//
// Notice that we still need to add the regular path to the selector because some version managers will install
// gems under the non-corrected path
if (/lib\/ruby\/gems\/(?=\d)/.test(gemPath)) {
if (/lib\/ruby\/gems\/(?=\d)/.test(pathAsGlobPattern)) {
documentSelector.push({
scheme,
language: "ruby",
pattern: `${gemPath.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`,
pattern: `${pathAsGlobPattern.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`,
});
}
});
Expand Down
7 changes: 5 additions & 2 deletions vscode/src/ruby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,11 @@ export class Ruby implements RubyInterface {

this.sanitizeEnvironment(env);

// We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths
process.env = env;
if (this.context.extensionMode !== vscode.ExtensionMode.Test) {
// We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths
process.env = env;
}

this._env = env;
this.rubyVersion = version;
this.yjitEnabled = (yjit && major > 3) || (major === 3 && minor >= 2);
Expand Down
78 changes: 39 additions & 39 deletions vscode/src/ruby/chruby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,45 @@ export class Chruby extends VersionManager {
return undefined;
}

// Run the activation script using the Ruby installation we found so that we can discover gem paths
protected async runActivationScript(
rubyExecutableUri: vscode.Uri,
rubyVersion: RubyVersion,
): Promise<{
defaultGems: string;
gemHome: string;
yjit: boolean;
version: string;
}> {
// Typically, GEM_HOME points to $HOME/.gem/ruby/version_without_patch. For example, for Ruby 3.2.2, it would be
// $HOME/.gem/ruby/3.2.0. However, chruby overrides GEM_HOME to use the patch part of the version, resulting in
// $HOME/.gem/ruby/3.2.2. In our activation script, we check if a directory using the patch exists and then prefer
// that over the default one.
const script = [
"user_dir = Gem.user_dir",
"paths = Gem.path",
"if paths.length > 2",
" paths.delete(Gem.default_dir)",
" paths.delete(Gem.user_dir)",
" if paths[0]",
" user_dir = paths[0] if Dir.exist?(paths[0])",
" end",
"end",
`newer_gem_home = File.join(File.dirname(user_dir), "${rubyVersion.version}")`,
"gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)",
`STDERR.print([Gem.default_dir, gems, !!defined?(RubyVM::YJIT), RUBY_VERSION].join("${ACTIVATION_SEPARATOR}"))`,
].join(";");

const result = await this.runScript(
`${rubyExecutableUri.fsPath} -W0 -e '${script}'`,
);

const [defaultGems, gemHome, yjit, version] =
result.stderr.split(ACTIVATION_SEPARATOR);

return { defaultGems, gemHome, yjit: yjit === "true", version };
}

private async findClosestRubyInstallation(rubyVersion: RubyVersion): Promise<{
uri: vscode.Uri;
rubyVersion: RubyVersion;
Expand Down Expand Up @@ -443,45 +482,6 @@ export class Chruby extends VersionManager {
throw new Error("Cannot find any Ruby installations");
}

// Run the activation script using the Ruby installation we found so that we can discover gem paths
private async runActivationScript(
rubyExecutableUri: vscode.Uri,
rubyVersion: RubyVersion,
): Promise<{
defaultGems: string;
gemHome: string;
yjit: boolean;
version: string;
}> {
// Typically, GEM_HOME points to $HOME/.gem/ruby/version_without_patch. For example, for Ruby 3.2.2, it would be
// $HOME/.gem/ruby/3.2.0. However, chruby overrides GEM_HOME to use the patch part of the version, resulting in
// $HOME/.gem/ruby/3.2.2. In our activation script, we check if a directory using the patch exists and then prefer
// that over the default one.
const script = [
"user_dir = Gem.user_dir",
"paths = Gem.path",
"if paths.length > 2",
" paths.delete(Gem.default_dir)",
" paths.delete(Gem.user_dir)",
" if paths[0]",
" user_dir = paths[0] if Dir.exist?(paths[0])",
" end",
"end",
`newer_gem_home = File.join(File.dirname(user_dir), "${rubyVersion.version}")`,
"gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)",
`STDERR.print([Gem.default_dir, gems, !!defined?(RubyVM::YJIT), RUBY_VERSION].join("${ACTIVATION_SEPARATOR}"))`,
].join(";");

const result = await this.runScript(
`${rubyExecutableUri.fsPath} -W0 -e '${script}'`,
);

const [defaultGems, gemHome, yjit, version] =
result.stderr.split(ACTIVATION_SEPARATOR);

return { defaultGems, gemHome, yjit: yjit === "true", version };
}

private missingRubyError(version: string) {
return new Error(`Cannot find Ruby installation for version ${version}`);
}
Expand Down
23 changes: 23 additions & 0 deletions vscode/src/ruby/rubyInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,29 @@ export class RubyInstaller extends Chruby {
);
}

protected async runActivationScript(
rubyExecutableUri: vscode.Uri,
rubyVersion: RubyVersion,
): Promise<{
defaultGems: string;
gemHome: string;
yjit: boolean;
version: string;
}> {
const activationResult = await super.runActivationScript(
rubyExecutableUri,
rubyVersion,
);

activationResult.gemHome = activationResult.gemHome.replace(/\//g, "\\");
activationResult.defaultGems = activationResult.defaultGems.replace(
/\//g,
"\\",
);

return activationResult;
}

// Override the `runScript` method to ensure that we do not pass any `shell` to `asyncExec`. The activation script is
// only compatible with `cmd.exe`, and not Powershell, due to escaping of quotes. We need to ensure to always run the
// script on `cmd.exe`.
Expand Down
93 changes: 11 additions & 82 deletions vscode/src/test/suite/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,41 +31,10 @@ import { after, afterEach, before } from "mocha";
import { Ruby, ManagerIdentifier } from "../../ruby";
import Client from "../../client";
import { WorkspaceChannel } from "../../workspaceChannel";
import { RUBY_VERSION, MAJOR, MINOR } from "../rubyVersion";
import { MAJOR, MINOR } from "../rubyVersion";

import { FAKE_TELEMETRY } from "./fakeTelemetry";

class FakeLogger {
receivedMessages = "";

trace(message: string, ..._args: any[]): void {
this.receivedMessages += message;
}

debug(message: string, ..._args: any[]): void {
this.receivedMessages += message;
}

info(message: string, ..._args: any[]): void {
this.receivedMessages += message;
}

warn(message: string, ..._args: any[]): void {
this.receivedMessages += message;
}

error(error: string | Error, ..._args: any[]): void {
this.receivedMessages += error.toString();
}

append(value: string): void {
this.receivedMessages += value;
}

appendLine(value: string): void {
this.receivedMessages += value;
}
}
import { FAKE_TELEMETRY, FakeLogger } from "./fakeTelemetry";
import { createRubySymlinks } from "./helpers";

async function launchClient(workspaceUri: vscode.Uri) {
const workspaceFolder: vscode.WorkspaceFolder = {
Expand All @@ -85,6 +54,8 @@ async function launchClient(workspaceUri: vscode.Uri) {
const fakeLogger = new FakeLogger();
const outputChannel = new WorkspaceChannel("fake", fakeLogger as any);

let managerConfig;

// Ensure that we're activating the correct Ruby version on CI
if (process.env.CI) {
await vscode.workspace
Expand All @@ -94,54 +65,12 @@ async function launchClient(workspaceUri: vscode.Uri) {
.getConfiguration("rubyLsp")
.update("linters", ["rubocop_internal"], true);

if (os.platform() === "linux") {
await vscode.workspace
.getConfiguration("rubyLsp")
.update(
"rubyVersionManager",
{ identifier: ManagerIdentifier.Chruby },
true,
);

fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
fs.symlinkSync(
`/opt/hostedtoolcache/Ruby/${RUBY_VERSION}/x64`,
path.join(os.homedir(), ".rubies", RUBY_VERSION),
);
} else if (os.platform() === "darwin") {
await vscode.workspace
.getConfiguration("rubyLsp")
.update(
"rubyVersionManager",
{ identifier: ManagerIdentifier.Chruby },
true,
);

fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
fs.symlinkSync(
`/Users/runner/hostedtoolcache/Ruby/${RUBY_VERSION}/arm64`,
path.join(os.homedir(), ".rubies", RUBY_VERSION),
);
createRubySymlinks();

if (os.platform() === "win32") {
managerConfig = { identifier: ManagerIdentifier.RubyInstaller };
} else {
await vscode.workspace
.getConfiguration("rubyLsp")
.update(
"rubyVersionManager",
{ identifier: ManagerIdentifier.RubyInstaller },
true,
);

fs.symlinkSync(
path.join(
"C:",
"hostedtoolcache",
"windows",
"Ruby",
RUBY_VERSION,
"x64",
),
path.join("C:", `Ruby${MAJOR}${MINOR}-${os.arch()}`),
);
managerConfig = { identifier: ManagerIdentifier.Chruby };
}
}

Expand All @@ -151,7 +80,7 @@ async function launchClient(workspaceUri: vscode.Uri) {
outputChannel,
FAKE_TELEMETRY,
);
await ruby.activateRuby();
await ruby.activateRuby(managerConfig);
ruby.env.RUBY_LSP_BYPASS_TYPECHECKER = "true";

const virtualDocuments = new Map<string, string>();
Expand Down
Loading

0 comments on commit 7f629ac

Please sign in to comment.