Skip to content

Commit

Permalink
Implement ASDF as a manager object
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Apr 5, 2024
1 parent c8a18d8 commit 0c56c7a
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 43 deletions.
47 changes: 4 additions & 43 deletions vscode/src/ruby.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Rbenv } from "./ruby/rbenv";
import { Rvm } from "./ruby/rvm";
import { None } from "./ruby/none";
import { Custom } from "./ruby/custom";
import { Asdf } from "./ruby/asdf";

export enum ManagerIdentifier {
Asdf = "asdf",
Expand Down Expand Up @@ -42,7 +43,6 @@ export class Ruby implements RubyInterface {
private _error = false;
private readonly context: vscode.ExtensionContext;
private readonly customBundleGemfile?: string;
private readonly cwd: string;
private readonly outputChannel: WorkspaceChannel;

constructor(
Expand All @@ -65,10 +65,6 @@ export class Ruby implements RubyInterface {
path.join(this.workspaceFolder.uri.fsPath, customBundleGemfile),
);
}

this.cwd = this.customBundleGemfile
? path.dirname(this.customBundleGemfile)
: this.workspaceFolder.uri.fsPath;
}

get versionManager() {
Expand Down Expand Up @@ -105,7 +101,9 @@ export class Ruby implements RubyInterface {
try {
switch (this.versionManager) {
case ManagerIdentifier.Asdf:
await this.activate("asdf exec ruby");
await this.runActivation(
new Asdf(this.workspaceFolder, this.outputChannel),
);
break;
case ManagerIdentifier.Chruby:
await this.runActivation(
Expand Down Expand Up @@ -180,43 +178,6 @@ export class Ruby implements RubyInterface {
this.yjitEnabled = (yjit && major > 3) || (major === 3 && minor >= 2);
}

private async activate(ruby: string) {
let command = this.shell ? `${this.shell} -i -c '` : "";

// The Ruby activation script is intentionally written as an array that gets joined into a one liner because some
// terminals cannot handle line breaks. Do not switch this to a multiline string or that will break activation for
// those terminals
const script = [
"STDERR.printf(%{RUBY_ENV_ACTIVATE%sRUBY_ENV_ACTIVATE}, ",
"JSON.dump({ env: ENV.to_h, ruby_version: RUBY_VERSION, yjit: defined?(RubyVM::YJIT) }))",
].join("");

command += `${ruby} -rjson -e "${script}"`;

if (this.shell) {
command += "'";
}

this.outputChannel.info(
`Trying to activate Ruby environment with command: ${command} inside directory: ${this.cwd}`,
);

const result = await asyncExec(command, { cwd: this.cwd });
const rubyInfoJson = /RUBY_ENV_ACTIVATE(.*)RUBY_ENV_ACTIVATE/.exec(
result.stderr,
)![1];

const rubyInfo = JSON.parse(rubyInfoJson);

this._env = rubyInfo.env;
this.rubyVersion = rubyInfo.ruby_version;

const [major, minor, _patch] = rubyInfo.ruby_version.split(".").map(Number);
this.yjitEnabled =
(rubyInfo.yjit === "constant" && major > 3) ||
(major === 3 && minor >= 2);
}

// Fetch information related to the Ruby version. This can only be invoked after activation, so that `rubyVersion` is
// set
private fetchRubyVersionInfo() {
Expand Down
98 changes: 98 additions & 0 deletions vscode/src/ruby/asdf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* eslint-disable no-process-env */

import path from "path";
import os from "os";

import * as vscode from "vscode";

import { asyncExec } from "../common";

import { VersionManager, ActivationResult } from "./versionManager";

// A tool to manage multiple runtime versions with a single CLI tool
//
// Learn more: https://github.com/asdf-vm/asdf
export class Asdf extends VersionManager {
async activate(): Promise<ActivationResult> {
const asdfUri = await this.findAsdfInstallation();
const activationScript = [
"STDERR.print(",
"{env: ENV.to_h,yjit:!!defined?(RubyVM::YJIT),version:RUBY_VERSION,home:Gem.user_dir,default:Gem.default_dir}",
".to_json)",
].join("");

const result = await asyncExec(
`. ${asdfUri.fsPath} && asdf exec ruby -W0 -rjson -e '${activationScript}'`,
{
cwd: this.bundleUri.fsPath,
env: {
ASDF_DIR: path.dirname(asdfUri.fsPath),
},
},
);

const parsedResult = JSON.parse(result.stderr);

// The addition of GEM_HOME, GEM_PATH and putting the bin directories into the PATH happens through ASDF's shell
// hooks. Since we want to avoid spawning shells due to integration issues, we need to insert these variables
// ourselves, so that gem executables can be properly found
parsedResult.env.GEM_HOME = parsedResult.home;
parsedResult.env.GEM_PATH = `${parsedResult.home}${path.delimiter}${parsedResult.default}`;
parsedResult.env.PATH = [
path.join(parsedResult.home, "bin"),
path.join(parsedResult.default, "bin"),
parsedResult.env.PATH,
].join(path.delimiter);

return {
env: { ...process.env, ...parsedResult.env },
yjit: parsedResult.yjit,
version: parsedResult.version,
};
}

// Only public for testing. Finds the ASDF installation URI based on what's advertised in the ASDF documentation
async findAsdfInstallation(): Promise<vscode.Uri> {
// Possible ASDF installation paths as described in https://asdf-vm.com/guide/getting-started.html#_3-install-asdf.
// In order, the methods of installation are:
// 1. Git
// 2. Pacman
// 3. Homebrew M series
// 4. Homebrew Intel series
const possiblePaths = [
vscode.Uri.joinPath(vscode.Uri.file(os.homedir()), ".asdf", "asdf.sh"),
vscode.Uri.joinPath(vscode.Uri.file("/"), "opt", "asdf-vm", "asdf.sh"),
vscode.Uri.joinPath(
vscode.Uri.file("/"),
"opt",
"homebrew",
"opt",
"asdf",
"libexec",
"asdf.sh",
),
vscode.Uri.joinPath(
vscode.Uri.file("/"),
"usr",
"local",
"opt",
"asdf",
"libexec",
"asdf.sh",
),
];

for (const possiblePath of possiblePaths) {
try {
await vscode.workspace.fs.stat(possiblePath);
return possiblePath;
} catch (error: any) {
// Continue looking
}
}

throw new Error(
`Could not find ASDF installation. Searched in ${possiblePaths.join(", ")}`,
);
}
}
79 changes: 79 additions & 0 deletions vscode/src/test/suite/ruby/asdf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import assert from "assert";
import path from "path";
import os from "os";

import * as vscode from "vscode";
import sinon from "sinon";

import { Asdf } from "../../../ruby/asdf";
import { WorkspaceChannel } from "../../../workspaceChannel";
import * as common from "../../../common";

suite("Asdf", () => {
if (os.platform() === "win32") {
// eslint-disable-next-line no-console
console.log("Skipping Asdf tests on Windows");
return;
}

test("Finds Ruby based on .tool-versions", async () => {
// eslint-disable-next-line no-process-env
const workspacePath = process.env.PWD!;
const workspaceFolder = {
uri: vscode.Uri.from({ scheme: "file", path: workspacePath }),
name: path.basename(workspacePath),
index: 0,
};
const outputChannel = new WorkspaceChannel("fake", common.LOG_CHANNEL);
const asdf = new Asdf(workspaceFolder, outputChannel);

const activationScript = [
"STDERR.print(",
"{env: ENV.to_h,yjit:!!defined?(RubyVM::YJIT),version:RUBY_VERSION,home:Gem.user_dir,default:Gem.default_dir}",
".to_json)",
].join("");

const execStub = sinon.stub(common, "asyncExec").resolves({
stdout: "",
stderr: JSON.stringify({
env: { ANY: "true" },
yjit: true,
version: "3.0.0",
home: "/home/user/.gem/ruby/3.0.0",
default: "/usr/lib/ruby/gems/3.0.0",
}),
});

const findInstallationStub = sinon
.stub(asdf, "findAsdfInstallation")
.resolves(vscode.Uri.file(`${os.homedir()}/.asdf/asdf.sh`));

const { env, version, yjit } = await asdf.activate();

assert.ok(
execStub.calledOnceWithExactly(
`. ${os.homedir()}/.asdf/asdf.sh && asdf exec ruby -W0 -rjson -e '${activationScript}'`,
{
cwd: workspacePath,
env: {
ASDF_DIR: `${os.homedir()}/.asdf`,
},
},
),
);

assert.strictEqual(version, "3.0.0");
assert.strictEqual(yjit, true);
assert.strictEqual(env.GEM_HOME, "/home/user/.gem/ruby/3.0.0");
assert.strictEqual(
env.GEM_PATH,
"/home/user/.gem/ruby/3.0.0:/usr/lib/ruby/gems/3.0.0",
);
assert.ok(env.PATH!.includes("/home/user/.gem/ruby/3.0.0/bin"));
assert.ok(env.PATH!.includes("/usr/lib/ruby/gems/3.0.0/bin"));
assert.strictEqual(env.ANY, "true");

execStub.restore();
findInstallationStub.restore();
});
});

0 comments on commit 0c56c7a

Please sign in to comment.