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 {}; - } -}