Skip to content

Commit

Permalink
extension/src/goInstallTools.ts: require go1.21+ for tools installation
Browse files Browse the repository at this point in the history
The Go extension will require go1.21 for tools installation from v0.44.0
(and is prerelease version v0.43.x).

This is a planned change and it was discussed in the v0.42.0 release note.
(https://github.com/golang/vscode-go/releases/tag/v0.42.0 Jul 17 2024).

`installTools` is the entry function for tools installation.
If the go version is too old, it suggests go1.21+ or the workaround
(go.toolsManagement.go).

* Misc changes
 - Previously, when the build info of a binary is not available,
   we didn't ask to update the tool. Since go1.18, the build info
   should be available. So, now suggest to reinstall the tool.
 - Bug fix: For vscgo, we used toolExecutionEnvironment when running
   go install.
   It should be toolInstallationEnvironment. This clears some env vars
   like GO111MODULE, GOPROXY, GOOS, GOARCH, GOROOT which can interfere
   with the go tool invocation.

Fixes #3411

Change-Id: Ifff0661d88a9adfc6bd3e0a25702d91921bcb77f
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/616676
Reviewed-by: Robert Findley <[email protected]>
Commit-Queue: Hyang-Ah Hana Kim <[email protected]>
kokoro-CI: kokoro <[email protected]>
Reviewed-by: Hongxiang Jiang <[email protected]>
  • Loading branch information
hyangah committed Oct 11, 2024
1 parent c107653 commit b78b78e
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 87 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Unreleased

### Changes

#### Tools installation

* The extension requires go1.21 or newer when it installs required tools. If your project must use go1.20 or older,
please manually install [compatible versions of required tools](https://github.com/golang/vscode-go/wiki/compatibility),
or configure the [`"go.toolsManagement.go"` setting](https://github.com/golang/vscode-go/wiki/settings#gotoolsmanagementgo)
to use the go1.21 or newer when installing tools. ([Issue 3411](https://github.com/golang/vscode-go/issues/3411))

### Code Health

* Extension build target is set to `es2022`. ([Issue 3540](https://github.com/golang/vscode-go/issues/3540))
Expand Down
102 changes: 60 additions & 42 deletions extension/src/goInstallTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ import { allToolsInformation } from './goToolsInformation';

const STATUS_BAR_ITEM_NAME = 'Go Tools';

// minimum go version required for tools installation.
const MINIMUM_GO_VERSION = '1.21.0';

// declinedUpdates tracks the tools that the user has declined to update.
const declinedUpdates: Tool[] = [];

Expand All @@ -52,7 +55,7 @@ const declinedInstalls: Tool[] = [];

export interface IToolsManager {
getMissingTools(filter: (tool: Tool) => boolean): Promise<Tool[]>;
installTool(tool: Tool, goVersion: GoVersion, env: NodeJS.Dict<string>): Promise<string | undefined>;
installTool(tool: Tool, goVersionForInstall: GoVersion, env: NodeJS.Dict<string>): Promise<string | undefined>;
}

export const defaultToolsManager: IToolsManager = {
Expand Down Expand Up @@ -105,20 +108,27 @@ export async function installAllTools(updateExistingToolsOnly = false) {
);
}

export const getGoForInstall = _getGoForInstall;
async function _getGoForInstall(goVersion: GoVersion): Promise<GoVersion | undefined> {
let configured = getGoConfig().get<string>('toolsManagement.go');
if (!configured) {
configured = goVersion.binaryPath;
// Returns the go version to be used for tools installation.
// If `go.toolsManagement.go` is set, it is preferred. Otherwise, the provided
// goVersion or the default version returned by getGoVersion is returned.
export const getGoVersionForInstall = _getGoVersionForInstall;
async function _getGoVersionForInstall(goVersion?: GoVersion): Promise<GoVersion | undefined> {
let configuredGoForInstall = getGoConfig().get<string>('toolsManagement.go');
if (!configuredGoForInstall) {
// A separate Go for install is not configured. Use the default Go.
const defaultGoVersion = goVersion ?? (await getGoVersion());
configuredGoForInstall = defaultGoVersion?.binaryPath;
}
try {
// goVersion may be the version picked based on the the minimum
// toolchain version requirement specified in go.mod or go.work.
// Compute the local toolchain version. (GOTOOLCHAIN=local go version)
const go = await getGoVersion(configured, 'local');
const go = await getGoVersion(configuredGoForInstall, 'local');
if (go) return go;
} catch (e) {
outputChannel.error(`failed to run "go version" with "${configured}". Provide a valid path to the Go binary`);
outputChannel.error(
`failed to run "go version" with "${configuredGoForInstall}". Provide a valid path to the Go binary`
);
}
return;
}
Expand Down Expand Up @@ -155,7 +165,7 @@ export async function installTools(
outputChannel.show();
}

const goForInstall = await getGoForInstall(goVersion);
const goForInstall = await getGoVersionForInstall(goVersion);
if (!goForInstall || !goForInstall.isValid()) {
vscode.window.showErrorMessage('Failed to find a go command needed to install tools.');
outputChannel.show(); // show error.
Expand All @@ -164,16 +174,15 @@ export async function installTools(
});
}

const minVersion = goForInstall.lt('1.21') ? (goVersion.lt('1.19') ? '1.19' : goVersion.format()) : '1.21.0';
if (goForInstall.lt(minVersion)) {
if (goForInstall.lt(MINIMUM_GO_VERSION)) {
vscode.window.showErrorMessage(
`Failed to find a go command (go${minVersion} or newer) needed to install tools. ` +
`Failed to find a go command (go${MINIMUM_GO_VERSION} or newer) needed to install tools. ` +
`The go command (${goForInstall.binaryPath}) is too old (go${goForInstall.svString}). ` +
'If your project requires a Go version older than go1.19, either manually install the tools or, use the "go.toolsManagement.go" setting ' +
'to configure the Go version used for tools installation. See https://github.com/golang/vscode-go/issues/2898.'
`If your project requires a Go version older than go${MINIMUM_GO_VERSION}, please manually install the tools or, use the "go.toolsManagement.go" setting ` +
`to configure a different go command (go ${MINIMUM_GO_VERSION}+) to be used for tools installation. See https://github.com/golang/vscode-go/issues/3411.`
);
return missing.map((tool) => {
return { tool: tool, reason: `failed to find go (requires go${minVersion} or newer)` };
return { tool: tool, reason: `failed to find go (requires go${MINIMUM_GO_VERSION} or newer)` };
});
}

Expand Down Expand Up @@ -273,20 +282,21 @@ async function tmpDirForToolInstallation() {
return toolsTmpDir;
}

// installTool installs the specified tool.
// installTool is used by goEnvironmentStatus.ts.
// TODO(hyangah): replace the callsite to use defaultToolsManager and remove this.
export async function installTool(tool: ToolAtVersion): Promise<string | undefined> {
const goVersion = await getGoForInstall(await getGoVersion());
if (!goVersion) {
const goVersionForInstall = await getGoVersionForInstall();
if (!goVersionForInstall) {
return 'failed to find "go" for install';
}
const envForTools = toolInstallationEnvironment();

return await installToolWithGo(tool, goVersion, envForTools);
return await installToolWithGo(tool, goVersionForInstall, envForTools);
}

async function installToolWithGo(
tool: ToolAtVersion,
goVersion: GoVersion, // go version to be used for installation.
goVersionForInstall: GoVersion, // go version used to install the tool.
envForTools: NodeJS.Dict<string>
): Promise<string | undefined> {
const env = Object.assign({}, envForTools);
Expand All @@ -295,10 +305,14 @@ async function installToolWithGo(
if (!version && tool.usePrereleaseInPreviewMode && extensionInfo.isPreview) {
version = await latestToolVersion(tool, true);
}
const importPath = getImportPathWithVersion(tool, version, goVersion);
// TODO(hyangah): should we allow to choose a different version of the tool
// depending on the project's go version (i.e. getGoVersion())? For example,
// if a user is using go1.20 for their project, should we pick [email protected]
// instead? In that case, we should pass getGoVersion().
const importPath = getImportPathWithVersion(tool, version, goVersionForInstall);

try {
await installToolWithGoInstall(goVersion, env, importPath);
await installToolWithGoInstall(goVersionForInstall, env, importPath);
const toolInstallPath = getBinPath(tool.name);
outputChannel.appendLine(`Installing ${importPath} (${toolInstallPath}) SUCCEEDED`);
} catch (e) {
Expand Down Expand Up @@ -533,8 +547,9 @@ export function updateGoVarsFromConfig(goCtx: GoExtensionContext): Promise<void>
});
}

// maybeInstallImportantTools checks whether important tools are installed,
// and tries to auto-install them if missing.
// maybeInstallImportantTools checks whether important tools are installed
// and they meet the version requirement.
// Then it tries to auto-install them if missing.
export async function maybeInstallImportantTools(
alternateTools: { [key: string]: string } | undefined,
tm: IToolsManager = defaultToolsManager
Expand Down Expand Up @@ -783,10 +798,10 @@ export async function shouldUpdateTool(tool: Tool, toolPath: string): Promise<bo
}

export async function suggestUpdates() {
const configuredGoVersion = await getGoVersion();
if (!configuredGoVersion || configuredGoVersion.lt('1.19')) {
// User is using an ancient or a dev version of go. Don't suggest updates -
// user should know what they are doing.
const configuredGoVersion = await getGoVersionForInstall();
if (!configuredGoVersion || configuredGoVersion.lt(MINIMUM_GO_VERSION)) {
// User is using an old or dev version of go.
// Don't suggest updates.
return;
}

Expand Down Expand Up @@ -830,15 +845,13 @@ export async function listOutdatedTools(configuredGoVersion: GoVersion | undefin
return;
}
const m = await inspectGoToolVersion(toolPath);
if (!m) {
console.log(`failed to get go tool version: ${toolPath}`);
return;
}
const { goVersion } = m;
const { goVersion } = m || {};
if (!goVersion) {
// TODO: we cannot tell whether the tool was compiled with a newer version of go
// The tool was compiled with a newer version of go
// or a very old go (<go1.18)
// or compiled in an unconventional way.
return;
// Suggest to reinstall the tool anyway.
return tool;
}
const toolGoVersion = new GoVersion('', `go version ${goVersion} os/arch`);
if (!toolGoVersion || !toolGoVersion.sv) {
Expand All @@ -860,12 +873,12 @@ export async function listOutdatedTools(configuredGoVersion: GoVersion | undefin
// We test the inequality by checking whether the exact beta or rc version
// appears in the `go version` output. e.g.,
// configuredGoVersion.version goVersion(tool) update
// 'go version go1.21 ...' 'go1.21beta1' Yes
// 'go version go1.21beta1 ...' 'go1.21beta1' No
// 'go version go1.21beta2 ...' 'go1.21beta1' Yes
// 'go version go1.21rc1 ...' 'go1.21beta1' Yes
// 'go version go1.21 ...' 'go1.21rc1' Yes
// 'go version go1.21rc1 ...' 'go1.21rc1' No
// 'go version go1.21rc2 ...' 'go1.21rc1' Yes
// 'go version go1.21rc1 ...' 'go1.21rc1' Yes
// 'go version go1.21rc1 ...' 'go1.21' No
// 'go version devel go1.21-deadbeaf ...' 'go1.21beta1' No (* rare)
// 'go version devel go1.21-deadbeef ...' 'go1.21rc1' No (* rare)
!configuredGoVersion.version.includes(goVersion)
) {
return tool;
Expand Down Expand Up @@ -899,7 +912,7 @@ export async function maybeInstallVSCGO(
const execFile = util.promisify(cp.execFile);

const cwd = path.join(extensionPath);
const env = toolExecutionEnvironment();
const env = toolInstallationEnvironment();
env['GOBIN'] = path.dirname(progPath);

const importPath = allToolsInformation['vscgo'].importPath;
Expand All @@ -911,9 +924,14 @@ export async function maybeInstallVSCGO(
: `@v${extensionVersion}`;
// build from source acquired from the module proxy if this is a non-preview version.
try {
const goForInstall = await getGoVersionForInstall();
const goBinary = goForInstall?.binaryPath;
if (!goBinary) {
throw new Error('"go" binary is not found');
}
const args = ['install', '-trimpath', `${importPath}${version}`];
console.log(`installing vscgo: ${args.join(' ')}`);
await execFile(getBinPath('go'), args, { cwd, env });
await execFile(goBinary, args, { cwd, env });
return progPath;
} catch (e) {
telemetryReporter.add('vscgo_install_fail', 1);
Expand Down
8 changes: 4 additions & 4 deletions extension/src/goTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ export interface Tool {
replacedByGopls?: boolean;
description: string;

// If true, consider prerelease version in preview mode
// (nightly & dev)
// If true, consider prerelease version in prerelease mode
// (prerelease & dev)
usePrereleaseInPreviewMode?: boolean;
// If set, this string will be used when installing the tool
// instead of the default 'latest'. It can be used when
// we need to pin a tool version (`deadbeaf`) or to use
// we need to pin a tool version (`deadbeef`) or to use
// a dev version available in a branch (e.g. `master`).
defaultVersion?: string;

Expand Down Expand Up @@ -53,7 +53,7 @@ export interface ToolAtVersion extends Tool {
export function getImportPathWithVersion(
tool: Tool,
version: semver.SemVer | string | undefined | null,
goVersion: GoVersion
goVersion: GoVersion // This is the Go version to build the project.
): string {
const importPath = tool.importPath;
if (version) {
Expand Down
Loading

0 comments on commit b78b78e

Please sign in to comment.