import * as path from 'path' ; import { TaskDefinition , Task , TaskGroup , ShellExecution , Uri , workspace , TaskProvider , TaskScope , CustomExecution , ProcessExecution , TextEditor , Pseudoterminal , EventEmitter , Event , TerminalDimensions , window , WorkspaceFolder } from 'vscode' ; import * as os from 'os' ; import * as util from '../common' ; import * as telemetry from '../telemetry' ; import { Client } from './client' ; import * as configs from './configurations' ; import * as ext from './extension' ; import * as cp from "child_process" ; import { OtherSettings } from './settings' ; import * as nls from 'vscode-nls' ; nls . config ( { messageFormat : nls . MessageFormat . bundle , bundleFormat : nls . BundleFormat . standalone } ) ( ) ; const localize = nls . loadMessageBundle ( ) ; export interface CppBuildTaskDefinition extends TaskDefinition { type : string ; label : string ; command : string ; args : string [ ] ; options : cp . ExecOptions | cp . SpawnOptions | undefined ; } export class CppBuildTask extends Task { detail ? : string ; } export class CppBuildTaskProvider implements TaskProvider { static CppBuildScriptType : string = 'cppbuild' ; static CppBuildSourceStr : string = "C/C++" ; constructor ( ) { } public async provideTasks ( ) { return this . getTasks ( false ) ; } public resolveTask ( _task ) { const execution = _task . execution ; if ( ! execution ) { const definition = < any > _task . definition ; _task = this . getTask ( definition . command , false , definition . args ? definition . args : [ ] , definition , _task . detail ) ; return _task ; } return undefined ; } public async getTasks ( appendSourceToName ) { const editor = window . activeTextEditor ; const emptyTasks = [ ] ; if ( ! editor ) { return emptyTasks ; } const fileExt = path . extname ( editor . document . fileName ) ; if ( ! fileExt ) { return emptyTasks ; } const fileExtLower = fileExt . toLowerCase ( ) ; const isHeader = ! fileExt || [ ".cuh" , ".hpp" , ".hh" , ".hxx" , ".h++" , ".hp" , ".h" , ".ii" , ".inl" , ".idl" , "" ] . some ( ext => fileExtLower === ext ) ; if ( isHeader ) { return emptyTasks ; } let fileIsCpp ; let fileIsC ; if ( fileExt === ".C" ) { fileIsCpp = true ; fileIsC = true ; } else { fileIsCpp = [ ".cu" , ".cpp" , ".cc" , ".cxx" , ".c++" , ".cp" , ".ino" , ".ipp" , ".tcc" ] . some ( ext => fileExtLower === ext ) ; fileIsC = fileExtLower === ".c" ; } if ( ! ( fileIsCpp || fileIsC ) ) { return emptyTasks ; } const isWindows = os . platform ( ) === 'win32' ; let activeClient ; try { activeClient = ext . getActiveClient ( ) ; } catch ( errJS ) { const e = errJS as Error ; if ( ! e || e . message !== ext . intelliSenseDisabledError ) { console . error ( "Unknown error calling getActiveClient()." ) ; } return emptyTasks ; } const userCompilerPathAndArgs = await activeClient . getCurrentCompilerPathAndArgs ( ) ; let userCompilerPath ; if ( userCompilerPathAndArgs ) { userCompilerPath = userCompilerPathAndArgs . compilerPath ; if ( userCompilerPath && userCompilerPathAndArgs . compilerName ) { userCompilerPath = userCompilerPath . trim ( ) ; if ( isWindows && userCompilerPath . startsWith ( "/" ) ) { userCompilerPath = undefined ; } else { userCompilerPath = userCompilerPath . replace ( /\\\\/g , "\\" ) ; } } } const isCompilerValid = userCompilerPath ? await util . checkFileExists ( userCompilerPath ) : false ; const userCompilerIsCl = isCompilerValid && ! ! userCompilerPathAndArgs && userCompilerPathAndArgs . compilerName === "cl.exe" ; const knownCompilerPathsSet = new Set ( ) ; let knownCompilers = await activeClient . getKnownCompilers ( ) ; if ( knownCompilers ) { const compiler_condition = info => ( ( fileIsCpp && ! info . isC ) || ( fileIsC && info . isC ) ) && ( ! isCompilerValid || ( ! ! userCompilerPathAndArgs && ( path . basename ( info . path ) !== userCompilerPathAndArgs . compilerName ) ) ) && ( ! isWindows || ! info . path . startsWith ( "/" ) ) ; const cl_to_add = userCompilerIsCl ? undefined : knownCompilers . find ( info => ( ( path . basename ( info . path ) === "cl.exe" ) && compiler_condition ( info ) ) ) ; knownCompilers = knownCompilers . filter ( info => ( ( info === cl_to_add ) || ( path . basename ( info . path ) !== "cl.exe" && compiler_condition ( info ) ) ) ) ; knownCompilers . map < void > ( info => { knownCompilerPathsSet . add ( info . path ) ; } ) ; } const knownCompilerPaths = knownCompilerPathsSet . size ? Array . from ( knownCompilerPathsSet ) : undefined ; if ( ! knownCompilerPaths && ! userCompilerPath ) { telemetry . logLanguageServerEvent ( 'noCompilerFound' ) ; return emptyTasks ; } let result = [ ] ; if ( knownCompilerPaths ) { result = knownCompilerPaths . map < Task > ( compilerPath => this . getTask ( compilerPath , appendSourceToName , undefined ) ) ; } if ( isCompilerValid && userCompilerPath ) { result . push ( this . getTask ( userCompilerPath , appendSourceToName , userCompilerPathAndArgs ?. additionalArgs ) ) ; } return result ; } private getTask : ( compilerPath : string , appendSourceToName : boolean , compilerArgs ? : string [ ] , definition ? : CppBuildTaskDefinition , detail ? : string ) => Task = ( compilerPath , appendSourceToName , compilerArgs ? , definition ? , detail ? ) => { const compilerPathBase = path . basename ( compilerPath ) ; const isCl = compilerPathBase . toLowerCase ( ) === "cl.exe" ; let resolvedcompilerPath = isCl ? compilerPathBase : compilerPath ; if ( resolvedcompilerPath && ! resolvedcompilerPath . startsWith ( "\"" ) && resolvedcompilerPath . includes ( " " ) ) { resolvedcompilerPath = "\"" + resolvedcompilerPath + "\"" ; } if ( ! definition ) { const taskLabel = ( ( appendSourceToName && ! compilerPathBase . startsWith ( CppBuildTaskProvider . CppBuildSourceStr ) ) ? CppBuildTaskProvider . CppBuildSourceStr + ": " : "" ) + compilerPathBase + " " + localize ( "build_active_file" , "build active file" ) ; const filePath = path . join ( '${fileDirname}' , '${fileBasenameNoExtension}' ) ; const isWindows = os . platform ( ) === 'win32' ; let args = isCl ? [ '/Zi' , '/EHsc' , '/nologo' , '/Fe:' , filePath + '.exe' , '${file}' ] : [ '-fdiagnostics-color=always' , '-g' , '${file}' , '-o' , filePath + ( isWindows ? '.exe' : '' ) ] ; if ( compilerArgs && compilerArgs . length > 0 ) { args = args . concat ( compilerArgs ) ; } const cwd = isWindows && ! isCl && ! process . env . PATH ?. includes ( path . dirname ( compilerPath ) ) ? path . dirname ( compilerPath ) : "${fileDirname}" ; const options = { cwd : cwd } ; definition = { type : CppBuildTaskProvider . CppBuildScriptType , label : taskLabel , command : isCl ? compilerPathBase : compilerPath , args : args , options : options } ; } const editor = window . activeTextEditor ; const folder = editor ? workspace . getWorkspaceFolder ( editor . document . uri ) : undefined ; if ( folder ) { const activeClient = ext . getActiveClient ( ) ; const uri = activeClient . RootUri ; if ( ! uri ) { throw new Error ( "No client URI found in getBuildTasks()" ) ; } if ( ! workspace . getWorkspaceFolder ( uri ) ) { throw new Error ( "No target WorkspaceFolder found in getBuildTasks()" ) ; } } const taskUsesActiveFile = definition . args . some ( arg => arg . indexOf ( '${file}' ) >= 0 ) ; const scope = folder ? folder : TaskScope . Workspace ; const task = new Task ( definition , scope , definition . label , CppBuildTaskProvider . CppBuildSourceStr , new CustomExecution ( async ( resolvedDefinition ) : Promise < Pseudoterminal > => new CustomBuildTaskTerminal ( resolvedcompilerPath , resolvedDefinition . args , resolvedDefinition . options , taskUsesActiveFile ) ) , isCl ? '$msCompile' : '$gcc' ) ; task . group = TaskGroup . Build ; task . detail = detail ? detail : localize ( "compiler_details" , "compiler:" ) + " " + resolvedcompilerPath ; return task ; } ; public async getJsonTasks ( ) { const rawJson = await this . getRawTasksJson ( ) ; const rawTasksJson = ( ! rawJson . tasks ) ? new Array ( ) : rawJson . tasks ; const buildTasksJson = rawTasksJson . map ( ( task ) => { if ( ! task . label ) { return null ; } const definition = { type : task . type , label : task . label , command : task . command , args : task . args , options : task . options } ; const cppBuildTask = new Task ( definition , TaskScope . Workspace , task . label , "C/C++" ) ; cppBuildTask . detail = task . detail ; return cppBuildTask ; } ) ; return buildTasksJson . filter ( ( task ) => task !== null ) ; } public async ensureBuildTaskExists ( taskLabel ) { const rawTasksJson = await this . getRawTasksJson ( ) ; if ( ! rawTasksJson . tasks ) { rawTasksJson . tasks = new Array ( ) ; } let selectedTask = rawTasksJson . tasks . find ( ( task ) => task . label && task . label === taskLabel ) ; if ( selectedTask ) { return ; } const buildTasks = await this . getTasks ( true ) ; const normalizedLabel = ( taskLabel . indexOf ( "ver(" ) !== - 1 ) ? taskLabel . slice ( 0 , taskLabel . indexOf ( "ver(" ) ) . trim ( ) : taskLabel ; selectedTask = buildTasks . find ( task => task . name === normalizedLabel ) ; console . assert ( selectedTask ) ; if ( ! selectedTask ) { throw new Error ( "Failed to get selectedTask in ensureBuildTaskExists()" ) ; } else { selectedTask . definition . label = taskLabel ; selectedTask . name = taskLabel ; } rawTasksJson . version = "2.0.0" ; rawTasksJson . tasks . forEach ( ( task ) => { if ( task . label === selectedTask ?. definition . label ) { task . group = { kind : "build" , "isDefault" : true } ; } else if ( task . group . kind && task . group . kind === "build" && task . group . isDefault && task . group . isDefault === true ) { task . group = "build" ; } } ) ; if ( ! rawTasksJson . tasks . find ( ( task ) => task . label === selectedTask ?. definition . label ) ) { const newTask = { ... selectedTask . definition , problemMatcher : selectedTask . problemMatchers , group : { kind : "build" , "isDefault" : true } , detail : localize ( "task_generated_by_debugger" , "Task generated by Debugger." ) } ; rawTasksJson . tasks . push ( newTask ) ; } const settings = new OtherSettings ( ) ; const tasksJsonPath = this . getTasksJsonPath ( ) ; if ( ! tasksJsonPath ) { throw new Error ( "Failed to get tasksJsonPath in ensureBuildTaskExists()" ) ; } await util . writeFileText ( tasksJsonPath , JSON . stringify ( rawTasksJson , null , settings . editorTabSize ) ) ; } public async ensureDebugConfigExists ( configName ) { const launchJsonPath = this . getLaunchJsonPath ( ) ; if ( ! launchJsonPath ) { throw new Error ( "Failed to get launchJsonPath in ensureDebugConfigExists()" ) ; } const rawLaunchJson = await this . getRawLaunchJson ( ) ; if ( ! rawLaunchJson || ! rawLaunchJson . configurations ) { throw new Error ( `Configuration '${ configName }' is missing in 'launch.json'.` ) ; } const selectedConfig = rawLaunchJson . configurations . find ( ( config ) => config . name && config . name === configName ) ; if ( ! selectedConfig ) { throw new Error ( `Configuration '${ configName }' is missing in 'launch.json'.` ) ; } return ; } public provideUniqueTaskLabel ( label , buildTasksJson ) { const taskNameDictionary = { } ; buildTasksJson . forEach ( task => { taskNameDictionary [ task . definition . label ] = { } ; } ) ; let newLabel = label ; let version = 0 ; do { version = version + 1 ; newLabel = label + ` ver(${ version })` ; } while ( taskNameDictionary [ newLabel ] ) ; return newLabel ; } private getLaunchJsonPath ( ) { return util . getJsonPath ( "launch.json" ) ; } private getTasksJsonPath ( ) { return util . getJsonPath ( "tasks.json" ) ; } public getRawLaunchJson ( ) { const path = this . getLaunchJsonPath ( ) ; return util . getRawJson ( path ) ; } public getRawTasksJson ( ) { const path = this . getTasksJsonPath ( ) ; return util . getRawJson ( path ) ; } } class CustomBuildTaskTerminal implements Pseudoterminal { private writeEmitter = new EventEmitter < string > ( ) ; private closeEmitter = new EventEmitter < number > ( ) ; public get onDidWrite ( ) : Event < string > { return this . writeEmitter . event ; } public get onDidClose ( ) : Event < number > { return this . closeEmitter . event ; } private endOfLine : string = "\r\n" ; constructor ( private command , private args , private options , private taskUsesActiveFile ) { } async open ( _initialDimensions ) { if ( this . taskUsesActiveFile && ! util . fileIsCOrCppSource ( window . activeTextEditor ?. document . fileName ) ) { this . writeEmitter . fire ( localize ( "cannot.build.non.cpp" , 'Cannot build and debug because the active file is not a C or C++ source file.' ) + this . endOfLine ) ; this . closeEmitter . fire ( - 1 ) ; return ; } telemetry . logLanguageServerEvent ( "cppBuildTaskStarted" ) ; this . writeEmitter . fire ( localize ( "starting_build" , "Starting build..." ) + this . endOfLine ) ; await this . doBuild ( ) ; } close ( ) { } private async doBuild ( ) { let command = util . resolveVariables ( this . command ) ; let activeCommand = command ; this . args . forEach ( ( value , index ) => { value = util . normalizeArg ( util . resolveVariables ( value ) ) ; activeCommand = activeCommand + " " + value ; this . args [ index ] = value ; } ) ; if ( this . options ) { this . options . shell = true ; } else { this . options = { "shell" : true } ; } if ( this . options . cwd ) { this . options . cwd = util . resolveVariables ( this . options . cwd ) ; } const splitWriteEmitter = ( lines ) => { const splitLines = lines . toString ( ) . split ( /\r?\n/g ) ; for ( let i = 0 ; i < splitLines . length ; i ++ ) { let line = splitLines [ i ] ; if ( i !== splitLines . length - 1 ) { line += this . endOfLine ; } this . writeEmitter . fire ( line ) ; } } ; if ( os . platform ( ) === 'win32' ) { command = `cmd /c chcp 65001>nul && ${ command }` ; } this . writeEmitter . fire ( activeCommand + this . endOfLine ) ; let child ; try { child = cp . spawn ( command , this . args , this . options ? this . options : { } ) ; let error = "" ; let stdout = "" ; let stderr = "" ; const result = await new Promise < number > ( resolve => { if ( child ) { child . on ( 'error' , err => { splitWriteEmitter ( err . message ) ; error = err . message ; resolve ( - 1 ) ; } ) ; child . stdout ?. on ( 'data' , data => { const str = data . toString ( ) ; splitWriteEmitter ( str ) ; stdout += str ; } ) ; child . stderr ?. on ( 'data' , data => { const str = data . toString ( ) ; splitWriteEmitter ( str ) ; stderr += str ; } ) ; child . on ( 'close' , result => { this . writeEmitter . fire ( this . endOfLine ) ; if ( result === null ) { this . writeEmitter . fire ( localize ( "build.run.terminated" , "Build run was terminated." ) + this . endOfLine ) ; resolve ( - 1 ) ; } resolve ( 0 ) ; } ) ; } } ) ; this . printBuildSummary ( error , stdout , stderr ) ; this . closeEmitter . fire ( result ) ; } catch { this . closeEmitter . fire ( - 1 ) ; } } private printBuildSummary ( error , stdout , stderr ) { if ( error || ( ! stdout && stderr && stderr . includes ( "error" ) ) || ( stdout && stdout . includes ( "error C" ) ) ) { telemetry . logLanguageServerEvent ( "cppBuildTaskError" ) ; this . writeEmitter . fire ( localize ( "build.finished.with.error" , "Build finished with error(s)." ) + this . endOfLine ) ; } else if ( ( ! stdout && stderr ) || ( stdout && stdout . includes ( "warning C" ) ) ) { telemetry . logLanguageServerEvent ( "cppBuildTaskWarnings" ) ; this . writeEmitter . fire ( localize ( "build.finished.with.warnings" , "Build finished with warning(s)." ) + this . endOfLine ) ; } else { this . writeEmitter . fire ( localize ( "build.finished.successfully" , "Build finished successfully." ) + this . endOfLine ) ; } } }