blob: 195ee4ccfc15eecaac5783714523a3201ce26fd6 [file] [log] [blame]
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------*/
'use strict';
import cp = require('child_process');
import fs = require('fs');
import path = require('path');
import { SemVer } from 'semver';
import vscode = require('vscode');
import { getLanguageServerToolPath } from './goLanguageServer';
import { restartLanguageServer } from './goMain';
import { envPath, getToolFromToolPath } from './goPath';
import { hideGoStatus, outputChannel, showGoStatus } from './goStatus';
import {
containsString,
containsTool,
disableModulesForWildcard,
getConfiguredTools,
getImportPath,
getImportPathWithVersion,
getTool,
hasModSuffix,
isGocode,
Tool,
ToolAtVersion
} from './goTools';
import {
getBinPath,
getCurrentGoPath,
getGoConfig,
getGoVersion,
getTempFilePath,
getToolsEnvVars,
getToolsGopath,
GoVersion,
resolvePath
} from './util';
// declinedUpdates tracks the tools that the user has declined to update.
const declinedUpdates: Tool[] = [];
// declinedUpdates tracks the tools that the user has declined to install.
const declinedInstalls: Tool[] = [];
export async function installAllTools(updateExistingToolsOnly: boolean = false) {
const goVersion = await getGoVersion();
let allTools = getConfiguredTools(goVersion);
// exclude tools replaced by alternateTools.
const alternateTools: { [key: string]: string } = getGoConfig().get('alternateTools');
allTools = allTools.filter((tool) => {
return !alternateTools[tool.name];
});
// Update existing tools by finding all tools the user has already installed.
if (updateExistingToolsOnly) {
installTools(
allTools.filter((tool) => {
const toolPath = getBinPath(tool.name);
return toolPath && path.isAbsolute(toolPath);
}),
goVersion
);
return;
}
// Otherwise, allow the user to select which tools to install or update.
vscode.window
.showQuickPick(
allTools.map((x) => {
const item: vscode.QuickPickItem = {
label: x.name,
description: x.description
};
return item;
}),
{
canPickMany: true,
placeHolder: 'Select the tools to install/update.'
}
)
.then((selectedTools) => {
if (!selectedTools) {
return;
}
installTools(
selectedTools.map((x) => getTool(x.label)),
goVersion
);
});
}
/**
* Installs given array of missing tools. If no input is given, the all tools are installed
*
* @param missing array of tool names and optionally, their versions to be installed.
* If a tool's version is not specified, it will install the latest.
* @param goVersion version of Go that affects how to install the tool. (e.g. modules vs legacy GOPATH mode)
*/
export function installTools(missing: ToolAtVersion[], goVersion: GoVersion): Promise<void> {
const goRuntimePath = getBinPath('go');
if (!goRuntimePath) {
vscode.window.showErrorMessage(
`Failed to run "go get" to install the packages as the "go" binary cannot be found in either GOROOT(${process.env['GOROOT']}) or PATH(${envPath})`
);
return;
}
if (!missing) {
return;
}
// http.proxy setting takes precedence over environment variables
const httpProxy = vscode.workspace.getConfiguration('http', null).get('proxy');
const envForTools = Object.assign({}, process.env, getToolsEnvVars());
if (httpProxy) {
envForTools['http_proxy'] = httpProxy;
envForTools['HTTP_PROXY'] = httpProxy;
envForTools['https_proxy'] = httpProxy;
envForTools['HTTPS_PROXY'] = httpProxy;
}
outputChannel.show();
outputChannel.clear();
// If the go.toolsGopath is set, use its value as the GOPATH for the "go get" child process.
// Else use the Current Gopath
let toolsGopath = getToolsGopath();
if (toolsGopath) {
// User has explicitly chosen to use toolsGopath, so ignore GOBIN
envForTools['GOBIN'] = '';
outputChannel.appendLine(`Using the value ${toolsGopath} from the go.toolsGopath setting.`);
} else {
toolsGopath = getCurrentGoPath();
outputChannel.appendLine(`go.toolsGopath setting is not set. Using GOPATH ${toolsGopath}`);
}
if (toolsGopath) {
const paths = toolsGopath.split(path.delimiter);
toolsGopath = paths[0];
envForTools['GOPATH'] = toolsGopath;
} else {
const msg = 'Cannot install Go tools. Set either go.gopath or go.toolsGopath in settings.';
vscode.window.showInformationMessage(msg, 'Open User Settings', 'Open Workspace Settings').then((selected) => {
switch (selected) {
case 'Open User Settings':
vscode.commands.executeCommand('workbench.action.openGlobalSettings');
break;
case 'Open Workspace Settings':
vscode.commands.executeCommand('workbench.action.openWorkspaceSettings');
break;
}
});
return;
}
let installingMsg = `Installing ${missing.length} ${missing.length > 1 ? 'tools' : 'tool'} at `;
if (envForTools['GOBIN']) {
installingMsg += `the configured GOBIN: ${envForTools['GOBIN']}`;
} else {
installingMsg += toolsGopath + path.sep + 'bin';
}
// If the user is on Go >= 1.11, tools should be installed with modules enabled.
// This ensures that users get the latest tagged version, rather than master,
// which may be unstable.
let modulesOff = false;
if (goVersion.lt('1.11')) {
modulesOff = true;
} else {
installingMsg += ' in module mode.';
}
outputChannel.appendLine(installingMsg);
missing.forEach((missingTool) => {
let toolName = missingTool.name;
if (missingTool.version) {
toolName += '@' + missingTool.version;
}
outputChannel.appendLine(' ' + toolName);
});
outputChannel.appendLine(''); // Blank line for spacing.
// Install tools in a temporary directory, to avoid altering go.mod files.
const toolsTmpDir = fs.mkdtempSync(getTempFilePath('go-tools-'));
return missing
.reduce((res: Promise<string[]>, tool: ToolAtVersion) => {
return res.then(
(sofar) =>
new Promise<string[]>(async (resolve, reject) => {
// Disable modules for tools which are installed with the "..." wildcard.
// TODO: ... will be supported in Go 1.13, so enable these tools to use modules then.
const modulesOffForTool = modulesOff || disableModulesForWildcard(tool, goVersion);
let tmpGoModFile: string;
if (modulesOffForTool) {
envForTools['GO111MODULE'] = 'off';
} else {
envForTools['GO111MODULE'] = 'on';
// Write a temporary go.mod file to avoid version conflicts.
tmpGoModFile = path.join(toolsTmpDir, 'go.mod');
fs.writeFileSync(tmpGoModFile, 'module tools');
}
let importPath: string;
if (modulesOffForTool) {
importPath = getImportPath(tool, goVersion);
} else {
importPath = getImportPathWithVersion(tool, tool.version, goVersion);
}
const callback = (err: Error, stdout: string, stderr: string) => {
// Make sure to delete the temporary go.mod file, if it exists.
if (tmpGoModFile && fs.existsSync(tmpGoModFile)) {
fs.unlinkSync(tmpGoModFile);
}
if (err) {
outputChannel.appendLine('Installing ' + importPath + ' FAILED');
const failureReason = tool.name + ';;' + err + stdout.toString() + stderr.toString();
resolve([...sofar, failureReason]);
} else {
outputChannel.appendLine('Installing ' + importPath + ' SUCCEEDED');
resolve([...sofar, null]);
}
};
// Perform any on-close actions before reinstalling the tool.
if (tool.close) {
const errMsg = await tool.close();
if (errMsg) {
outputChannel.appendLine(errMsg);
resolve([...sofar, null]);
return;
}
}
const args = ['get', '-v'];
// Only get tools at master if we are not using modules.
if (modulesOffForTool) {
args.push('-u');
}
// Tools with a "mod" suffix should not be installed,
// instead we run "go build -o" to rename them.
if (hasModSuffix(tool)) {
args.push('-d');
}
args.push(importPath);
const opts = {
env: envForTools,
cwd: toolsTmpDir
};
cp.execFile(goRuntimePath, args, opts, (err, stdout, stderr) => {
if (stderr.indexOf('unexpected directory layout:') > -1) {
outputChannel.appendLine(
`Installing ${importPath} failed with error "unexpected directory layout". Retrying...`
);
cp.execFile(goRuntimePath, args, opts, callback);
} else if (!err && hasModSuffix(tool)) {
const outputFile = path.join(
toolsGopath,
'bin',
process.platform === 'win32' ? `${tool.name}.exe` : tool.name
);
cp.execFile(
goRuntimePath,
['build', '-o', outputFile, getImportPath(tool, goVersion)],
opts,
callback
);
} else {
callback(err, stdout, stderr);
}
});
})
);
}, Promise.resolve([]))
.then((res) => {
outputChannel.appendLine(''); // Blank line for spacing
const failures = res.filter((x) => x != null);
if (failures.length === 0) {
outputChannel.appendLine('All tools successfully installed. You are ready to Go :).');
// Restart the language server since a new binary has been installed.
if (containsString(missing, 'gopls')) {
restartLanguageServer();
}
return;
}
outputChannel.appendLine(failures.length + ' tools failed to install.\n');
failures.forEach((failure) => {
const reason = failure.split(';;');
outputChannel.appendLine(reason[0] + ':');
outputChannel.appendLine(reason[1]);
});
});
}
export async function promptForMissingTool(toolName: string) {
const tool = getTool(toolName);
// If user has declined to install this tool, don't prompt for it.
if (containsTool(declinedInstalls, tool)) {
return;
}
const goVersion = await getGoVersion();
// Show error messages for outdated tools or outdated Go versions.
if (tool.minimumGoVersion && goVersion.lt(tool.minimumGoVersion.format())) {
vscode.window.showInformationMessage(`You are using go${goVersion.format()}, but ${tool.name} requires at least go${tool.minimumGoVersion.format()}.`);
return;
}
if (tool.maximumGoVersion && goVersion.gt(tool.maximumGoVersion.format())) {
vscode.window.showInformationMessage(`You are using go${goVersion.format()}, but ${tool.name} only supports go${tool.maximumGoVersion.format()} and below.`);
return;
}
const installOptions = ['Install'];
let missing = await getMissingTools(goVersion);
if (!containsTool(missing, tool)) {
return;
}
missing = missing.filter((x) => x === tool || tool.isImportant);
if (missing.length > 1) {
// Offer the option to install all tools.
installOptions.push('Install All');
}
const msg = `The "${tool.name}" command is not available. Run "go get -v ${getImportPath(
tool,
goVersion
)}" to install.`;
vscode.window.showInformationMessage(msg, ...installOptions).then((selected) => {
switch (selected) {
case 'Install':
installTools([tool], goVersion);
break;
case 'Install All':
installTools(missing, goVersion);
hideGoStatus();
break;
default:
// The user has declined to install this tool.
declinedInstalls.push(tool);
break;
}
});
}
export async function promptForUpdatingTool(toolName: string, newVersion?: SemVer) {
const tool = getTool(toolName);
const toolVersion = { ...tool, version: newVersion }; // ToolWithVersion
// If user has declined to update, then don't prompt.
if (containsTool(declinedUpdates, tool)) {
return;
}
const goVersion = await getGoVersion();
let updateMsg = `Your version of ${tool.name} appears to be out of date. Please update for an improved experience.`;
const choices: string[] = ['Update'];
if (toolName === `gopls`) {
choices.push('Release Notes');
}
if (newVersion) {
updateMsg = `A new version of ${tool.name} (v${newVersion}) is available. Please update for an improved experience.`;
}
vscode.window.showInformationMessage(updateMsg, ...choices).then((selected) => {
switch (selected) {
case 'Update':
installTools([toolVersion], goVersion);
break;
case 'Release Notes':
vscode.commands.executeCommand(
'vscode.open',
vscode.Uri.parse('https://github.com/golang/go/issues/33030#issuecomment-510151934')
);
break;
default:
declinedUpdates.push(tool);
break;
}
});
}
export function updateGoPathGoRootFromConfig(): Promise<void> {
const goroot = getGoConfig()['goroot'];
if (goroot) {
process.env['GOROOT'] = resolvePath(goroot);
}
if (process.env['GOPATH'] && process.env['GOROOT'] && process.env['GOPROXY']) {
return Promise.resolve();
}
// If GOPATH is still not set, then use the one from `go env`
const goRuntimePath = getBinPath('go');
if (!goRuntimePath) {
vscode.window.showErrorMessage(
`Failed to run "go env" to find GOPATH as the "go" binary cannot be found in either GOROOT(${process.env['GOROOT']}) or PATH(${envPath})`
);
return;
}
const goRuntimeBasePath = path.dirname(goRuntimePath);
// cgo and a few other Go tools expect Go binary to be in the path
let pathEnvVar: string;
if (process.env.hasOwnProperty('PATH')) {
pathEnvVar = 'PATH';
} else if (process.platform === 'win32' && process.env.hasOwnProperty('Path')) {
pathEnvVar = 'Path';
}
if (
goRuntimeBasePath &&
pathEnvVar &&
process.env[pathEnvVar] &&
(<string>process.env[pathEnvVar]).split(path.delimiter).indexOf(goRuntimeBasePath) === -1
) {
// Place the goRuntimeBasePath to the front so tools use the same version of go.
process.env[pathEnvVar] = goRuntimeBasePath + path.delimiter + process.env[pathEnvVar];
}
return new Promise<void>((resolve, reject) => {
cp.execFile(goRuntimePath, ['env', 'GOPATH', 'GOROOT', 'GOPROXY'], (err, stdout, stderr) => {
if (err) {
return reject();
}
const envOutput = stdout.split('\n');
if (!process.env['GOPATH'] && envOutput[0].trim()) {
process.env['GOPATH'] = envOutput[0].trim();
}
if (!process.env['GOROOT'] && envOutput[1] && envOutput[1].trim()) {
process.env['GOROOT'] = envOutput[1].trim();
}
if (!process.env['GOPROXY'] && envOutput[2] && envOutput[2].trim()) {
process.env['GOPROXY'] = envOutput[2].trim();
}
return resolve();
});
});
}
let alreadyOfferedToInstallTools = false;
export async function offerToInstallTools() {
if (alreadyOfferedToInstallTools) {
return;
}
alreadyOfferedToInstallTools = true;
const goVersion = await getGoVersion();
let missing = await getMissingTools(goVersion);
missing = missing.filter((x) => x.isImportant);
if (missing.length > 0) {
showGoStatus('Analysis Tools Missing', 'go.promptforinstall', 'Not all Go tools are available on the GOPATH');
vscode.commands.registerCommand('go.promptforinstall', () => {
const installItem = {
title: 'Install',
command() {
hideGoStatus();
installTools(missing, goVersion);
}
};
const showItem = {
title: 'Show',
command() {
outputChannel.clear();
outputChannel.appendLine('Below tools are needed for the basic features of the Go extension.');
missing.forEach((x) => outputChannel.appendLine(x.name));
}
};
vscode.window
.showInformationMessage(
'Failed to find some of the Go analysis tools. Would you like to install them?',
installItem,
showItem
)
.then((selection) => {
if (selection) {
selection.command();
} else {
hideGoStatus();
}
});
});
}
const usingSourceGraph = getToolFromToolPath(getLanguageServerToolPath()) === 'go-langserver';
if (usingSourceGraph && goVersion.gt('1.10')) {
const promptMsg =
'The language server from Sourcegraph is no longer under active development and it does not support Go modules as well. Please install and use the language server from Google or disable the use of language servers altogether.';
const disableLabel = 'Disable language server';
const installLabel = 'Install';
vscode.window.showInformationMessage(promptMsg, installLabel, disableLabel).then((selected) => {
if (selected === installLabel) {
installTools([getTool('gopls')], goVersion).then(() => {
// Restart the language server since the binary has changed.
restartLanguageServer();
});
} else if (selected === disableLabel) {
const goConfig = getGoConfig();
const inspectLanguageServerSetting = goConfig.inspect('useLanguageServer');
if (inspectLanguageServerSetting.globalValue === true) {
goConfig.update('useLanguageServer', false, vscode.ConfigurationTarget.Global);
} else if (inspectLanguageServerSetting.workspaceFolderValue === true) {
goConfig.update('useLanguageServer', false, vscode.ConfigurationTarget.WorkspaceFolder);
}
}
});
}
}
function getMissingTools(goVersion: GoVersion): Promise<Tool[]> {
const keys = getConfiguredTools(goVersion);
return Promise.all<Tool>(
keys.map(
(tool) =>
new Promise<Tool>((resolve, reject) => {
const toolPath = getBinPath(tool.name);
resolve(path.isAbsolute(toolPath) ? null : tool);
})
)
).then((res) => {
return res.filter((x) => x != null);
});
}