Skip to content

Commit 50af30c

Browse files
committed
Fix launcher file paths on Windows
1 parent cd85897 commit 50af30c

11 files changed

+363
-209
lines changed

exe/ruby-lsp

+3-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ if ENV["BUNDLE_GEMFILE"].nil?
6464
# which gives us the opportunity to control which specs are activated and enter degraded mode if any gems failed to
6565
# install rather than failing to boot the server completely
6666
if options[:launcher]
67-
command = +"#{Gem.ruby} #{File.expand_path("ruby-lsp-launcher", __dir__)}"
68-
command << " --debug" if options[:debug]
69-
exit exec(command)
67+
flags = []
68+
flags << "--debug" if options[:debug]
69+
exit exec(Gem.ruby, File.expand_path("ruby-lsp-launcher", __dir__), *flags)
7070
end
7171

7272
require_relative "../lib/ruby_lsp/setup_bundler"

vscode/src/client.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,14 @@ function collectClientOptions(
178178

179179
ruby.gemPath.forEach((gemPath) => {
180180
supportedSchemes.forEach((scheme) => {
181+
// On Windows, gem paths may be using backslashes, but those are not valid as a glob pattern. We need to ensure
182+
// that we're using forward slashes for the document selectors
183+
const pathAsGlobPattern = gemPath.replace(/\\/g, "/");
184+
181185
documentSelector.push({
182186
scheme,
183187
language: "ruby",
184-
pattern: `${gemPath}/**/*`,
188+
pattern: `${pathAsGlobPattern}/**/*`,
185189
});
186190

187191
// Because of how default gems are installed, the gemPath location is actually not exactly where the files are
@@ -193,11 +197,11 @@ function collectClientOptions(
193197
//
194198
// Notice that we still need to add the regular path to the selector because some version managers will install
195199
// gems under the non-corrected path
196-
if (/lib\/ruby\/gems\/(?=\d)/.test(gemPath)) {
200+
if (/lib\/ruby\/gems\/(?=\d)/.test(pathAsGlobPattern)) {
197201
documentSelector.push({
198202
scheme,
199203
language: "ruby",
200-
pattern: `${gemPath.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`,
204+
pattern: `${pathAsGlobPattern.replace(/lib\/ruby\/gems\/(?=\d)/, "lib/ruby/")}/**/*`,
201205
});
202206
}
203207
});

vscode/src/ruby.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,11 @@ export class Ruby implements RubyInterface {
260260

261261
this.sanitizeEnvironment(env);
262262

263-
// We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths
264-
process.env = env;
263+
if (this.context.extensionMode !== vscode.ExtensionMode.Test) {
264+
// We need to set the process environment too to make other extensions such as Sorbet find the right Ruby paths
265+
process.env = env;
266+
}
267+
265268
this._env = env;
266269
this.rubyVersion = version;
267270
this.yjitEnabled = (yjit && major > 3) || (major === 3 && minor >= 2);

vscode/src/ruby/chruby.ts

+39-39
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,45 @@ export class Chruby extends VersionManager {
169169
return undefined;
170170
}
171171

172+
// Run the activation script using the Ruby installation we found so that we can discover gem paths
173+
protected async runActivationScript(
174+
rubyExecutableUri: vscode.Uri,
175+
rubyVersion: RubyVersion,
176+
): Promise<{
177+
defaultGems: string;
178+
gemHome: string;
179+
yjit: boolean;
180+
version: string;
181+
}> {
182+
// Typically, GEM_HOME points to $HOME/.gem/ruby/version_without_patch. For example, for Ruby 3.2.2, it would be
183+
// $HOME/.gem/ruby/3.2.0. However, chruby overrides GEM_HOME to use the patch part of the version, resulting in
184+
// $HOME/.gem/ruby/3.2.2. In our activation script, we check if a directory using the patch exists and then prefer
185+
// that over the default one.
186+
const script = [
187+
"user_dir = Gem.user_dir",
188+
"paths = Gem.path",
189+
"if paths.length > 2",
190+
" paths.delete(Gem.default_dir)",
191+
" paths.delete(Gem.user_dir)",
192+
" if paths[0]",
193+
" user_dir = paths[0] if Dir.exist?(paths[0])",
194+
" end",
195+
"end",
196+
`newer_gem_home = File.join(File.dirname(user_dir), "${rubyVersion.version}")`,
197+
"gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)",
198+
`STDERR.print([Gem.default_dir, gems, !!defined?(RubyVM::YJIT), RUBY_VERSION].join("${ACTIVATION_SEPARATOR}"))`,
199+
].join(";");
200+
201+
const result = await this.runScript(
202+
`${rubyExecutableUri.fsPath} -W0 -e '${script}'`,
203+
);
204+
205+
const [defaultGems, gemHome, yjit, version] =
206+
result.stderr.split(ACTIVATION_SEPARATOR);
207+
208+
return { defaultGems, gemHome, yjit: yjit === "true", version };
209+
}
210+
172211
private async findClosestRubyInstallation(rubyVersion: RubyVersion): Promise<{
173212
uri: vscode.Uri;
174213
rubyVersion: RubyVersion;
@@ -443,45 +482,6 @@ export class Chruby extends VersionManager {
443482
throw new Error("Cannot find any Ruby installations");
444483
}
445484

446-
// Run the activation script using the Ruby installation we found so that we can discover gem paths
447-
private async runActivationScript(
448-
rubyExecutableUri: vscode.Uri,
449-
rubyVersion: RubyVersion,
450-
): Promise<{
451-
defaultGems: string;
452-
gemHome: string;
453-
yjit: boolean;
454-
version: string;
455-
}> {
456-
// Typically, GEM_HOME points to $HOME/.gem/ruby/version_without_patch. For example, for Ruby 3.2.2, it would be
457-
// $HOME/.gem/ruby/3.2.0. However, chruby overrides GEM_HOME to use the patch part of the version, resulting in
458-
// $HOME/.gem/ruby/3.2.2. In our activation script, we check if a directory using the patch exists and then prefer
459-
// that over the default one.
460-
const script = [
461-
"user_dir = Gem.user_dir",
462-
"paths = Gem.path",
463-
"if paths.length > 2",
464-
" paths.delete(Gem.default_dir)",
465-
" paths.delete(Gem.user_dir)",
466-
" if paths[0]",
467-
" user_dir = paths[0] if Dir.exist?(paths[0])",
468-
" end",
469-
"end",
470-
`newer_gem_home = File.join(File.dirname(user_dir), "${rubyVersion.version}")`,
471-
"gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)",
472-
`STDERR.print([Gem.default_dir, gems, !!defined?(RubyVM::YJIT), RUBY_VERSION].join("${ACTIVATION_SEPARATOR}"))`,
473-
].join(";");
474-
475-
const result = await this.runScript(
476-
`${rubyExecutableUri.fsPath} -W0 -e '${script}'`,
477-
);
478-
479-
const [defaultGems, gemHome, yjit, version] =
480-
result.stderr.split(ACTIVATION_SEPARATOR);
481-
482-
return { defaultGems, gemHome, yjit: yjit === "true", version };
483-
}
484-
485485
private missingRubyError(version: string) {
486486
return new Error(`Cannot find Ruby installation for version ${version}`);
487487
}

vscode/src/ruby/rubyInstaller.ts

+23
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,29 @@ export class RubyInstaller extends Chruby {
5555
);
5656
}
5757

58+
protected async runActivationScript(
59+
rubyExecutableUri: vscode.Uri,
60+
rubyVersion: RubyVersion,
61+
): Promise<{
62+
defaultGems: string;
63+
gemHome: string;
64+
yjit: boolean;
65+
version: string;
66+
}> {
67+
const activationResult = await super.runActivationScript(
68+
rubyExecutableUri,
69+
rubyVersion,
70+
);
71+
72+
activationResult.gemHome = activationResult.gemHome.replace(/\//g, "\\");
73+
activationResult.defaultGems = activationResult.defaultGems.replace(
74+
/\//g,
75+
"\\",
76+
);
77+
78+
return activationResult;
79+
}
80+
5881
// Override the `runScript` method to ensure that we do not pass any `shell` to `asyncExec`. The activation script is
5982
// only compatible with `cmd.exe`, and not Powershell, due to escaping of quotes. We need to ensure to always run the
6083
// script on `cmd.exe`.

vscode/src/test/suite/client.test.ts

+11-82
Original file line numberDiff line numberDiff line change
@@ -31,41 +31,10 @@ import { after, afterEach, before } from "mocha";
3131
import { Ruby, ManagerIdentifier } from "../../ruby";
3232
import Client from "../../client";
3333
import { WorkspaceChannel } from "../../workspaceChannel";
34-
import { RUBY_VERSION, MAJOR, MINOR } from "../rubyVersion";
34+
import { MAJOR, MINOR } from "../rubyVersion";
3535

36-
import { FAKE_TELEMETRY } from "./fakeTelemetry";
37-
38-
class FakeLogger {
39-
receivedMessages = "";
40-
41-
trace(message: string, ..._args: any[]): void {
42-
this.receivedMessages += message;
43-
}
44-
45-
debug(message: string, ..._args: any[]): void {
46-
this.receivedMessages += message;
47-
}
48-
49-
info(message: string, ..._args: any[]): void {
50-
this.receivedMessages += message;
51-
}
52-
53-
warn(message: string, ..._args: any[]): void {
54-
this.receivedMessages += message;
55-
}
56-
57-
error(error: string | Error, ..._args: any[]): void {
58-
this.receivedMessages += error.toString();
59-
}
60-
61-
append(value: string): void {
62-
this.receivedMessages += value;
63-
}
64-
65-
appendLine(value: string): void {
66-
this.receivedMessages += value;
67-
}
68-
}
36+
import { FAKE_TELEMETRY, FakeLogger } from "./fakeTelemetry";
37+
import { createRubySymlinks } from "./helpers";
6938

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

57+
let managerConfig;
58+
8859
// Ensure that we're activating the correct Ruby version on CI
8960
if (process.env.CI) {
9061
await vscode.workspace
@@ -94,54 +65,12 @@ async function launchClient(workspaceUri: vscode.Uri) {
9465
.getConfiguration("rubyLsp")
9566
.update("linters", ["rubocop_internal"], true);
9667

97-
if (os.platform() === "linux") {
98-
await vscode.workspace
99-
.getConfiguration("rubyLsp")
100-
.update(
101-
"rubyVersionManager",
102-
{ identifier: ManagerIdentifier.Chruby },
103-
true,
104-
);
105-
106-
fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
107-
fs.symlinkSync(
108-
`/opt/hostedtoolcache/Ruby/${RUBY_VERSION}/x64`,
109-
path.join(os.homedir(), ".rubies", RUBY_VERSION),
110-
);
111-
} else if (os.platform() === "darwin") {
112-
await vscode.workspace
113-
.getConfiguration("rubyLsp")
114-
.update(
115-
"rubyVersionManager",
116-
{ identifier: ManagerIdentifier.Chruby },
117-
true,
118-
);
119-
120-
fs.mkdirSync(path.join(os.homedir(), ".rubies"), { recursive: true });
121-
fs.symlinkSync(
122-
`/Users/runner/hostedtoolcache/Ruby/${RUBY_VERSION}/arm64`,
123-
path.join(os.homedir(), ".rubies", RUBY_VERSION),
124-
);
68+
createRubySymlinks();
69+
70+
if (os.platform() === "win32") {
71+
managerConfig = { identifier: ManagerIdentifier.RubyInstaller };
12572
} else {
126-
await vscode.workspace
127-
.getConfiguration("rubyLsp")
128-
.update(
129-
"rubyVersionManager",
130-
{ identifier: ManagerIdentifier.RubyInstaller },
131-
true,
132-
);
133-
134-
fs.symlinkSync(
135-
path.join(
136-
"C:",
137-
"hostedtoolcache",
138-
"windows",
139-
"Ruby",
140-
RUBY_VERSION,
141-
"x64",
142-
),
143-
path.join("C:", `Ruby${MAJOR}${MINOR}-${os.arch()}`),
144-
);
73+
managerConfig = { identifier: ManagerIdentifier.Chruby };
14574
}
14675
}
14776

@@ -151,7 +80,7 @@ async function launchClient(workspaceUri: vscode.Uri) {
15180
outputChannel,
15281
FAKE_TELEMETRY,
15382
);
154-
await ruby.activateRuby();
83+
await ruby.activateRuby(managerConfig);
15584
ruby.env.RUBY_LSP_BYPASS_TYPECHECKER = "true";
15685

15786
const virtualDocuments = new Map<string, string>();

0 commit comments

Comments
 (0)