blob: a9001934ebf09596c253bea819e4663e8874986f [file] [log] [blame]
/*---------------------------------------------------------
* Copyright 2020 The Go Authors. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
'use strict';
import cp = require('child_process');
import fs = require('fs');
import os = require('os');
import path = require('path');
import { promisify } from 'util';
import vscode = require('vscode');
import WebRequest = require('web-request');
import { toolInstallationEnvironment } from './goEnv';
import { outputChannel } from './goStatus';
import { getFromWorkspaceState, updateWorkspaceState } from './stateUtils';
import { getBinPath, getGoVersion, getTempFilePath, GoVersion, rmdirRecursive } from './util';
import { correctBinname, getBinPathFromEnvVar, getCurrentGoRoot, pathExists } from './utils/goPath';
export class GoEnvironmentOption {
public static fromQuickPickItem({ description, label }: vscode.QuickPickItem): GoEnvironmentOption {
return new GoEnvironmentOption(description, label);
}
constructor(public binpath: string, public label: string) {}
public toQuickPickItem(): vscode.QuickPickItem {
return {
label: this.label,
description: this.binpath,
};
}
}
// statusbar item for switching the Go environment
let goEnvStatusbarItem: vscode.StatusBarItem;
let terminalCreationListener: vscode.Disposable;
/**
* Initialize the status bar item with current Go binary
*/
export async function initGoStatusBar() {
if (!goEnvStatusbarItem) {
goEnvStatusbarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
}
// set Go version and command
const version = await getGoVersion();
const goOption = new GoEnvironmentOption(version.binaryPath, formatGoVersion(version));
hideGoStatusBar();
goEnvStatusbarItem.text = goOption.label;
goEnvStatusbarItem.command = 'go.environment.choose';
showGoStatusBar();
}
/**
* disable the Go environment status bar item
*/
export function disposeGoStatusBar() {
if (!!goEnvStatusbarItem) {
goEnvStatusbarItem.dispose();
}
if (!!terminalCreationListener) {
terminalCreationListener.dispose();
}
}
/**
* Show the Go Environment statusbar item on the statusbar
*/
export function showGoStatusBar() {
if (!!goEnvStatusbarItem) {
goEnvStatusbarItem.show();
}
}
/**
* Hide the Go Environment statusbar item from the statusbar
*/
export function hideGoStatusBar() {
if (!!goEnvStatusbarItem) {
goEnvStatusbarItem.hide();
}
}
let environmentVariableCollection: vscode.EnvironmentVariableCollection;
export function setEnvironmentVariableCollection(env: vscode.EnvironmentVariableCollection) {
environmentVariableCollection = env;
}
/**
* Present a command palette menu to the user to select their go binary
*/
export async function chooseGoEnvironment() {
if (!goEnvStatusbarItem) {
return;
}
// if there is no workspace, show GOROOT with message
if (!vscode.workspace.name) {
vscode.window.showInformationMessage(`GOROOT: ${getCurrentGoRoot()}. Switching Go version is not yet supported in single-file mode.`);
return;
}
// fetch default go and uninstalled go versions
let defaultOption: GoEnvironmentOption;
let uninstalledOptions: GoEnvironmentOption[];
let goSDKOptions: GoEnvironmentOption[];
try {
[defaultOption, uninstalledOptions, goSDKOptions] = await Promise.all([
getDefaultGoOption(),
fetchDownloadableGoVersions(),
getSDKGoOptions()
]);
} catch (e) {
vscode.window.showErrorMessage(e.message);
return;
}
// create quick pick items
const uninstalledQuickPicks = uninstalledOptions.map((op) => op.toQuickPickItem());
const defaultQuickPick = defaultOption.toQuickPickItem();
const goSDKQuickPicks = goSDKOptions.map((op) => op.toQuickPickItem());
// dedup options by eliminating duplicate paths (description)
const clearOption: vscode.QuickPickItem = { label: 'Clear selection' };
const options = [clearOption, defaultQuickPick, ...goSDKQuickPicks, ...uninstalledQuickPicks]
.reduce((opts, nextOption) => {
if (opts.find((op) => op.description === nextOption.description || op.label === nextOption.label)) {
return opts;
}
return [...opts, nextOption];
}, [] as vscode.QuickPickItem[]);
// get user's selection, return if none was made
const selection = await vscode.window.showQuickPick<vscode.QuickPickItem>(options);
if (!selection) {
return;
}
// update currently selected go
try {
const changed = await setSelectedGo(GoEnvironmentOption.fromQuickPickItem(selection));
if (changed) {
vscode.window.showInformationMessage(`Switched to ${selection.label}`);
}
} catch (e) {
vscode.window.showErrorMessage(e.message);
}
}
/**
* update the selected go path and label in the workspace state
*/
export async function setSelectedGo(goOption: GoEnvironmentOption, promptReload = true): Promise<boolean> {
if (!goOption) {
return false;
}
// if the selected go version is not installed, install it
if (goOption.binpath?.startsWith('go get')) {
// start a loading indicator
await downloadGo(goOption);
} else if (goOption.label === 'Clear selection') {
if (!getSelectedGo()) {
return false; // do nothing.
}
await updateWorkspaceState('selectedGo', undefined);
// TODO: goEnvStatusbarItem?
} else {
// check that the given binary is not already at the beginning of the PATH
const go = await getGoVersion();
if (!!go && (go.binaryPath === goOption.binpath || 'Go ' + go.format() === goOption.label)) {
return false;
}
await updateWorkspaceState('selectedGo', goOption);
}
// prompt the user to reload the window.
// promptReload defaults to true and should only be false for tests.
if (promptReload) {
const choice = await vscode.window.showInformationMessage('Please reload the window to finish applying Go version changes.', 'Reload Window');
if (choice === 'Reload Window') {
await vscode.commands.executeCommand('workbench.action.reloadWindow');
}
}
goEnvStatusbarItem.text = 'Go: reload required';
goEnvStatusbarItem.command = 'workbench.action.reloadWindow';
return true;
}
// downloadGo downloads the specified go version available in dl.golang.org.
async function downloadGo(goOption: GoEnvironmentOption) {
const execFile = promisify(cp.execFile);
await vscode.window.withProgress({
title: `Downloading ${goOption.label}`,
location: vscode.ProgressLocation.Notification,
}, async () => {
outputChannel.show();
outputChannel.clear();
outputChannel.appendLine('Finding Go executable for downloading');
const goExecutable = getBinPath('go');
if (!goExecutable) {
outputChannel.appendLine('Could not find Go executable.');
throw new Error('Could not find Go tool.');
}
// TODO(bcloud) dedup repeated logic below which comes from
// https://github.com/golang/vscode-go/blob/bc23fa854192d04200c8e4f74dca18d2c3021b46/src/goInstallTools.ts#L184
// Install tools in a temporary directory, to avoid altering go.mod files.
const mkdtemp = promisify(fs.mkdtemp);
const toolsTmpDir = await mkdtemp(getTempFilePath('go-tools-'));
let tmpGoModFile: string;
// Write a temporary go.mod file to avoid version conflicts.
tmpGoModFile = path.join(toolsTmpDir, 'go.mod');
const writeFile = promisify(fs.writeFile);
await writeFile(tmpGoModFile, 'module tools');
// use the current go executable to download the new version
const env = {
...toolInstallationEnvironment(),
GO111MODULE: 'on',
};
const [, ...args] = goOption.binpath.split(' ');
outputChannel.appendLine(`Running ${goExecutable} ${args.join(' ')}`);
try {
await execFile(goExecutable, args, {
env,
cwd: toolsTmpDir,
});
} catch (getErr) {
outputChannel.appendLine(`Error finding Go: ${getErr}`);
throw new Error('Could not find Go version.');
}
// run `goX.X download`
const newExecutableName = args[1].split('/')[2];
const goXExecutable = getBinPath(newExecutableName);
outputChannel.appendLine(`Running: ${goXExecutable} download`);
try {
await execFile(goXExecutable, ['download'], { env });
} catch (downloadErr) {
outputChannel.appendLine(`Error finishing installation: ${downloadErr}`);
throw new Error('Could not download Go version.');
}
outputChannel.appendLine('Finding newly downloaded Go');
const sdkPath = path.join(os.homedir(), 'sdk');
if (!await pathExists(sdkPath)) {
outputChannel.appendLine(`SDK path does not exist: ${sdkPath}`);
throw new Error(`SDK path does not exist: ${sdkPath}`);
}
const readdir = promisify(fs.readdir);
const subdirs = await readdir(sdkPath);
const dir = subdirs.find((subdir) => subdir === newExecutableName);
if (!dir) {
outputChannel.appendLine('Could not find newly downloaded Go');
throw new Error('Could not install Go version.');
}
const binpath = path.join(sdkPath, dir, 'bin', correctBinname('go'));
const newOption = new GoEnvironmentOption(binpath, goOption.label);
await updateWorkspaceState('selectedGo', newOption);
// remove tmp directories
outputChannel.appendLine('Cleaning up...');
rmdirRecursive(toolsTmpDir);
outputChannel.appendLine('Success!');
});
}
// PATH value cached before addGoRuntimeBaseToPath modified.
let defaultPathEnv = '';
// addGoRuntimeBaseToPATH adds the given path to the front of the PATH environment variable.
// It removes duplicates.
// TODO: can we avoid changing PATH but utilize toolExecutionEnv?
export function addGoRuntimeBaseToPATH(newGoRuntimeBase: string) {
if (!newGoRuntimeBase) {
return;
}
let pathEnvVar: string;
if (process.env.hasOwnProperty('PATH')) {
pathEnvVar = 'PATH';
} else if (process.platform === 'win32' && process.env.hasOwnProperty('Path')) {
pathEnvVar = 'Path';
} else {
return;
}
if (!defaultPathEnv) { // cache the default value
defaultPathEnv = <string>process.env[pathEnvVar];
}
// calling this multiple times will override the previous value.
// environmentVariableCollection.clear();
if (process.platform !== 'darwin') {
environmentVariableCollection?.prepend(pathEnvVar, newGoRuntimeBase + path.delimiter);
} else if (!terminalCreationListener) { // process.platform === 'darwin'
// We don't use EnvironmentVariableCollection on mac
// because this gets confusing for users. Instead we send the
// shell command to change the PATH env var,
// following the suggestion to workaround described in
// https://github.com/microsoft/vscode/issues/99878#issuecomment-642808852
const terminalShellArgs = <string[]>(
vscode.workspace.getConfiguration('terminal.integrated.shellArgs').get('osx') || []);
// User explicitly chose to run the login shell. So, don't mess with their config.
if (!terminalShellArgs.includes('-l') && !terminalShellArgs.includes('--login')) {
for (const term of vscode.window.terminals) {
updateIntegratedTerminal(term);
}
terminalCreationListener = vscode.window.onDidOpenTerminal(updateIntegratedTerminal);
}
}
let pathVars = defaultPathEnv.split(path.delimiter);
pathVars = pathVars.filter((p) => p !== newGoRuntimeBase);
pathVars.unshift(newGoRuntimeBase);
process.env[pathEnvVar] = pathVars.join(path.delimiter);
}
/**
* update the PATH variable in the given terminal to default to the currently selected Go
*/
export async function updateIntegratedTerminal(terminal: vscode.Terminal): Promise<void> {
if (!terminal) { return; }
const gorootBin = path.join(getCurrentGoRoot(), 'bin');
const defaultGoRuntimeBin = path.dirname(getBinPathFromEnvVar('go', defaultPathEnv, false));
if (gorootBin === defaultGoRuntimeBin) {
return;
}
// append the goroot to the beginning of the PATH so it takes precedence
// TODO: add support for more terminal names
// this assumes all non-windows shells are bash-like.
if (terminal.name.toLowerCase() === 'cmd') {
terminal.sendText(`set PATH=${gorootBin};%Path%`, true);
terminal.sendText('cls');
} else if (['powershell', 'pwsh'].includes(terminal.name.toLowerCase())) {
terminal.sendText(`$env:Path="${gorootBin};$env:Path"`, true);
terminal.sendText('clear');
} else if (terminal.name.toLowerCase() === 'fish') {
terminal.sendText(`set -gx PATH ${gorootBin} $PATH`);
terminal.sendText('clear');
} else if (['bash', 'sh', 'zsh', 'ksh'].includes(terminal.name.toLowerCase())) {
terminal.sendText(`export PATH=${gorootBin}:$PATH`, true);
terminal.sendText('clear');
}
}
/**
* retreive the current selected Go from the workspace state
*/
export function getSelectedGo(): GoEnvironmentOption {
return getFromWorkspaceState('selectedGo');
}
/**
* return reference to the statusbar item
*/
export function getGoEnvironmentStatusbarItem(): vscode.StatusBarItem {
return goEnvStatusbarItem;
}
export function formatGoVersion(version?: GoVersion): string {
if (!version || !version.isValid()) {
return `Go (unknown)`;
}
const versionStr = version.format(true);
const versionWords = versionStr.split(' ');
if (versionWords.length > 1 && versionWords[0] === 'devel') {
// Go devel +hash
return `Go ${versionWords[1]}`;
} else {
return `Go ${versionWords[0]}`;
}
}
async function getSDKGoOptions(): Promise<GoEnvironmentOption[]> {
// get list of Go versions
const sdkPath = path.join(os.homedir(), 'sdk');
if (!await pathExists(sdkPath)) {
return [];
}
const readdir = promisify(fs.readdir);
const subdirs = await readdir(sdkPath);
// the dir happens to be the version, which will be used as the label
// the path is assembled and used as the description
return subdirs.map((dir: string) =>
new GoEnvironmentOption(
path.join(sdkPath, dir, 'bin', correctBinname('go')),
dir.replace('go', 'Go '),
)
);
}
export async function getDefaultGoOption(): Promise<GoEnvironmentOption> {
// make goroot default to go.goroot
const goroot = getCurrentGoRoot();
if (!goroot) {
throw new Error('No Go command could be found.');
}
// set Go version and command
const version = await getGoVersion();
return new GoEnvironmentOption(
path.join(goroot, 'bin', correctBinname('go')),
formatGoVersion(version),
);
}
/**
* make a web request to get versions of Go
*/
interface GoVersionWebResult {
version: string;
stable: boolean;
files: {
filename: string;
os: string;
arch: string;
version: string;
sha256: string;
size: number;
kind: string;
}[];
}
async function fetchDownloadableGoVersions(): Promise<GoEnvironmentOption[]> {
// fetch information about what Go versions are available to install
const webResults = await WebRequest.json<GoVersionWebResult[]>('https://golang.org/dl/?mode=json');
if (!webResults) {
return [];
}
// turn the web result into GoEnvironmentOption model
return webResults.reduce((opts, result: GoVersionWebResult) => {
// TODO: allow downloading from different sites
const dlPath = `go get golang.org/dl/${result.version}`;
const label = result.version.replace('go', 'Go ');
return [...opts, new GoEnvironmentOption(dlPath, label)];
}, []);
}