Skip to content

Commit

Permalink
src/goInstallTools: use go list to check upgradability
Browse files Browse the repository at this point in the history
The extension has been querying the module proxy's /@v/list
endpoint directly to get the list of available gopls version.
This change changes the logic to use `go list -m` to get the
version list. With go1.16, version retraction feature
was added, and using the go command is more reliable way to
learn about the retracted versions. So, use that.

This change also fixes a bug in the tool installation that
prevented Go nightly users from installing gopls prerelease
versions when using 'Go: Update/Install Tools' command.
If the go command supports `@latest-prerelease` tag, it would
have been easier. But that's not available, so we run `go list
-m --versions` and find the last version from Versions.

Updates golang/go#43141

Change-Id: I67fe368cbce734e3b39e5352b3b3f6f08918562c
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/295418
Trust: Hyang-Ah Hana Kim <[email protected]>
Trust: Rebecca Stambler <[email protected]>
Trust: Suzy Mueller <[email protected]>
Run-TryBot: Hyang-Ah Hana Kim <[email protected]>
TryBot-Result: kokoro <[email protected]>
Reviewed-by: Suzy Mueller <[email protected]>
  • Loading branch information
hyangah committed Mar 11, 2021
1 parent 3d30385 commit a319988
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 89 deletions.
88 changes: 68 additions & 20 deletions src/goInstallTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import cp = require('child_process');
import fs = require('fs');
import path = require('path');
import { SemVer } from 'semver';
import semver = require('semver');
import { ConfigurationTarget } from 'vscode';
import { getGoConfig } from './config';
import { toolExecutionEnvironment, toolInstallationEnvironment } from './goEnv';
Expand Down Expand Up @@ -41,6 +41,7 @@ import {
import { correctBinname, envPath, getCurrentGoRoot, setCurrentGoRoot } from './utils/pathUtils';
import util = require('util');
import vscode = require('vscode');
import { isInPreviewMode } from './goLanguageServer';

// declinedUpdates tracks the tools that the user has declined to update.
const declinedUpdates: Tool[] = [];
Expand Down Expand Up @@ -195,6 +196,18 @@ export async function installTools(
return failures;
}

async function tmpDirForToolInstallation() {
// Install tools in a temporary directory, to avoid altering go.mod files.
const mkdtemp = util.promisify(fs.mkdtemp);
const toolsTmpDir = await mkdtemp(getTempFilePath('go-tools-'));
// Write a temporary go.mod file to avoid version conflicts.
const tmpGoModFile = path.join(toolsTmpDir, 'go.mod');
const writeFile = util.promisify(fs.writeFile);
await writeFile(tmpGoModFile, 'module tools');

return toolsTmpDir;
}

export async function installTool(
tool: ToolAtVersion,
goVersion: GoVersion,
Expand All @@ -208,21 +221,16 @@ export async function installTool(
return reason;
}
}
// Install tools in a temporary directory, to avoid altering go.mod files.
const mkdtemp = util.promisify(fs.mkdtemp);
const toolsTmpDir = await mkdtemp(getTempFilePath('go-tools-'));
let toolsTmpDir = '';
try {
toolsTmpDir = await tmpDirForToolInstallation();
} catch (e) {
return `Failed to create a temp directory: ${e}`;
}

const env = Object.assign({}, envForTools);
let tmpGoModFile: string;
if (modulesOn) {
env['GO111MODULE'] = 'on';
env['GO111MODULE'] = modulesOn ? 'on' : 'off';

// Write a temporary go.mod file to avoid version conflicts.
tmpGoModFile = path.join(toolsTmpDir, 'go.mod');
const writeFile = util.promisify(fs.writeFile);
await writeFile(tmpGoModFile, 'module tools');
} else {
env['GO111MODULE'] = 'off';
}
// Some users use direnv-like setup where the choice of go is affected by
// the current directory path. In order to avoid choosing a different go,
// we will explicitly use `GOROOT/bin/go` instead of goVersion.binaryPath
Expand All @@ -246,7 +254,11 @@ export async function installTool(
if (!modulesOn) {
importPath = getImportPath(tool, goVersion);
} else {
importPath = getImportPathWithVersion(tool, tool.version, goVersion);
let version = tool.version;
if (!version && tool.usePrereleaseInPreviewMode && isInPreviewMode()) {
version = await latestToolVersion(tool, true);
}
importPath = getImportPathWithVersion(tool, version, goVersion);
}
args.push(importPath);

Expand All @@ -266,7 +278,7 @@ export async function installTool(
// Actual installation of the -gomod tool is done by running go build.
const gopath = env['GOBIN'] || env['GOPATH'];
if (!gopath) {
return 'GOBIN/GOPATH not configured in environment';
throw new Error('GOBIN/GOPATH not configured in environment');
}
const destDir = gopath.split(path.delimiter)[0];
const outputFile = path.join(destDir, 'bin', process.platform === 'win32' ? `${tool.name}.exe` : tool.name);
Expand All @@ -278,11 +290,11 @@ export async function installTool(
outputChannel.appendLine(`Installing ${importPath} FAILED`);
outputChannel.appendLine(`${JSON.stringify(e, null, 1)}`);
result = `failed to install ${tool.name}(${importPath}): ${e} ${output}`;
} finally {
// Delete the temporary installation directory.
rmdirRecursive(toolsTmpDir);
}

// Delete the temporary installation directory.
rmdirRecursive(toolsTmpDir);

return result;
}

Expand Down Expand Up @@ -347,7 +359,7 @@ Run "go get -v ${getImportPath(tool, goVersion)}" to install.`;

export async function promptForUpdatingTool(
toolName: string,
newVersion?: SemVer,
newVersion?: semver.SemVer,
crashed?: boolean,
message?: string
) {
Expand Down Expand Up @@ -571,3 +583,39 @@ async function suggestDownloadGo() {
}
suggestedDownloadGo = true;
}

// ListVersionsOutput is the output of `go list -m -versions -json`.
interface ListVersionsOutput {
Version: string; // module version
Versions?: string[]; // available module versions (with -versions)
}

// latestToolVersion returns the latest version of the tool.
export async function latestToolVersion(tool: Tool, includePrerelease?: boolean): Promise<semver.SemVer | null> {
const goCmd = getBinPath('go');
const tmpDir = await tmpDirForToolInstallation();
const execFile = util.promisify(cp.execFile);
let ret: semver.SemVer | null = null;
try {
const env = toolInstallationEnvironment();
env['GO111MODULE'] = 'on';
// Run go list in a temp directory to avoid altering go.mod
// when using older versions of go (<1.16).
const version = 'latest'; // TODO(hyangah): use 'master' for delve-dap.
const { stdout } = await execFile(
goCmd,
['list', '-m', '--versions', '-json', `${tool.modulePath}@${version}`],
{ env, cwd: tmpDir }
);
const m = <ListVersionsOutput>JSON.parse(stdout);
// Versions field is a list of all known versions of the module,
// ordered according to semantic versioning, earliest to latest.
const latest = includePrerelease && m.Versions && m.Versions.length > 0 ? m.Versions.pop() : m.Version;
ret = semver.parse(latest);
} catch (e) {
console.log(`failed to retrieve the latest tool ${tool.name} version: ${e}`);
} finally {
rmdirRecursive(tmpDir);
}
return ret;
}
38 changes: 3 additions & 35 deletions src/goLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { toolExecutionEnvironment } from './goEnv';
import { GoHoverProvider } from './goExtraInfo';
import { GoDocumentFormattingEditProvider } from './goFormat';
import { GoImplementationProvider } from './goImplementations';
import { installTools, promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
import { installTools, latestToolVersion, promptForMissingTool, promptForUpdatingTool } from './goInstallTools';
import { parseLiveFile } from './goLiveErrors';
import {
buildDiagnosticCollection,
Expand Down Expand Up @@ -1006,7 +1006,8 @@ export async function shouldUpdateLanguageServer(tool: Tool, cfg: LanguageServer
}

// Get the latest gopls version. If it is for nightly, using the prereleased version is ok.
let latestVersion = cfg.checkForUpdates === 'local' ? tool.latestVersion : await getLatestGoplsVersion(tool);
let latestVersion =
cfg.checkForUpdates === 'local' ? tool.latestVersion : await latestToolVersion(tool, isInPreviewMode());

// If we failed to get the gopls version, pick the one we know to be latest at the time of this extension's last update
if (!latestVersion) {
Expand Down Expand Up @@ -1126,39 +1127,6 @@ export const getTimestampForVersion = async (tool: Tool, version: semver.SemVer)
return time;
};

const acceptGoplsPrerelease = isInPreviewMode();

export const getLatestGoplsVersion = async (tool: Tool) => {
// If the user has a version of gopls that we understand,
// ask the proxy for the latest version, and if the user's version is older,
// prompt them to update.
const data = await goProxyRequest(tool, 'list');
if (!data) {
return null;
}
// Coerce the versions into SemVers so that they can be sorted correctly.
const versions = [];
for (const version of data.trim().split('\n')) {
const parsed = semver.parse(version, {
includePrerelease: true,
loose: true
});
if (parsed) {
versions.push(parsed);
}
}
if (versions.length === 0) {
return null;
}
versions.sort(semver.rcompare);

if (acceptGoplsPrerelease) {
return versions[0]; // The first one (newest one).
}
// The first version in the sorted list without a prerelease tag.
return versions.find((version) => !version.prerelease || !version.prerelease.length);
};

// getLocalGoplsVersion returns the version of gopls that is currently
// installed on the user's machine. This is determined by running the
// `gopls version` command.
Expand Down
Loading

0 comments on commit a319988

Please sign in to comment.