Skip to content

Commit

Permalink
Allow omitting patch in .ruby-version
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed May 2, 2024
1 parent 94194d8 commit 07bb07a
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 28 deletions.
100 changes: 72 additions & 28 deletions vscode/src/ruby/chruby.ts
Expand Up @@ -43,7 +43,7 @@ export class Chruby extends VersionManager {
async activate(): Promise<ActivationResult> {
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(
Expand All @@ -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<vscode.Uri> {
if (/\d+\.\d+\.\d+/.exec(rubyVersion.version)) {
return this.findRubyUriForCompleteVersion(rubyVersion);
}

return this.findRubyUriWithOmittedPatch(rubyVersion);
}

private async findRubyUriWithOmittedPatch(
rubyVersion: RubyVersion,
): Promise<vscode.Uri> {
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<vscode.Uri> {
// 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
Expand Down Expand Up @@ -99,46 +138,51 @@ export class Chruby extends VersionManager {
private async discoverRubyVersion(): Promise<RubyVersion> {
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 =
/((?<engine>[A-Za-z]+)-)?(?<version>\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 =
/((?<engine>[A-Za-z]+)-)?(?<version>\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
Expand All @@ -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(";");

Expand Down
35 changes: 35 additions & 0 deletions vscode/src/test/suite/ruby/chruby.test.ts
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
});
});
});

0 comments on commit 07bb07a

Please sign in to comment.