From 07bb07abe60381b6d2ab807cc6eb2b6eed1644fd Mon Sep 17 00:00:00 2001 From: Vinicius Stock Date: Thu, 2 May 2024 13:39:06 -0400 Subject: [PATCH] Allow omitting patch in .ruby-version --- vscode/src/ruby/chruby.ts | 100 ++++++++++++++++------ vscode/src/test/suite/ruby/chruby.test.ts | 35 ++++++++ 2 files changed, 107 insertions(+), 28 deletions(-) diff --git a/vscode/src/ruby/chruby.ts b/vscode/src/ruby/chruby.ts index 99ab5e1fd..55763ee3f 100644 --- a/vscode/src/ruby/chruby.ts +++ b/vscode/src/ruby/chruby.ts @@ -43,7 +43,7 @@ export class Chruby extends VersionManager { async activate(): Promise { const versionInfo = await this.discoverRubyVersion(); const rubyUri = await this.findRubyUri(versionInfo); - const { defaultGems, gemHome, yjit } = + const { defaultGems, gemHome, yjit, version } = await this.runActivationScript(rubyUri); this.outputChannel.info( @@ -62,12 +62,51 @@ export class Chruby extends VersionManager { return { env: { ...process.env, ...rubyEnv }, yjit, - version: versionInfo.version, + version, }; } // Returns the full URI to the Ruby executable protected async findRubyUri(rubyVersion: RubyVersion): Promise { + if (/\d+\.\d+\.\d+/.exec(rubyVersion.version)) { + return this.findRubyUriForCompleteVersion(rubyVersion); + } + + return this.findRubyUriWithOmittedPatch(rubyVersion); + } + + private async findRubyUriWithOmittedPatch( + rubyVersion: RubyVersion, + ): Promise { + const possibleVersionNames = rubyVersion.engine + ? [`${rubyVersion.engine}-${rubyVersion.version}`, rubyVersion.version] + : [rubyVersion.version, `ruby-${rubyVersion.version}`]; + + for (const uri of this.rubyInstallationUris) { + const directories = (await vscode.workspace.fs.readDirectory(uri)).sort( + (left, right) => right[0].localeCompare(left[0]), + ); + + for (const versionName of possibleVersionNames) { + const targetDirectory = directories.find(([name]) => + name.startsWith(versionName), + ); + + if (targetDirectory) { + return vscode.Uri.joinPath(uri, targetDirectory[0], "bin", "ruby"); + } + } + } + + throw new Error( + `Cannot find installation directory for Ruby version ${possibleVersionNames.join(" or ")}. + Searched in ${this.rubyInstallationUris.map((uri) => uri.fsPath).join(", ")}`, + ); + } + + private async findRubyUriForCompleteVersion( + rubyVersion: RubyVersion, + ): Promise { // If an engine was specified in the .ruby-version file, we favor looking for that first and also try just the // version number. If no engine was specified, we first try just the version number and then we try using `ruby` as // the default engine @@ -99,46 +138,51 @@ export class Chruby extends VersionManager { private async discoverRubyVersion(): Promise { let uri = this.bundleUri; const root = path.parse(uri.fsPath).root; + let version: string; + let rubyVersionUri: vscode.Uri; while (uri.fsPath !== root) { try { - const rubyVersionUri = vscode.Uri.joinPath(uri, ".ruby-version"); + rubyVersionUri = vscode.Uri.joinPath(uri, ".ruby-version"); const content = await vscode.workspace.fs.readFile(rubyVersionUri); - const version = content.toString().trim(); - - if (version === "") { - throw new Error(`Ruby version file ${rubyVersionUri} is empty`); - } - - const match = - /((?[A-Za-z]+)-)?(?\d\.\d\.\d(-[A-Za-z0-9]+)?)/.exec( - version, - ); - - if (!match?.groups) { - throw new Error( - `Ruby version file ${rubyVersionUri} contains invalid format. Expected (engine-)?version, got ${version}`, - ); - } - - this.outputChannel.info( - `Discovered Ruby version ${version} from ${rubyVersionUri.toString()}`, - ); - return { engine: match.groups.engine, version: match.groups.version }; + version = content.toString().trim(); } catch (error: any) { // If the file doesn't exist, continue going up the directory tree uri = vscode.Uri.file(path.dirname(uri.fsPath)); continue; } + + if (version === "") { + throw new Error(`Ruby version file ${rubyVersionUri} is empty`); + } + + const match = + /((?[A-Za-z]+)-)?(?\d+\.\d+(\.\d+)?(-[A-Za-z0-9]+)?)/.exec( + version, + ); + + if (!match?.groups) { + throw new Error( + `Ruby version file ${rubyVersionUri} contains invalid format. Expected (engine-)?version, got ${version}`, + ); + } + + this.outputChannel.info( + `Discovered Ruby version ${version} from ${rubyVersionUri.toString()}`, + ); + return { engine: match.groups.engine, version: match.groups.version }; } throw new Error("No .ruby-version file was found"); } // Run the activation script using the Ruby installation we found so that we can discover gem paths - private async runActivationScript( - rubyExecutableUri: vscode.Uri, - ): Promise<{ defaultGems: string; gemHome: string; yjit: boolean }> { + private async runActivationScript(rubyExecutableUri: vscode.Uri): 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 @@ -155,7 +199,7 @@ export class Chruby extends VersionManager { "end", "newer_gem_home = File.join(File.dirname(user_dir), RUBY_VERSION)", "gems = (Dir.exist?(newer_gem_home) ? newer_gem_home : user_dir)", - "data = { defaultGems: Gem.default_dir, gemHome: gems, yjit: !!defined?(RubyVM::YJIT) }", + "data = { defaultGems: Gem.default_dir, gemHome: gems, yjit: !!defined?(RubyVM::YJIT), version: RUBY_VERSION }", "STDERR.print(JSON.dump(data))", ].join(";"); diff --git a/vscode/src/test/suite/ruby/chruby.test.ts b/vscode/src/test/suite/ruby/chruby.test.ts index 30bc0ec30..80d7df504 100644 --- a/vscode/src/test/suite/ruby/chruby.test.ts +++ b/vscode/src/test/suite/ruby/chruby.test.ts @@ -135,6 +135,10 @@ suite("Chruby", () => { assert.match(env.GEM_PATH!, new RegExp(`lib/ruby/gems/${VERSION_REGEX}`)); assert.strictEqual(version, RUBY_VERSION); assert.notStrictEqual(yjit, undefined); + fs.rmSync(path.join(rootPath, "opt", "rubies", `${RUBY_VERSION}-custom`), { + recursive: true, + force: true, + }); }); test("Considers Ruby as the default engine if missing", async () => { @@ -181,4 +185,35 @@ suite("Chruby", () => { assert.strictEqual(version, RUBY_VERSION); assert.notStrictEqual(yjit, undefined); }); + + test("Finds Ruby when .ruby-version omits patch", async () => { + fs.mkdirSync( + path.join(rootPath, "opt", "rubies", `${major}.${minor}.0`, "bin"), + { + recursive: true, + }, + ); + + fs.writeFileSync( + path.join(workspacePath, ".ruby-version"), + `${major}.${minor}`, + ); + + const chruby = new Chruby(workspaceFolder, outputChannel); + chruby.rubyInstallationUris = [ + vscode.Uri.file(path.join(rootPath, "opt", "rubies")), + ]; + + const { env, version, yjit } = await chruby.activate(); + + assert.match(env.GEM_PATH!, new RegExp(`ruby/${VERSION_REGEX}`)); + assert.match(env.GEM_PATH!, new RegExp(`lib/ruby/gems/${VERSION_REGEX}`)); + assert.strictEqual(version, RUBY_VERSION); + assert.notStrictEqual(yjit, undefined); + + fs.rmSync(path.join(rootPath, "opt", "rubies", `${major}.${minor}.0`), { + recursive: true, + force: true, + }); + }); });