'use strict' ; import cp = require ( 'child_process' ) ; import fs = require ( 'fs' ) ; import path = require ( 'path' ) ; import { SemVer } from 'semver' ; import util = require ( 'util' ) ; import vscode = require ( 'vscode' ) ; import { toolInstallationEnvironment } from './goEnv' ; import { envPath , getCurrentGoRoot , getToolFromToolPath , setCurrentGoRoot } from './goPath' ; import { outputChannel } from './gopStatus' ; import { containsTool , disableModulesForWildcard , getConfiguredTools , getImportPath , getImportPathWithVersion , getTool , hasModSuffix , Tool , ToolAtVersion , } from './goTools' ; import { getBinPath , getGoPlusConfig , getGoVersion , getTempFilePath , GoVersion , rmdirRecursive , } from './util' ; const declinedUpdates = [ ] ; const declinedInstalls = [ ] ; export async function installAllTools ( updateExistingToolsOnly = false ) { const goVersion = await getGoVersion ( ) ; let allTools = getConfiguredTools ( ) ; const alternateTools = getGoPlusConfig ( ) . get ( 'alternateTools' ) ; allTools = allTools . filter ( ( tool ) => { return ! alternateTools [ tool . name ] ; } ) ; if ( updateExistingToolsOnly ) { await installTools ( allTools . filter ( ( tool ) => { const toolPath = getBinPath ( tool . name ) ; return toolPath && path . isAbsolute ( toolPath ) ; } ) , goVersion ) ; return ; } const selected = await vscode . window . showQuickPick ( allTools . map ( ( x ) => { const item = { label : x . name , description : x . description } ; return item ; } ) , { canPickMany : true , placeHolder : 'Select the tools to install/update.' } ) ; if ( ! selected ) { return ; } await installTools ( selected . map ( ( x ) => getTool ( x . label ) ) , goVersion ) ; } param missing param goVersion export async function installTools ( missing , goVersion ) { if ( ! missing ) { return ; } outputChannel . show ( ) ; outputChannel . clear ( ) ; const envForTools = toolInstallationEnvironment ( ) ; const toolsGopath = envForTools [ 'GOPATH' ] ; let envMsg = `Tools environment: GOPATH=${ toolsGopath }` ; if ( envForTools [ 'GOBIN' ] ) { envMsg += `, GOBIN=${ envForTools [ 'GOBIN' ] }` ; } outputChannel . appendLine ( envMsg ) ; let installingMsg = `Installing ${ missing . length } ${ missing . length > 1 ? 'tools' : 'tool' } at ` ; if ( envForTools [ 'GOBIN' ] ) { installingMsg += `the configured GOBIN: ${ envForTools [ 'GOBIN' ] }` ; } else { installingMsg += `${ toolsGopath }${ path . sep }bin` ; } let modulesOff = true ; outputChannel . appendLine ( installingMsg ) ; missing . forEach ( ( missingTool ) => { let toolName = missingTool . name ; if ( missingTool . version ) { toolName += '@' + missingTool . version ; } outputChannel . appendLine ( '  ' + toolName ) ; } ) ; outputChannel . appendLine ( '' ) ; const toInstall = [ ] ; for ( const tool of missing ) { const modulesOffForTool = modulesOff || disableModulesForWildcard ( tool , goVersion ) ; const reason = installTool ( tool , goVersion , envForTools , ! modulesOffForTool ) ; toInstall . push ( Promise . resolve ( { tool , reason : await reason } ) ) ; } const results = await Promise . all ( toInstall ) ; const failures = [ ] ; for ( const result of results ) { if ( result . reason != "" ) { failures . push ( result ) ; } } outputChannel . appendLine ( '' ) ; if ( failures . length === 0 ) { outputChannel . appendLine ( 'All tools successfully installed. You are ready to Go :).' ) ; } else { outputChannel . appendLine ( failures . length + ' tools failed to install.\n' ) ; for ( const failure of failures ) { outputChannel . appendLine ( `${ failure . tool . name }: ${ failure . reason } ` ) ; } } } export async function installTool ( tool , goVersion , envForTools , modulesOn ) { if ( tool . close ) { const reason = await tool . close ( ) ; if ( reason ) { return reason ; } } const mkdtemp = util . promisify ( fs . mkdtemp ) ; const toolsTmpDir = await mkdtemp ( getTempFilePath ( 'go-tools-' ) ) ; const env = Object . assign ( { } , envForTools ) ; let tmpGoModFile ; if ( modulesOn ) { env [ 'GO111MODULE' ] = 'on' ; tmpGoModFile = path . join ( toolsTmpDir , 'go.mod' ) ; const writeFile = util . promisify ( fs . writeFile ) ; await writeFile ( tmpGoModFile , 'module tools' ) ; } else { envForTools [ 'GO111MODULE' ] = 'off' ; } const args = [ 'get' , '-v' ] ; if ( ! modulesOn ) { args . push ( '-u' ) ; } if ( hasModSuffix ( tool ) ) { args . push ( '-d' ) ; } let importPath ; if ( ! modulesOn ) { importPath = getImportPath ( tool ) ; } else { importPath = getImportPathWithVersion ( tool , tool . version ) ; } args . push ( importPath ) ; let output ; let result = '' ; try { const opts = { env , cwd : toolsTmpDir , } ; const execFile = util . promisify ( cp . execFile ) ; const { stdout , stderr } = await execFile ( goVersion . binaryPath , args , opts ) ; output = `${ stdout } ${ stderr }` ; if ( stderr . indexOf ( 'unexpected directory layout:' ) > - 1 ) { await execFile ( goVersion . binaryPath , args , opts ) ; } else if ( hasModSuffix ( tool ) ) { const gopath = env [ 'GOPATH' ] ; if ( ! gopath ) { return `GOPATH not configured in environment` ; } const outputFile = path . join ( gopath , 'bin' , process . platform === 'win32' ? `${ tool . name }.exe` : tool . name ) ; await execFile ( goVersion . binaryPath , [ 'build' , '-o' , outputFile , importPath ] , opts ) ; } outputChannel . appendLine ( `Installing ${ importPath } SUCCEEDED` ) ; } catch ( e ) { outputChannel . appendLine ( `Installing ${ importPath } FAILED` ) ; result = `failed to install ${ tool }: ${ e } ${ output } ` ; } rmdirRecursive ( toolsTmpDir ) ; return result ; } export async function promptForMissingTool ( toolName ) { const tool = getTool ( toolName ) ; if ( containsTool ( declinedInstalls , tool ) ) { return ; } const goVersion = await getGoVersion ( ) ; if ( ! goVersion ) { return ; } if ( tool . minimumGoVersion && goVersion . lt ( tool . minimumGoVersion . format ( ) ) ) { vscode . window . showInformationMessage ( `You are using go${ goVersion . format ( ) }, but ${ tool . name } requires at least go${ tool . minimumGoVersion . format ( ) }.` ) ; return ; } if ( tool . maximumGoVersion && goVersion . gt ( tool . maximumGoVersion . format ( ) ) ) { vscode . window . showInformationMessage ( `You are using go${ goVersion . format ( ) }, but ${ tool . name } only supports go${ tool . maximumGoVersion . format ( ) } and below.` ) ; return ; } const installOptions = [ 'Install' ] ; let missing = await getMissingTools ( ) ; if ( ! containsTool ( missing , tool ) ) { return ; } missing = missing . filter ( ( x ) => x === tool || tool . isImportant ) ; if ( missing . length > 1 ) { installOptions . push ( 'Install All' ) ; } const msg = `The "${ tool . name }" command is not available.
Run "go get -v ${ getImportPath ( tool ) }" to install.` ; const selected = await vscode . window . showInformationMessage ( msg , ... installOptions ) ; switch ( selected ) { case 'Install' : await installTools ( [ tool ] , goVersion ) ; break ; case 'Install All' : await installTools ( missing , goVersion ) ; break ; default : declinedInstalls . push ( tool ) ; break ; } } export async function promptForUpdatingTool ( toolName , newVersion ? ) { const tool = getTool ( toolName ) ; const toolVersion = { ... tool , version : newVersion } ; if ( containsTool ( declinedUpdates , tool ) ) { return ; } const goVersion = await getGoVersion ( ) ; let updateMsg = `Your version of ${ tool . name } appears to be out of date. Please update for an improved experience.` ; const choices = [ 'Update' ] ; if ( toolName === `gopls` ) { choices . push ( 'Release Notes' ) ; } if ( newVersion ) { updateMsg = `A new version of ${ tool . name } (v${ newVersion }) is available. Please update for an improved experience.` ; } const selected = await vscode . window . showInformationMessage ( updateMsg , ... choices ) ; switch ( selected ) { case 'Update' : await installTools ( [ toolVersion ] , goVersion ) ; break ; case 'Release Notes' : vscode . commands . executeCommand ( 'vscode.open' , vscode . Uri . parse ( 'https://github.com/golang/go/issues/33030#issuecomment-510151934' ) ) ; break ; default : declinedUpdates . push ( tool ) ; break ; } } export function updateGoVarsFromConfig ( ) { if ( process . env [ 'GOPATH' ] && process . env [ 'GOPROXY' ] && process . env [ 'GOBIN' ] ) { return Promise . resolve ( ) ; } const goRuntimePath = getBinPath ( 'go' , false ) ; if ( ! goRuntimePath ) { vscode . window . showErrorMessage ( `Failed to run "go env" to find GOPATH as the "go" binary cannot be found in either GOROOT(${ getCurrentGoRoot ( ) }) or PATH(${ envPath })` ) ; return ; } return new Promise < void > ( ( resolve , reject ) => { cp . execFile ( goRuntimePath , [ 'env' , 'GOPATH' , 'GOROOT' , 'GOPROXY' , 'GOBIN' ] , ( err , stdout , stderr ) => { if ( err ) { return reject ( ) ; } const envOutput = stdout . split ( '\n' ) ; if ( ! process . env [ 'GOPATH' ] && envOutput [ 0 ] . trim ( ) ) { process . env [ 'GOPATH' ] = envOutput [ 0 ] . trim ( ) ; } if ( envOutput [ 1 ] && envOutput [ 1 ] . trim ( ) ) { setCurrentGoRoot ( envOutput [ 1 ] . trim ( ) ) ; } if ( ! process . env [ 'GOPROXY' ] && envOutput [ 2 ] && envOutput [ 2 ] . trim ( ) ) { process . env [ 'GOPROXY' ] = envOutput [ 2 ] . trim ( ) ; } if ( ! process . env [ 'GOBIN' ] && envOutput [ 3 ] && envOutput [ 3 ] . trim ( ) ) { process . env [ 'GOBIN' ] = envOutput [ 3 ] . trim ( ) ; } addGoRuntimeBaseToPATH ( path . join ( getCurrentGoRoot ( ) , 'bin' ) ) ; return resolve ( ) ; } ) ; } ) ; } let defaultPathEnv = '' ; function addGoRuntimeBaseToPATH ( newGoRuntimeBase ) { if ( ! newGoRuntimeBase ) { return ; } let pathEnvVar ; if ( process . env . hasOwnProperty ( 'PATH' ) ) { pathEnvVar = 'PATH' ; } else if ( process . platform === 'win32' && process . env . hasOwnProperty ( 'Path' ) ) { pathEnvVar = 'Path' ; } else { return ; } if ( ! defaultPathEnv ) { defaultPathEnv = < string > process . env [ pathEnvVar ] ; } let pathVars = defaultPathEnv . split ( path . delimiter ) ; pathVars = pathVars . filter ( ( p ) => p !== newGoRuntimeBase ) ; pathVars . unshift ( newGoRuntimeBase ) ; process . env [ pathEnvVar ] = pathVars . join ( path . delimiter ) ; } let alreadyOfferedToInstallTools = false ; export async function offerToInstallTools ( ) { if ( alreadyOfferedToInstallTools ) { return ; } alreadyOfferedToInstallTools = true ; const goVersion = await getGoVersion ( ) ; let missing = await getMissingTools ( ) ; missing = missing . filter ( ( x ) => x . isImportant ) ; if ( missing . length > 0 ) { vscode . commands . registerCommand ( 'go.promptforinstall' , ( ) => { const installItem = { title : 'Install' , async command ( ) { await installTools ( missing , goVersion ) ; } } ; const showItem = { title : 'Show' , command ( ) { outputChannel . clear ( ) ; outputChannel . appendLine ( 'Below tools are needed for the basic features of the Go extension.' ) ; missing . forEach ( ( x ) => outputChannel . appendLine ( x . name ) ) ; } } ; vscode . window . showInformationMessage ( 'Failed to find some of the Go analysis tools. Would you like to install them?' , installItem , showItem ) . then ( ( selection ) => { if ( selection ) { selection . command ( ) ; } } ) ; } ) ; } } function getMissingTools ( ) { const keys = getConfiguredTools ( ) ; return Promise . all < Tool > ( keys . map ( ( tool ) => new Promise < Tool > ( ( resolve , reject ) => { const toolPath = getBinPath ( tool . name ) ; resolve ( path . isAbsolute ( toolPath ) ? null : tool ) ; } ) ) ) . then ( ( res ) => { return res . filter ( ( x ) => x != null ) ; } ) ; } export async function getActiveGoRoot ( ) { let goroot = getCurrentGoRoot ( ) ; if ( ! goroot ) { await updateGoVarsFromConfig ( ) ; goroot = getCurrentGoRoot ( ) ; } return goroot || undefined ; }