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
  • Loading branch information
hyangah committed Oct 9, 2024
1 parent c107653 commit 81f04c5
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 78 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
84 changes: 48 additions & 36 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 @@ -106,10 +109,11 @@ export async function installAllTools(updateExistingToolsOnly = false) {
}

export const getGoForInstall = _getGoForInstall;
async function _getGoForInstall(goVersion: GoVersion): Promise<GoVersion | undefined> {
async function _getGoForInstall(goVersion?: GoVersion): Promise<GoVersion | undefined> {
let configured = getGoConfig().get<string>('toolsManagement.go');
if (!configured) {
configured = goVersion.binaryPath;
const defaultGoVersion = goVersion ?? (await getGoVersion('go'));
configured = defaultGoVersion?.binaryPath;
}
try {
// goVersion may be the version picked based on the the minimum
Expand Down Expand Up @@ -164,16 +168,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 +276,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 getGoForInstall();
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 +299,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 +541,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 +792,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 getGoForInstall();
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 +839,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 +867,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 +906,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 +918,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 getGoForInstall();
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
78 changes: 40 additions & 38 deletions extension/test/integration/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ suite('Installation Tests', function () {
});

test('Try to install with old go', async () => {
const oldGo = new GoVersion(getBinPath('go'), 'go version go1.17 amd64/linux');
const oldGo = new GoVersion(getBinPath('go'), 'go version go1.20 amd64/linux');
sandbox.stub(goInstallTools, 'getGoForInstall').returns(Promise.resolve(oldGo));
const failures = await installTools([getToolAtVersion('gopls')], oldGo);
assert(failures?.length === 1 && failures[0].tool.name === 'gopls' && failures[0].reason.includes('or newer'));
Expand All @@ -235,44 +235,44 @@ suite('Installation Tests', function () {
const gofumptDefault = allToolsInformation['gofumpt'].defaultVersion!;
test('Install gofumpt with old go', async () => {
await runTest(
[{ name: 'gofumpt', versions: ['v0.4.0', 'v0.5.0', gofumptDefault], wantVersion: 'v0.5.0' }],
[{ name: 'gofumpt', versions: ['v0.5.0', 'v0.6.0', gofumptDefault], wantVersion: 'v0.6.0' }],
true, // LOCAL PROXY
true, // GOBIN
'go1.19' // Go Version
'go1.21' // Go Version
);
});
test('Install gofumpt with new go', async () => {
await runTest(
[{ name: 'gofumpt', versions: ['v0.4.0', 'v0.5.0', gofumptDefault], wantVersion: gofumptDefault }],
[{ name: 'gofumpt', versions: ['v0.4.7', 'v0.5.0', gofumptDefault], wantVersion: gofumptDefault }],
true, // LOCAL PROXY
true, // GOBIN
'go1.22.0' // Go Version
'go1.23.0' // Go Version
);
});

test('Install a tool, with go1.21.0', async () => {
test('Install a tool, with go for install', async () => {
const systemGoVersion = await getGoVersion();
const oldGo = new GoVersion(systemGoVersion.binaryPath, 'go version go1.21.0 linux/amd64');
const wantGoForInstall = new GoVersion(systemGoVersion.binaryPath, 'go version go1.99.0 linux/amd64');
const tm: IToolsManager = {
getMissingTools: () => {
assert.fail('must not be called');
},
installTool: (tool, goVersion, env) => {
installTool: (tool, goVersionForInstall, env) => {
// Assert the go install command is what we expect.
assert.strictEqual(tool.name, 'gopls');
assert.strictEqual(goVersion, oldGo);
assert.strictEqual(goVersionForInstall, wantGoForInstall);
assert(env['GOTOOLCHAIN'], `go${systemGoVersion.format()}+auto`);
// runTest checks if the tool build succeeds. So, delegate the remaining
// build task to the default tools manager's installTool function.
return defaultToolsManager.installTool(tool, goVersion, env);
return defaultToolsManager.installTool(tool, systemGoVersion, env);
}
};
await runTest(
[{ name: 'gopls', versions: ['v0.1.0', 'v1.0.0'], wantVersion: 'v1.0.0' }],
true, // LOCAL PROXY
true, // GOBIN
'go' + systemGoVersion.format(true), // Go Version
oldGo, // Go for install
wantGoForInstall, // Go for install
tm // stub installTool to
);
});
Expand Down Expand Up @@ -371,6 +371,8 @@ suite('listOutdatedTools', () => {
});
teardown(() => sandbox.restore());

// goVersion: go toolchain's version
// tools: toolname -> go toolchain version used to compile the tool
async function runTest(goVersion: string | undefined, tools: { [key: string]: string | undefined }) {
const binPathStub = sandbox.stub(utilModule, 'getBinPath');
const versionStub = sandbox.stub(goInstallTools, 'inspectGoToolVersion');
Expand All @@ -387,53 +389,53 @@ suite('listOutdatedTools', () => {
}

test('minor version difference requires updates', async () => {
const x = await runTest('go version go1.18 linux/amd64', {
gopls: 'go1.16', // 1.16 < 1.18
dlv: 'go1.17', // 1.17 < 1.18
staticcheck: 'go1.18', // 1.18 == 1.18
gotests: 'go1.19' // 1.19 > 1.18
const x = await runTest('go version go1.23.0 linux/amd64', {
gopls: 'go1.22.2',
dlv: 'go1.21.0',
staticcheck: 'go1.23.0',
gotests: 'go1.24.1'
});
assert.deepStrictEqual(x, ['gopls', 'dlv']);
});
test('patch version difference does not require updates', async () => {
const x = await runTest('go version go1.16.1 linux/amd64', {
gopls: 'go1.16', // 1.16 < 1.16.1
dlv: 'go1.16.1', // 1.16.1 == 1.16.1
staticcheck: 'go1.16.2', // 1.16.2 > 1.16.1
gotests: 'go1.16rc1' // 1.16rc1 != 1.16.1
const x = await runTest('go version go1.23.1 linux/amd64', {
gopls: 'go1.23.0', // 1.16 < 1.16.1
dlv: 'go1.23.1', // 1.16.1 == 1.16.1
staticcheck: 'go1.23.2', // 1.16.2 > 1.16.1
gotests: 'go1.23rc1' // go1.23rc1 != go1.23.0
});
assert.deepStrictEqual(x, ['gotests']);
});
test('go is beta version', async () => {
const x = await runTest('go version go1.18beta2 linux/amd64', {
gopls: 'go1.17.1', // 1.17.1 < 1.18beta2
dlv: 'go1.18beta1', // 1.18beta1 != 1.18beta2
staticcheck: 'go1.18beta2', // 1.18beta2 == 1.18beta2
gotests: 'go1.18' // 1.18 > 1.18beta2
test('go is rc version', async () => {
const x = await runTest('go version go1.23rc2 linux/amd64', {
gopls: 'go1.22.3', // go1.22.3 < go1.23rc2
dlv: 'go1.23rc1', // go1.23rc1 < go1.23rc2
staticcheck: 'go1.23rc2', // same
gotests: 'go1.23.0' // go1.23rc2 < go1.23.0
});
assert.deepStrictEqual(x, ['gopls', 'dlv']);
});
test('go is dev version', async () => {
const x = await runTest('go version devel go1.18-41f485b9a7 linux/amd64', {
gopls: 'go1.17.1',
dlv: 'go1.18beta1',
staticcheck: 'go1.18',
gotests: 'go1.19'
test('go is dev version - skip version check', async () => {
const x = await runTest('go version devel go1.24-41f485b9a7 linux/amd64', {
gopls: 'go1.13.1',
dlv: 'go1.24rc1',
staticcheck: 'go1.23.0',
gotests: 'go1.24.0'
});
assert.deepStrictEqual(x, []);
});
test('go is unknown version', async () => {
test('go is unknown version - skip version check', async () => {
const x = await runTest('', {
gopls: 'go1.17.1'
gopls: 'go1.23.1'
});
assert.deepStrictEqual(x, []);
});
test('tools are unknown versions', async () => {
const x = await runTest('go version go1.17 linux/amd64', {
const x = await runTest('go version go1.22.0 linux/amd64', {
gopls: undefined, // this can be because gopls was compiled with go1.18 or it's too old.
dlv: 'go1.16.1'
dlv: 'go1.20'
});
assert.deepStrictEqual(x, ['dlv']);
assert.deepStrictEqual(x, ['gopls', 'dlv']);
});
});

Expand Down

0 comments on commit 81f04c5

Please sign in to comment.