Skip to content

Commit

Permalink
src/goInstallTools: install dlv-dap (dev version of dlv)
Browse files Browse the repository at this point in the history
The new debug mode that uses `dlv dap` needs the dev version
of dlv (newer than the official release). This CL installs
a separate copy of delve (dlv) as `dlv-dap`, built from master.
This `dlv-dap` is used only when debugging in dlv-dap mode.

If the local version of dlv-dap is older than the hard-coded
minimum required version (latestVersion), the extension will
prompt users to update, while resolving the debug configuration.

For now, we will not query the upstream release status
(as we are currently doing for gopls).

We can also consider to provide the auto-update feature
(as we are currently doing for gopls) in the future.

Fixes #794

Change-Id: I77d4b13b8eaa69d157d7c8f94d462503c92d7e55
Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/297189
Trust: Hyang-Ah Hana Kim <hyangah@gmail.com>
Trust: Suzy Mueller <suzmue@golang.org>
Run-TryBot: Hyang-Ah Hana Kim <hyangah@gmail.com>
TryBot-Result: kokoro <noreply+kokoro@google.com>
Reviewed-by: Polina Sokolova <polina@google.com>
Reviewed-by: Suzy Mueller <suzmue@golang.org>
  • Loading branch information
hyangah committed Mar 18, 2021
1 parent d22e0f6 commit a6df4c4
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 40 deletions.
22 changes: 18 additions & 4 deletions src/goDebugConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import path = require('path');
import vscode = require('vscode');
import { getGoConfig } from './config';
import { toolExecutionEnvironment } from './goEnv';
import { promptForMissingTool } from './goInstallTools';
import { promptForMissingTool, promptForUpdatingTool, shouldUpdateTool } from './goInstallTools';
import { packagePathToGoModPathMap } from './goModules';
import { getToolAtVersion } from './goTools';
import { pickProcess, pickProcessByName } from './pickProcess';
import { getFromGlobalState, updateGlobalState } from './stateUtils';
import { getBinPath, resolvePath } from './util';
import { parseEnvFiles } from './utils/envUtils';

let dlvDAPVersionCurrent = false;

export class GoDebugConfigurationProvider implements vscode.DebugConfigurationProvider {
constructor(private defaultDebugAdapterType: string = 'go') {}

Expand Down Expand Up @@ -227,11 +230,22 @@ export class GoDebugConfigurationProvider implements vscode.DebugConfigurationPr
}
}

debugConfiguration['dlvToolPath'] = getBinPath('dlv');
if (!path.isAbsolute(debugConfiguration['dlvToolPath'])) {
promptForMissingTool('dlv');
const debugAdapter = debugConfiguration['debugAdapter'] === 'dlv-dap' ? 'dlv-dap' : 'dlv';
const dlvToolPath = getBinPath(debugAdapter);
if (!path.isAbsolute(dlvToolPath)) {
await promptForMissingTool(debugAdapter);
return;
}
debugConfiguration['dlvToolPath'] = dlvToolPath;

if (debugAdapter === 'dlv-dap' && !dlvDAPVersionCurrent) {
const tool = getToolAtVersion('dlv-dap');
if (await shouldUpdateTool(tool, dlvToolPath)) {
promptForUpdatingTool('dlv-dap');
return;
}
dlvDAPVersionCurrent = true;
}

if (debugConfiguration['mode'] === 'auto') {
debugConfiguration['mode'] =
Expand Down
72 changes: 63 additions & 9 deletions src/goInstallTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import {
getBinPath,
getBinPathWithExplanation,
getCheckForToolsUpdatesConfig,
getGoVersion,
getTempFilePath,
getWorkspaceFolderPath,
Expand Down Expand Up @@ -245,18 +246,22 @@ export async function installTool(
if (!modulesOn) {
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');
// dlv-dap or tools with a "mod" suffix can't be installed with
// simple `go install` or `go get`. We need to get, build, and rename them.
if (hasModSuffix(tool) || tool.name === 'dlv-dap') {
args.push('-d'); // get the version, but don't build.
}
let importPath: string;
if (!modulesOn) {
importPath = getImportPath(tool, goVersion);
} else {
let version = tool.version;
if (!version && tool.usePrereleaseInPreviewMode && isInPreviewMode()) {
version = await latestToolVersion(tool, true);
let version: semver.SemVer | string | undefined = tool.version;
if (!version) {
if (tool.usePrereleaseInPreviewMode && isInPreviewMode()) {
version = await latestToolVersion(tool, true);
} else if (tool.defaultVersion) {
version = tool.defaultVersion;
}
}
importPath = getImportPathWithVersion(tool, version, goVersion);
}
Expand All @@ -274,14 +279,16 @@ export async function installTool(
output = `${stdout} ${stderr}`;
logVerbose('install: %s %s\n%s%s', goBinary, args.join(' '), stdout, stderr);

if (hasModSuffix(tool)) {
// Actual installation of the -gomod tool is done by running go build.
if (hasModSuffix(tool) || tool.name === 'dlv-dap') {
// Actual installation of the -gomod tool and dlv-dap is done by running go build.
const gopath = env['GOBIN'] || env['GOPATH'];
if (!gopath) {
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);
// go build does not take @version suffix yet.
const importPath = getImportPath(tool, goVersion);
await execFile(goBinary, ['build', '-o', outputFile, importPath], opts);
}
const toolInstallPath = getBinPath(tool.name);
Expand Down Expand Up @@ -619,3 +626,50 @@ export async function latestToolVersion(tool: Tool, includePrerelease?: boolean)
}
return ret;
}

// inspectGoToolVersion reads the go version and module version
// of the given go tool using `go version -m` command.
export async function inspectGoToolVersion(binPath: string): Promise<{ goVersion?: string; moduleVersion?: string }> {
const goCmd = getBinPath('go');
const execFile = util.promisify(cp.execFile);
try {
const { stdout } = await execFile(goCmd, ['version', '-m', binPath]);
/* The output format will look like this:
/Users/hakim/go/bin/gopls: go1.16
path golang.org/x/tools/gopls
mod golang.org/x/tools/gopls v0.6.6 h1:GmCsAKZMEb1BD1BTWnQrMyx4FmNThlEsmuFiJbLBXio=
dep github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
*/
const lines = stdout.split('\n', 3);
const goVersion = lines[0].split(/\s+/)[1];
const moduleVersion = lines[2].split(/\s+/)[3];
return { goVersion, moduleVersion };
} catch (e) {
outputChannel.appendLine(
`Failed to determine the version of ${binPath}. For debugging, run "go version -m ${binPath}"`
);
// either go version failed or stdout is not in the expected format.
return {};
}
}

export async function shouldUpdateTool(tool: Tool, toolPath: string): Promise<boolean> {
if (!tool.latestVersion) {
return false;
}

const checkForUpdates = getCheckForToolsUpdatesConfig(getGoConfig());
if (checkForUpdates === 'off') {
return false;
}
const { moduleVersion } = await inspectGoToolVersion(toolPath);
if (!moduleVersion) {
return false; // failed to inspect the tool version.
}
const localVersion = semver.parse(moduleVersion, { includePrerelease: true });
return semver.lt(localVersion, tool.latestVersion);
// update only if the local version is older than the desired version.

// TODO(hyangah): figure out when to check if a version newer than
// tool.latestVersion is released when checkForUpdates === 'proxy'
}
19 changes: 18 additions & 1 deletion src/goTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export interface Tool {
// If true, consider prerelease version in preview mode
// (nightly & 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
// a dev version available in a branch (e.g. `master`).
defaultVersion?: string;

// latestVersion and latestVersionTimestamp are hardcoded default values
// for the last known version of the given tool. We also hardcode values
Expand Down Expand Up @@ -433,7 +438,19 @@ export const allToolsInformation: { [key: string]: Tool } = {
modulePath: 'github.com/go-delve/delve',
replacedByGopls: false,
isImportant: true,
description: 'Debugging'
description: 'Go debugger (Delve)'
},
'dlv-dap': {
name: 'dlv-dap',
importPath: 'github.com/go-delve/delve/cmd/dlv',
modulePath: 'github.com/go-delve/delve',
replacedByGopls: false,
isImportant: false,
description: 'Go debugger (Delve built for DAP experiment)',
defaultVersion: 'master', // Always build from the master.
minimumGoVersion: semver.coerce('1.14'), // last 3 versions per delve policy
latestVersion: semver.parse('v1.6.1-0.20210224092741-5360c6286949'),
latestVersionTimestamp: moment('2021-02-24', 'YYYY-MM-DD')
},
'fillstruct': {
name: 'fillstruct',
Expand Down
59 changes: 59 additions & 0 deletions test/gopls/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,62 @@ suite('gopls update tests', () => {
}
});
});

suite.only('version comparison', () => {
const tool = getTool('dlv-dap');
const latestVersion = tool.latestVersion;

teardown(() => {
sinon.restore();
});

async function testShouldUpdateTool(expected: boolean, moduleVersion?: string) {
sinon.stub(goInstallTools, 'inspectGoToolVersion').returns(Promise.resolve({ moduleVersion }));
assert.strictEqual(
expected,
goInstallTools.shouldUpdateTool(tool, '/bin/path/to/dlv-dap'),
`hard-coded minimum: ${tool.latestVersion.toString()} vs localVersion: ${moduleVersion}`
);
}

test('local delve is old', async () => {
testShouldUpdateTool(true, 'v1.6.0');
});

test('local delve is the minimum required version', async () => {
testShouldUpdateTool(false, 'v' + latestVersion.toString());
});

test('local delve is newer', async () => {
testShouldUpdateTool(false, `v${latestVersion.major}.${latestVersion.minor + 1}.0`);
});

test('local delve is slightly older', async () => {
testShouldUpdateTool(
true,
`v{$latestVersion.major}.${latestVersion.minor}.${latestVersion.patch}-0.20201231000000-5360c6286949`
);
});

test('local delve is slightly newer', async () => {
testShouldUpdateTool(
false,
`v{$latestVersion.major}.${latestVersion.minor}.${latestVersion.patch}-0.30211231000000-5360c6286949`
);
});

test('local delve version is unknown', async () => {
// maybe a wrapper shellscript?
testShouldUpdateTool(false, undefined);
});

test('local delve version is non-sense', async () => {
// maybe a wrapper shellscript?
testShouldUpdateTool(false, 'hello');
});

test('local delve version is non-sense again', async () => {
// maybe a wrapper shellscript?
testShouldUpdateTool(false, '');
});
});
47 changes: 21 additions & 26 deletions test/integration/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import AdmZip = require('adm-zip');
import * as assert from 'assert';
import * as config from '../../src/config';
import { toolInstallationEnvironment } from '../../src/goEnv';
import { installTools } from '../../src/goInstallTools';
import { inspectGoToolVersion, installTools } from '../../src/goInstallTools';
import { allToolsInformation, getConfiguredTools, getTool, getToolAtVersion } from '../../src/goTools';
import { getBinPath, getGoVersion, GoVersion, rmdirRecursive } from '../../src/util';
import { correctBinname } from '../../src/utils/pathUtils';
Expand Down Expand Up @@ -149,7 +149,12 @@ suite('Installation Tests', function () {
await runTest(
[
{ name: 'gopls', versions: ['v0.1.0', 'v1.0.0-pre.1', 'v1.0.0'], wantVersion: 'v1.0.0' },
{ name: 'guru', versions: ['v1.0.0'], wantVersion: 'v1.0.0' }
{ name: 'guru', versions: ['v1.0.0'], wantVersion: 'v1.0.0' },
{
name: 'dlv-dap',
versions: ['v1.0.0', 'master'],
wantVersion: 'v' + getTool('dlv-dap').latestVersion!.toString()
}
],
true
);
Expand All @@ -173,7 +178,9 @@ function buildFakeProxy(testCases: installationTestCase[]) {
const proxyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'proxydir'));
for (const tc of testCases) {
const tool = getTool(tc.name);
const module = tool.importPath;
const module = tool.modulePath;
const pathInModule =
tool.modulePath === tool.importPath ? '' : tool.importPath.slice(tool.modulePath.length + 1) + '/';
const versions = tc.versions ?? ['v1.0.0']; // hardcoded for now
const dir = path.join(proxyDir, module, '@v');
fs.mkdirSync(dir, { recursive: true });
Expand All @@ -182,6 +189,16 @@ function buildFakeProxy(testCases: installationTestCase[]) {
fs.writeFileSync(path.join(dir, 'list'), `${versions.join('\n')}\n`);

versions.map((version) => {
if (version === 'master') {
// for dlv-dap that retrieves the version from master
const resolvedVersion = tool.latestVersion?.toString() || '1.0.0';
version = `v${resolvedVersion}`;
fs.writeFileSync(
path.join(dir, 'master.info'),
`{ "Version": "${version}", "Time": "2020-04-07T14:45:07Z" } `
);
}

// Write the go.mod file.
fs.writeFileSync(path.join(dir, `${version}.mod`), `module ${module}\n`);
// Write the info file.
Expand All @@ -193,7 +210,7 @@ function buildFakeProxy(testCases: installationTestCase[]) {
// Write the zip file.
const zip = new AdmZip();
const content = 'package main; func main() {};';
zip.addFile(`${module}@${version}/main.go`, Buffer.alloc(content.length, content));
zip.addFile(`${module}@${version}/${pathInModule}main.go`, Buffer.alloc(content.length, content));
zip.writeZip(path.join(dir, `${version}.zip`));
});
}
Expand Down Expand Up @@ -233,25 +250,3 @@ suite('getConfiguredTools', () => {
function fakeGoVersion(version: string) {
return new GoVersion('/path/to/go', `go version go${version} windows/amd64`);
}

// inspectGoToolVersion reads the go version and module version
// of the given go tool using `go version -m` command.
async function inspectGoToolVersion(binPath: string): Promise<{ goVersion?: string; moduleVersion?: string }> {
const goCmd = getBinPath('go');
const execFile = util.promisify(cp.execFile);
try {
const { stdout } = await execFile(goCmd, ['version', '-m', binPath]);
/* The output format will look like this:
/Users/hakim/go/bin/gopls: go1.16
path golang.org/x/tools/gopls
mod golang.org/x/tools/gopls v0.6.6 h1:GmCsAKZMEb1BD1BTWnQrMyx4FmNThlEsmuFiJbLBXio=
dep github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
*/
const lines = stdout.split('\n', 3);
const goVersion = lines[0].split(/\s+/)[1];
const moduleVersion = lines[2].split(/\s+/)[3];
return { goVersion, moduleVersion };
} catch (e) {
return {};
}
}

0 comments on commit a6df4c4

Please sign in to comment.