|
| 1 | +import * as vscode from "vscode"; |
| 2 | +import * as os from "os"; |
| 3 | +import { Config } from "./config"; |
| 4 | +import { log, isValidExecutable } from "./util"; |
| 5 | +import { PersistentState } from "./persistent_state"; |
| 6 | +import { exec } from "child_process"; |
| 7 | + |
| 8 | +export async function bootstrap( |
| 9 | + context: vscode.ExtensionContext, |
| 10 | + config: Config, |
| 11 | + state: PersistentState |
| 12 | +): Promise<string> { |
| 13 | + const path = await getServer(context, config, state); |
| 14 | + if (!path) { |
| 15 | + throw new Error( |
| 16 | + "Rust Analyzer Language Server is not available. " + |
| 17 | + "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)." |
| 18 | + ); |
| 19 | + } |
| 20 | + |
| 21 | + log.info("Using server binary at", path); |
| 22 | + |
| 23 | + if (!isValidExecutable(path)) { |
| 24 | + if (config.serverPath) { |
| 25 | + throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\ |
| 26 | + Consider removing this config or making a valid server binary available at that path.`); |
| 27 | + } else { |
| 28 | + throw new Error(`Failed to execute ${path} --version`); |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + return path; |
| 33 | +} |
| 34 | + |
| 35 | +async function patchelf(dest: vscode.Uri): Promise<void> { |
| 36 | + await vscode.window.withProgress( |
| 37 | + { |
| 38 | + location: vscode.ProgressLocation.Notification, |
| 39 | + title: "Patching rust-analyzer for NixOS", |
| 40 | + }, |
| 41 | + async (progress, _) => { |
| 42 | + const expression = ` |
| 43 | + {srcStr, pkgs ? import <nixpkgs> {}}: |
| 44 | + pkgs.stdenv.mkDerivation { |
| 45 | + name = "rust-analyzer"; |
| 46 | + src = /. + srcStr; |
| 47 | + phases = [ "installPhase" "fixupPhase" ]; |
| 48 | + installPhase = "cp $src $out"; |
| 49 | + fixupPhase = '' |
| 50 | + chmod 755 $out |
| 51 | + patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out |
| 52 | + ''; |
| 53 | + } |
| 54 | + `; |
| 55 | + const origFile = vscode.Uri.file(dest.fsPath + "-orig"); |
| 56 | + await vscode.workspace.fs.rename(dest, origFile, { overwrite: true }); |
| 57 | + try { |
| 58 | + progress.report({ message: "Patching executable", increment: 20 }); |
| 59 | + await new Promise((resolve, reject) => { |
| 60 | + const handle = exec( |
| 61 | + `nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`, |
| 62 | + (err, stdout, stderr) => { |
| 63 | + if (err != null) { |
| 64 | + reject(Error(stderr)); |
| 65 | + } else { |
| 66 | + resolve(stdout); |
| 67 | + } |
| 68 | + } |
| 69 | + ); |
| 70 | + handle.stdin?.write(expression); |
| 71 | + handle.stdin?.end(); |
| 72 | + }); |
| 73 | + } finally { |
| 74 | + await vscode.workspace.fs.delete(origFile); |
| 75 | + } |
| 76 | + } |
| 77 | + ); |
| 78 | +} |
| 79 | + |
| 80 | +async function getServer( |
| 81 | + context: vscode.ExtensionContext, |
| 82 | + config: Config, |
| 83 | + state: PersistentState |
| 84 | +): Promise<string | undefined> { |
| 85 | + const explicitPath = serverPath(config); |
| 86 | + if (explicitPath) { |
| 87 | + if (explicitPath.startsWith("~/")) { |
| 88 | + return os.homedir() + explicitPath.slice("~".length); |
| 89 | + } |
| 90 | + return explicitPath; |
| 91 | + } |
| 92 | + if (config.package.releaseTag === null) return "rust-analyzer"; |
| 93 | + |
| 94 | + const ext = process.platform === "win32" ? ".exe" : ""; |
| 95 | + const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`); |
| 96 | + const bundledExists = await vscode.workspace.fs.stat(bundled).then( |
| 97 | + () => true, |
| 98 | + () => false |
| 99 | + ); |
| 100 | + if (bundledExists) { |
| 101 | + let server = bundled; |
| 102 | + if (await isNixOs()) { |
| 103 | + await vscode.workspace.fs.createDirectory(config.globalStorageUri).then(); |
| 104 | + const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`); |
| 105 | + let exists = await vscode.workspace.fs.stat(dest).then( |
| 106 | + () => true, |
| 107 | + () => false |
| 108 | + ); |
| 109 | + if (exists && config.package.version !== state.serverVersion) { |
| 110 | + await vscode.workspace.fs.delete(dest); |
| 111 | + exists = false; |
| 112 | + } |
| 113 | + if (!exists) { |
| 114 | + await vscode.workspace.fs.copy(bundled, dest); |
| 115 | + await patchelf(dest); |
| 116 | + } |
| 117 | + server = dest; |
| 118 | + } |
| 119 | + await state.updateServerVersion(config.package.version); |
| 120 | + return server.fsPath; |
| 121 | + } |
| 122 | + |
| 123 | + await state.updateServerVersion(undefined); |
| 124 | + await vscode.window.showErrorMessage( |
| 125 | + "Unfortunately we don't ship binaries for your platform yet. " + |
| 126 | + "You need to manually clone the rust-analyzer repository and " + |
| 127 | + "run `cargo xtask install --server` to build the language server from sources. " + |
| 128 | + "If you feel that your platform should be supported, please create an issue " + |
| 129 | + "about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " + |
| 130 | + "will consider it." |
| 131 | + ); |
| 132 | + return undefined; |
| 133 | +} |
| 134 | +function serverPath(config: Config): string | null { |
| 135 | + return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath; |
| 136 | +} |
| 137 | + |
| 138 | +async function isNixOs(): Promise<boolean> { |
| 139 | + try { |
| 140 | + const contents = ( |
| 141 | + await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release")) |
| 142 | + ).toString(); |
| 143 | + const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux"; |
| 144 | + return idString.indexOf("nixos") !== -1; |
| 145 | + } catch { |
| 146 | + return false; |
| 147 | + } |
| 148 | +} |
0 commit comments