Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix file paths for exec launcher on Windows #3073

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions exe/ruby-lsp
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ 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)
flags = []
flags << "--debug" if options[:debug]
exit exec(Gem.ruby, File.expand_path("ruby-lsp-launcher", __dir__), *flags)
end

require_relative "../lib/ruby_lsp/setup_bundler"
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
Loading