Skip to content

Commit 8e21cbe

Browse files
committed
[release] src/goInstallTools.ts: fix PATH adjustment when a different go is chosen
With commits d93a0ae and a5e40ca (microsoft/vscode-go#3152), we tried to include the go binary's path to the PATH (Path on windows) in order to ensure all underlying go tools (gofmt, cgo, gopls, ...) that simply invoke 'go' can find the matching go binary by searching the PATH. We found that trick does not work when users specifies a different go version using go.alternateTools and the specified binary is not named 'go'. For example, golang.org provides an easy way to install extra versions of Go https://golang.org/doc/install#extra_versions through a wrapper, whose name includes the version. Users who take this approach should be able to configure to pick up the chosen version with ``` "go.alternateTools": { "go": "/Users/username/go/bin/go1.13.11" } ``` Previously, we just added /Users/username/go/bin (the go binary directory name) to PATH. Because there is no 'go' binary in this directory, the underlying tools failed to pick the right go tool. In this CL, we instead use the GOROOT (found from go env call) and add GOROOT/bin to the PATH. In this CL - We also arrange to call updateGoVarsFromConfig only when the relevant configs are changed (onDidChangeConfiguration setup in goMain.ts). Previously, this was called almost on every file save events if the repository has a workspace setting file (.vscode/setting.json). - We also changed initGoStatusBar to be called after the goroot is updated. That eliminates an extra call path (initGoStatusBar -> ... -> updateGoVarsFromConfig, and also, reflects goroot changes correctly when the configuration is updated. Updates #146 Updates #26 Change-Id: I9b0e42787308e17547067460960b5fdd8b678991 GitHub-Last-Rev: 995c1a3 GitHub-Pull-Request: #252 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/239697 Reviewed-by: Brayden Cloud <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> (cherry picked from commit 057186f) Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/239981
1 parent c711a58 commit 8e21cbe

File tree

7 files changed

+240
-52
lines changed

7 files changed

+240
-52
lines changed

src/goEnvironmentStatus.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,15 @@ let goEnvStatusbarItem: vscode.StatusBarItem;
1818
* Initialize the status bar item with current Go binary
1919
*/
2020
export async function initGoStatusBar() {
21-
goEnvStatusbarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
22-
23-
// make goroot default to go.goroot and fallback on $PATH
24-
const goroot = await getActiveGoRoot();
25-
if (!goroot) {
26-
// TODO: prompt user to install Go
27-
vscode.window.showErrorMessage('No Go command could be found.');
21+
if (!goEnvStatusbarItem) {
22+
goEnvStatusbarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 50);
2823
}
29-
3024
// set Go version and command
3125
const version = await getGoVersion();
26+
27+
hideGoStatusBar();
3228
goEnvStatusbarItem.text = formatGoVersion(version.format());
3329
goEnvStatusbarItem.command = 'go.environment.choose';
34-
3530
showGoStatusBar();
3631
}
3732

src/goInstallTools.ts

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SemVer } from 'semver';
1212
import util = require('util');
1313
import vscode = require('vscode');
1414
import { toolInstallationEnvironment } from './goEnv';
15+
import { initGoStatusBar } from './goEnvironmentStatus';
1516
import { getLanguageServerToolPath } from './goLanguageServer';
1617
import { restartLanguageServer } from './goMain';
1718
import { envPath, getCurrentGoRoot, getToolFromToolPath, setCurrentGoRoot } from './goPath';
@@ -335,36 +336,22 @@ export async function promptForUpdatingTool(toolName: string, newVersion?: SemVe
335336
}
336337

337338
export function updateGoVarsFromConfig(): Promise<void> {
338-
if (getCurrentGoRoot() && process.env['GOPATH'] && process.env['GOPROXY'] && process.env['GOBIN']) {
339+
// FIXIT: when user changes the environment variable settings or go.gopath, the following
340+
// condition prevents from updating the process.env accordingly, so the extension will lie.
341+
// Needs to clean up.
342+
if (process.env['GOPATH'] && process.env['GOPROXY'] && process.env['GOBIN']) {
339343
return Promise.resolve();
340344
}
341345

342-
// If GOPATH is still not set, then use the one from `go env`
343-
const goRuntimePath = getBinPath('go');
346+
// FIXIT: if updateGoVarsFromConfig is called again after addGoRuntimeBaseToPATH sets PATH,
347+
// the go chosen by getBinPath based on PATH will not change.
348+
const goRuntimePath = getBinPath('go', false);
344349
if (!goRuntimePath) {
345350
vscode.window.showErrorMessage(
346351
`Failed to run "go env" to find GOPATH as the "go" binary cannot be found in either GOROOT(${getCurrentGoRoot()}) or PATH(${envPath})`
347352
);
348353
return;
349354
}
350-
const goRuntimeBasePath = path.dirname(goRuntimePath);
351-
352-
// cgo and a few other Go tools expect Go binary to be in the path
353-
let pathEnvVar: string;
354-
if (process.env.hasOwnProperty('PATH')) {
355-
pathEnvVar = 'PATH';
356-
} else if (process.platform === 'win32' && process.env.hasOwnProperty('Path')) {
357-
pathEnvVar = 'Path';
358-
}
359-
if (
360-
goRuntimeBasePath &&
361-
pathEnvVar &&
362-
process.env[pathEnvVar] &&
363-
(<string>process.env[pathEnvVar]).split(path.delimiter).indexOf(goRuntimeBasePath) === -1
364-
) {
365-
// Place the goRuntimeBasePath to the front so tools use the same version of go.
366-
process.env[pathEnvVar] = goRuntimeBasePath + path.delimiter + process.env[pathEnvVar];
367-
}
368355

369356
return new Promise<void>((resolve, reject) => {
370357
cp.execFile(goRuntimePath, ['env', 'GOPATH', 'GOROOT', 'GOPROXY', 'GOBIN'], (err, stdout, stderr) => {
@@ -375,7 +362,7 @@ export function updateGoVarsFromConfig(): Promise<void> {
375362
if (!process.env['GOPATH'] && envOutput[0].trim()) {
376363
process.env['GOPATH'] = envOutput[0].trim();
377364
}
378-
if (!getCurrentGoRoot() && envOutput[1] && envOutput[1].trim()) {
365+
if (envOutput[1] && envOutput[1].trim()) {
379366
setCurrentGoRoot(envOutput[1].trim());
380367
}
381368
if (!process.env['GOPROXY'] && envOutput[2] && envOutput[2].trim()) {
@@ -384,11 +371,48 @@ export function updateGoVarsFromConfig(): Promise<void> {
384371
if (!process.env['GOBIN'] && envOutput[3] && envOutput[3].trim()) {
385372
process.env['GOBIN'] = envOutput[3].trim();
386373
}
374+
375+
// cgo, gopls, and other underlying tools will inherit the environment and attempt
376+
// to locate 'go' from the PATH env var.
377+
addGoRuntimeBaseToPATH(path.join(getCurrentGoRoot(), 'bin'));
378+
initGoStatusBar();
379+
// TODO: restart language server or synchronize with language server update.
380+
387381
return resolve();
388382
});
389383
});
390384
}
391385

386+
// PATH value cached before addGoRuntimeBaseToPath modified.
387+
let defaultPathEnv = '';
388+
389+
// addGoRuntimeBaseToPATH adds the given path to the front of the PATH environment variable.
390+
// It removes duplicates.
391+
// TODO: can we avoid changing PATH but utilize toolExecutionEnv?
392+
function addGoRuntimeBaseToPATH(newGoRuntimeBase: string) {
393+
if (!newGoRuntimeBase) {
394+
return;
395+
}
396+
397+
let pathEnvVar: string;
398+
if (process.env.hasOwnProperty('PATH')) {
399+
pathEnvVar = 'PATH';
400+
} else if (process.platform === 'win32' && process.env.hasOwnProperty('Path')) {
401+
pathEnvVar = 'Path';
402+
} else {
403+
return;
404+
}
405+
406+
if (!defaultPathEnv) { // cache the default value
407+
defaultPathEnv = <string>process.env[pathEnvVar];
408+
}
409+
410+
let pathVars = defaultPathEnv.split(path.delimiter);
411+
pathVars = pathVars.filter((p) => p !== newGoRuntimeBase);
412+
pathVars.unshift(newGoRuntimeBase);
413+
process.env[pathEnvVar] = pathVars.join(path.delimiter);
414+
}
415+
392416
let alreadyOfferedToInstallTools = false;
393417

394418
export async function offerToInstallTools() {

src/goMain.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
import { GoDebugConfigurationProvider } from './goDebugConfiguration';
1919
import { extractFunction, extractVariable } from './goDoctor';
2020
import { toolExecutionEnvironment } from './goEnv';
21-
import { chooseGoEnvironment, initGoStatusBar } from './goEnvironmentStatus';
21+
import { chooseGoEnvironment } from './goEnvironmentStatus';
2222
import { runFillStruct } from './goFillStruct';
2323
import * as goGenerateTests from './goGenerateTests';
2424
import { goGetPackage } from './goGetPackage';
@@ -167,11 +167,6 @@ export function activate(ctx: vscode.ExtensionContext): void {
167167
);
168168
})
169169
);
170-
showHideStatus(vscode.window.activeTextEditor);
171-
172-
// show the go environment status bar item
173-
initGoStatusBar();
174-
175170
const testCodeLensProvider = new GoRunTestCodeLensProvider();
176171
const referencesCodeLensProvider = new GoReferencesCodeLensProvider();
177172

@@ -407,8 +402,14 @@ export function activate(ctx: vscode.ExtensionContext): void {
407402
return;
408403
}
409404
const updatedGoConfig = getGoConfig();
410-
updateGoVarsFromConfig();
411405

406+
if (e.affectsConfiguration('go.goroot') ||
407+
e.affectsConfiguration('go.alternateTools') ||
408+
e.affectsConfiguration('go.gopath') ||
409+
e.affectsConfiguration('go.toolsEnvVars') ||
410+
e.affectsConfiguration('go.testEnvFile')) {
411+
updateGoVarsFromConfig();
412+
}
412413
// If there was a change in "toolsGopath" setting, then clear cache for go tools
413414
if (getToolsGopath() !== getToolsGopath(false)) {
414415
clearCacheForTools();

src/goPath.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import fs = require('fs');
1313
import os = require('os');
1414
import path = require('path');
15+
import { getGoConfig } from './util';
1516

1617
let binPathCache: { [bin: string]: string } = {};
1718

@@ -34,14 +35,16 @@ export function getBinPathFromEnvVar(toolName: string, envVarValue: string, appe
3435
export function getBinPathWithPreferredGopath(
3536
toolName: string,
3637
preferredGopaths: string[],
37-
alternateTool?: string
38+
alternateTool?: string,
39+
useCache = true,
3840
) {
3941
if (alternateTool && path.isAbsolute(alternateTool) && executableFileExists(alternateTool)) {
4042
binPathCache[toolName] = alternateTool;
4143
return alternateTool;
4244
}
4345

44-
if (binPathCache[toolName]) {
46+
// FIXIT: this cache needs to be invalidated when go.goroot or go.alternateTool is changed.
47+
if (useCache && binPathCache[toolName]) {
4548
return binPathCache[toolName];
4649
}
4750

@@ -64,7 +67,7 @@ export function getBinPathWithPreferredGopath(
6467
}
6568

6669
// Check GOROOT (go, gofmt, godoc would be found here)
67-
const pathFromGoRoot = getBinPathFromEnvVar(binname, getCurrentGoRoot(), true);
70+
const pathFromGoRoot = getBinPathFromEnvVar(binname, getGoConfig().get('goroot') || getCurrentGoRoot(), true);
6871
if (pathFromGoRoot) {
6972
binPathCache[toolName] = pathFromGoRoot;
7073
return pathFromGoRoot;

src/util.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,13 @@ export class GoVersion {
136136
}
137137
}
138138

139+
let cachedGoBinPath: string | undefined;
139140
let cachedGoVersion: GoVersion | undefined;
140141
let vendorSupport: boolean | undefined;
141142
let toolsGopath: string;
142143

143-
export function getGoConfig(uri?: vscode.Uri): vscode.WorkspaceConfiguration {
144+
// getGoConfig is declared as an exported const rather than a function, so it can be stubbbed in testing.
145+
export const getGoConfig = (uri?: vscode.Uri) => {
144146
if (!uri) {
145147
if (vscode.window.activeTextEditor) {
146148
uri = vscode.window.activeTextEditor.document.uri;
@@ -149,7 +151,7 @@ export function getGoConfig(uri?: vscode.Uri): vscode.WorkspaceConfiguration {
149151
}
150152
}
151153
return vscode.workspace.getConfiguration('go', uri);
152-
}
154+
};
153155

154156
export function byteOffsetAt(document: vscode.TextDocument, position: vscode.Position): number {
155157
const offset = document.offsetAt(position);
@@ -311,7 +313,7 @@ export async function getGoVersion(): Promise<GoVersion | undefined> {
311313
warn(`unable to locate "go" binary in GOROOT (${getCurrentGoRoot()}) or PATH (${envPath})`);
312314
return;
313315
}
314-
if (cachedGoVersion) {
316+
if (cachedGoBinPath === goRuntimePath && cachedGoVersion) {
315317
if (cachedGoVersion.isValid()) {
316318
return Promise.resolve(cachedGoVersion);
317319
}
@@ -324,6 +326,7 @@ export async function getGoVersion(): Promise<GoVersion | undefined> {
324326
warn(`failed to run "${goRuntimePath} version": stdout: ${stdout}, stderr: ${stderr}`);
325327
return;
326328
}
329+
cachedGoBinPath = goRuntimePath;
327330
cachedGoVersion = new GoVersion(goRuntimePath, stdout);
328331
} catch (err) {
329332
warn(`failed to run "${goRuntimePath} version": ${err}`);
@@ -432,14 +435,15 @@ function resolveToolsGopath(): string {
432435
}
433436
}
434437

435-
export function getBinPath(tool: string): string {
438+
export function getBinPath(tool: string, useCache = true): string {
436439
const alternateTools: { [key: string]: string } = getGoConfig().get('alternateTools');
437440
const alternateToolPath: string = alternateTools[tool];
438441

439442
return getBinPathWithPreferredGopath(
440443
tool,
441444
tool === 'go' ? [] : [getToolsGopath(), getCurrentGoPath()],
442445
resolvePath(alternateToolPath),
446+
useCache
443447
);
444448
}
445449

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Licensed under the MIT License.
3+
// See LICENSE in the project root for license information.
4+
5+
// This is a helper used to fake a go binary.
6+
// It currently fakes `go env` and `go version` commands.
7+
// For `go env`, it returns FAKEGOROOT for 'GOROOT', and an empty string for others.
8+
// For `go version`, it returns FAKEGOVERSION or a default dev version string.
9+
package main
10+
11+
import (
12+
"fmt"
13+
"os"
14+
)
15+
16+
func main() {
17+
args := os.Args
18+
19+
if len(args) <= 1 {
20+
return
21+
}
22+
switch args[1] {
23+
case "env":
24+
fakeEnv(args[2:])
25+
case "version":
26+
fakeVersion()
27+
default:
28+
fmt.Fprintf(os.Stderr, "not implemented")
29+
os.Exit(1)
30+
}
31+
os.Exit(0)
32+
}
33+
34+
func fakeEnv(args []string) {
35+
for _, a := range args {
36+
fmt.Println(os.Getenv("FAKE" + a))
37+
}
38+
}
39+
40+
func fakeVersion() {
41+
ver := os.Getenv("FAKEGOVERSION")
42+
if ver != "" {
43+
fmt.Println(ver)
44+
return
45+
}
46+
fmt.Println("go version devel +a07e2819 Thu Jun 18 20:58:26 2020 +0000 darwin/amd64")
47+
}

0 commit comments

Comments
 (0)