From a6df4c44efb47e0e6ee8cf1b96eb1ade160f1211 Mon Sep 17 00:00:00 2001 From: Hana Date: Fri, 26 Feb 2021 18:13:41 -0500 Subject: [PATCH] src/goInstallTools: install dlv-dap (dev version of dlv) 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 golang/vscode-go#794 Change-Id: I77d4b13b8eaa69d157d7c8f94d462503c92d7e55 Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/297189 Trust: Hyang-Ah Hana Kim Trust: Suzy Mueller Run-TryBot: Hyang-Ah Hana Kim TryBot-Result: kokoro Reviewed-by: Polina Sokolova Reviewed-by: Suzy Mueller --- src/goDebugConfiguration.ts | 22 ++++++++-- src/goInstallTools.ts | 72 ++++++++++++++++++++++++++++---- src/goTools.ts | 19 ++++++++- test/gopls/update.test.ts | 59 ++++++++++++++++++++++++++ test/integration/install.test.ts | 47 ++++++++++----------- 5 files changed, 179 insertions(+), 40 deletions(-) diff --git a/src/goDebugConfiguration.ts b/src/goDebugConfiguration.ts index f87e3954c6..6d5fa13a21 100644 --- a/src/goDebugConfiguration.ts +++ b/src/goDebugConfiguration.ts @@ -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') {} @@ -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'] = diff --git a/src/goInstallTools.ts b/src/goInstallTools.ts index cd7652da36..9d37f52f9f 100644 --- a/src/goInstallTools.ts +++ b/src/goInstallTools.ts @@ -32,6 +32,7 @@ import { import { getBinPath, getBinPathWithExplanation, + getCheckForToolsUpdatesConfig, getGoVersion, getTempFilePath, getWorkspaceFolderPath, @@ -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); } @@ -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); @@ -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 { + 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' +} diff --git a/src/goTools.ts b/src/goTools.ts index bc9bc77b4b..d6708eb700 100644 --- a/src/goTools.ts +++ b/src/goTools.ts @@ -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 @@ -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', diff --git a/test/gopls/update.test.ts b/test/gopls/update.test.ts index ca6c3afcda..bf8f0950c1 100644 --- a/test/gopls/update.test.ts +++ b/test/gopls/update.test.ts @@ -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, ''); + }); +}); diff --git a/test/integration/install.test.ts b/test/integration/install.test.ts index 22ec7686f9..a4375d6f6e 100644 --- a/test/integration/install.test.ts +++ b/test/integration/install.test.ts @@ -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'; @@ -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 ); @@ -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 }); @@ -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. @@ -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`)); }); } @@ -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 {}; - } -}