/* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All Rights Reserved. * See 'LICENSE' in the project root for license information. * ------------------------------------------------------------------------------------------ */ import * as cp from "child_process"; import * as os from 'os'; import * as path from 'path'; import { CustomExecution, Disposable, Event, EventEmitter, ProcessExecution, Pseudoterminal, ShellExecution, Task, TaskDefinition, TaskEndEvent, TaskExecution, TaskGroup, TaskProvider, tasks, TaskScope, TerminalDimensions, TextEditor, window, workspace, WorkspaceFolder } from 'vscode'; import * as nls from 'vscode-nls'; import * as util from '../common'; import * as telemetry from '../telemetry'; import { logAndReturn } from "../Utility/Async/returns"; import { Client } from './client'; import * as configs from './configurations'; import { getEffectiveEnvironment, isEnvironmentOverrideApplied } from "./devcmd"; import * as ext from './extension'; import { OtherSettings } from './settings'; nls.config({ messageFormat: nls.MessageFormat.bundle, bundleFormat: nls.BundleFormat.standalone })(); const localize: nls.LocalizeFunc = nls.loadMessageBundle(); export interface CppBuildTaskDefinition extends TaskDefinition { type: string; label: string; // The label appears in tasks.json file. command: string | util.IQuotedString; args?: (string | util.IQuotedString)[]; options?: cp.ExecOptions | undefined; windows?: CppBuildTaskPlatformOverride; linux?: CppBuildTaskPlatformOverride; osx?: CppBuildTaskPlatformOverride; } interface CppBuildTaskPlatformOverride { command?: string | util.IQuotedString; args?: (string | util.IQuotedString)[]; options?: cp.ExecOptions | undefined; problemMatcher?: string | string[]; } export class CppBuildTask extends Task { existing?: boolean; isDefault?: boolean; } interface BuildOptions { taskUsesActiveFile: boolean; insertStd?: boolean; } export class CppBuildTaskProvider implements TaskProvider { static CppBuildScriptType: string = 'cppbuild'; public async provideTasks(): Promise { return this.getTasks(false); } // Resolves a task that has no [`execution`](#Task.execution) set. public resolveTask(_task: CppBuildTask): CppBuildTask | undefined { const execution: ProcessExecution | ShellExecution | CustomExecution | undefined = _task.execution; if (!execution) { const definition: CppBuildTaskDefinition = _task.definition; _task = this.getTask(definition, _task.detail); return _task; } return undefined; } public resolveInsiderTask(_task: CppBuildTask): CppBuildTask | undefined { const definition: CppBuildTaskDefinition = _task.definition; definition.label = definition.label.replace(ext.configPrefix, ""); _task = this.getTask(definition, _task.detail); return _task; } // Generate tasks to build the current file based on the user's detected compilers, the user's compilerPath setting, and the current file's extension. public async getTasks(appendSourceToName: boolean = false): Promise { const editor: TextEditor | undefined = window.activeTextEditor; const emptyTasks: CppBuildTask[] = []; if (!editor) { return emptyTasks; } const fileExt: string = path.extname(editor.document.fileName); if (!fileExt) { return emptyTasks; } // Don't offer tasks for header files. const isHeader: boolean = util.isHeaderFile(editor.document.uri); if (isHeader) { return emptyTasks; } // Don't offer tasks if the active file's extension is not a recognized C/C++ extension. const fileIsCpp: boolean = util.isCppFile(editor.document.uri); const fileIsC: boolean = util.isCFile(editor.document.uri); if (!(fileIsCpp || fileIsC)) { return emptyTasks; } // Get compiler paths. const isWindows: boolean = os.platform() === 'win32'; let activeClient: Client; try { activeClient = ext.getActiveClient(); } catch { return emptyTasks; // Language service features may be disabled. } // Get user compiler path. const userCompilerPathAndArgs: util.CompilerPathAndArgs | undefined = await activeClient.getCurrentCompilerPathAndArgs(); let userCompilerPath: string | undefined; 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: boolean = userCompilerPath ? await util.checkFileExists(userCompilerPath) : false; const userCompilerIsCl: boolean = isCompilerValid && !!userCompilerPathAndArgs && userCompilerPathAndArgs.compilerName === "cl.exe"; // Get known compiler paths. Do not include the known compiler path that is the same as user compiler path. // Filter them based on the file type to get a reduced list appropriate for the active file. // Only allow one instance of cl.exe to be included, as the user must launch VS Code using a VS command // prompt in order to build with cl.exe, so only one can apply. const knownCompilerPathsSet: Set = new Set(); let knownCompilers: configs.KnownCompiler[] | undefined = await activeClient.getKnownCompilers(); if (knownCompilers) { const compiler_condition: (info: configs.KnownCompiler) => boolean = info => ( // Filter out c compilers for cpp files and vice versa, except for cl.exe, which handles both. path.basename(info.path) === "cl.exe" || (fileIsCpp && !info.isC) || (fileIsC && info.isC) ) && ( !isCompilerValid || (!!userCompilerPathAndArgs && (path.basename(info.path) !== userCompilerPathAndArgs.compilerName)) ) && ( !isWindows || !info.path.startsWith("/") ); const cl_to_add: configs.KnownCompiler | undefined = 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.forEach(info => { knownCompilerPathsSet.add(info.path); }); } const knownCompilerPaths: string[] | undefined = knownCompilerPathsSet.size ? Array.from(knownCompilerPathsSet) : undefined; if (!knownCompilerPaths && !userCompilerPath) { // Don't prompt a message yet until we can make a data-based decision. telemetry.logLanguageServerEvent('noCompilerFound'); return emptyTasks; } // Create a build task per compiler path. const result: CppBuildTask[] = []; // Task for valid user compiler path setting. if (isCompilerValid && userCompilerPath) { result.push(this.generateTask(userCompilerPath, appendSourceToName, userCompilerPathAndArgs?.allCompilerArgs)); } // Tasks for known compiler paths. if (knownCompilerPaths) { result.push(...knownCompilerPaths.map(compilerPath => this.generateTask(compilerPath, appendSourceToName, undefined))); } return result; } private generateTask(compilerPath: string | util.IQuotedString, appendSourceToName: boolean, compilerArgs?: (string | util.IQuotedString)[]): CppBuildTask { const compilerPathString: string = util.isString(compilerPath) ? compilerPath : compilerPath.value; const compilerName: string = path.basename(compilerPathString); const isCl: boolean = compilerName.toLowerCase() === "cl.exe"; const isClang: boolean = !isCl && compilerName.toLowerCase().includes("clang"); const isWindows: boolean = os.platform() === 'win32'; const taskLabel: string = ((appendSourceToName && !compilerName.startsWith(ext.configPrefix)) ? ext.configPrefix : "") + compilerName + " " + localize("build.active.file", "build active file"); const programName: string = util.defaultExePath(); let args: (string | util.IQuotedString)[] = isCl ? ['/Zi', '/EHsc', '/nologo', `/Fe${programName}`, '${file}'] : isClang ? ['-fcolor-diagnostics', '-fansi-escape-codes', '-g', '${file}', '-o', programName] : ['-fdiagnostics-color=always', '-g', '${file}', '-o', programName]; if (compilerArgs && compilerArgs.length > 0) { args = args.concat(compilerArgs); } const cwd: string = isWindows && !isCl && !process.env.PATH?.includes(path.dirname(compilerPathString)) ? path.dirname(compilerPathString) : "${fileDirname}"; const options: cp.ExecOptions | undefined = { cwd: cwd }; const definition: CppBuildTaskDefinition = { type: CppBuildTaskProvider.CppBuildScriptType, label: taskLabel, command: isCl ? compilerName : compilerPath, args: args, options: options }; return this.getTask(definition); } private getTask(definition: CppBuildTaskDefinition, detail?: string): CppBuildTask { const platformDefinition: CppBuildTaskDefinition = this.applyPlatformOverrides(definition); const command: string = util.isString(platformDefinition.command) ? platformDefinition.command : platformDefinition.command.value; const compilerName: string = path.basename(command); const isCl: boolean = compilerName.toLowerCase() === "cl.exe"; const isClang: boolean = !isCl && compilerName.toLowerCase().includes("clang"); const editor: TextEditor | undefined = window.activeTextEditor; const folder: WorkspaceFolder | undefined = editor ? workspace.getWorkspaceFolder(editor.document.uri) : undefined; const taskUsesActiveFile: boolean = platformDefinition.args?.some(arg => { if (util.isString(arg)) { return arg.indexOf('${file}') >= 0; } return arg.value.indexOf('${file}') >= 0; }) || false; // Need to check this before ${file} is resolved const scope: WorkspaceFolder | TaskScope = folder ? folder : TaskScope.Workspace; const customExecution: CustomExecution = new CustomExecution(async (resolvedDefinition: TaskDefinition): Promise => { // When the task is executed, this callback will run. Here, we setup for running the task. // Apply platform-specific overrides (windows/linux/osx) at execution time so that VS Code // can still match the task definition by its original shape during the resolve phase. const effectiveDefinition: CppBuildTaskDefinition = this.applyPlatformOverrides(resolvedDefinition as CppBuildTaskDefinition); const effectiveArgs: (string | util.IQuotedString)[] = effectiveDefinition.args ? effectiveDefinition.args : []; return new CustomBuildTaskTerminal( effectiveDefinition.command, effectiveArgs, effectiveDefinition.options, { taskUsesActiveFile, insertStd: isClang && os.platform() === 'darwin' } ); }); const task: CppBuildTask = new CppBuildTask(definition, scope, definition.label, ext.CppSourceStr, customExecution, platformDefinition.problemMatcher ?? (isCl ? '$msCompile' : '$gcc')); task.group = TaskGroup.Build; task.detail = detail ? detail : localize("compiler.details", "compiler:") + " " + (isCl ? compilerName : command); return task; } private applyPlatformOverrides(definition: CppBuildTaskDefinition): CppBuildTaskDefinition { const platform: NodeJS.Platform = os.platform(); let platformOverride: CppBuildTaskPlatformOverride | undefined; if (platform === 'win32') { platformOverride = definition.windows; } else if (platform === 'linux') { platformOverride = definition.linux; } else if (platform === 'darwin') { platformOverride = definition.osx; } if (!platformOverride) { return definition; } const mergedDefinition: CppBuildTaskDefinition = { ...definition, command: platformOverride.command ?? definition.command, args: platformOverride.args ?? definition.args, options: platformOverride.options ?? definition.options, problemMatcher: platformOverride.problemMatcher ?? definition.problemMatcher }; return mergedDefinition; } public async getJsonTasks(): Promise { const rawJson: any = await this.getRawTasksJson(); const rawTasksJson: any = !rawJson.tasks ? [] : rawJson.tasks; const buildTasksJson: CppBuildTask[] = rawTasksJson.map((task: any) => { if (!task.label || !task.type || task.type !== CppBuildTaskProvider.CppBuildScriptType) { return null; } const definition: CppBuildTaskDefinition = { type: task.type, label: task.label, command: task.command, args: task.args, options: task.options, windows: task.windows, linux: task.linux, osx: task.osx, problemMatcher: task.problemMatcher }; const cppBuildTask: CppBuildTask = new CppBuildTask(definition, TaskScope.Workspace, task.label, ext.CppSourceStr); cppBuildTask.detail = task.detail; cppBuildTask.existing = true; if (util.isObject(task.group) && task.group.isDefault) { cppBuildTask.isDefault = true; } return cppBuildTask; }); return buildTasksJson.filter((task: CppBuildTask) => task !== null); } public async writeDefaultBuildTask(taskLabel: string, workspaceFolder?: WorkspaceFolder): Promise { return this.writeBuildTask(taskLabel, workspaceFolder, true); } public async isExistingTask(taskLabel: string, workspaceFolder?: WorkspaceFolder): Promise { const rawTasksJson: any = await this.getRawTasksJson(workspaceFolder); if (!rawTasksJson.tasks) { return false; } // Check if the task exists in the user's task.json. return rawTasksJson.tasks.find((task: any) => task.label && task.label === taskLabel); } public async writeBuildTask(taskLabel: string, workspaceFolder?: WorkspaceFolder, setAsDefault: boolean = false): Promise { const rawTasksJson: any = await this.getRawTasksJson(workspaceFolder); if (!rawTasksJson.tasks) { rawTasksJson.tasks = []; } // Check if the task exists in the user's task.json. if (rawTasksJson.tasks.find((task: any) => task.label && task.label === taskLabel)) { return; } // Create the task which should be created based on the selected "debug configuration". const buildTasks: CppBuildTask[] = await this.getTasks(true); const selectedTask: any = buildTasks.find(task => task.name === taskLabel); console.assert(selectedTask); if (!selectedTask) { throw new Error("Failed to get selectedTask in checkBuildTaskExists()"); } else { selectedTask.definition.label = taskLabel; selectedTask.name = taskLabel; } rawTasksJson.version = "2.0.0"; // If the new task should be set as the default task, modify the current default task. if (setAsDefault) { rawTasksJson.tasks.forEach((task: any) => { if (task.label === selectedTask?.definition.label) { task.group = { kind: "build", isDefault: true }; } else if (!util.isString(task.group) && task.group?.kind === "build" && task.group?.isDefault) { task.group.isDefault = false; } }); } if (!rawTasksJson.tasks.find((task: any) => task.label === selectedTask?.definition.label)) { const newTask: any = { ...selectedTask.definition, problemMatcher: selectedTask.problemMatchers, group: setAsDefault ? { kind: "build", isDefault: true } : "build", detail: localize("task.generated.by.debugger", "Task generated by Debugger.") }; rawTasksJson.tasks.push(newTask); } const settings: OtherSettings = new OtherSettings(); const tasksJsonPath: string | undefined = this.getTasksJsonPath(); if (!tasksJsonPath) { throw new Error("Failed to get tasksJsonPath in checkBuildTaskExists()"); } // Vs Code removes the comments in tasks.json, microsoft/vscode#29453 await util.writeFileText(tasksJsonPath, JSON.stringify(rawTasksJson, null, settings.editorTabSize)); } public async runBuildTask(taskLabel: string): Promise { let task: CppBuildTask | undefined; const configuredBuildTasks: CppBuildTask[] = await this.getJsonTasks(); task = configuredBuildTasks.find(task => task.name === taskLabel); if (!task) { const detectedBuildTasks: CppBuildTask[] = await this.getTasks(true); task = detectedBuildTasks.find(task => task.name === taskLabel); } if (!task) { throw new Error("Failed to find task in runBuildTask()"); } else { const resolvedTask: CppBuildTask | undefined = this.resolveInsiderTask(task); if (resolvedTask) { const execution: TaskExecution = await tasks.executeTask(resolvedTask); return new Promise((resolve) => { const disposable: Disposable = tasks.onDidEndTask((endEvent: TaskEndEvent) => { if (endEvent.execution.task.group === TaskGroup.Build && endEvent.execution === execution) { disposable.dispose(); resolve(); } }); }); } else { throw new Error("Failed to run resolved task in runBuildTask()"); } } } private getTasksJsonPath(workspaceFolder?: WorkspaceFolder): string | undefined { return util.getJsonPath("tasks.json", workspaceFolder); } public getRawTasksJson(workspaceFolder?: WorkspaceFolder): Promise { const path: string | undefined = this.getTasksJsonPath(workspaceFolder); return util.getRawJson(path); } } export const cppBuildTaskProvider: CppBuildTaskProvider = new CppBuildTaskProvider(); class CustomBuildTaskTerminal implements Pseudoterminal { private writeEmitter = new EventEmitter(); private closeEmitter = new EventEmitter(); public get onDidWrite(): Event { return this.writeEmitter.event; } public get onDidClose(): Event { return this.closeEmitter.event; } private endOfLine: string = "\r\n"; constructor(private command: string | util.IQuotedString, private args: (string | util.IQuotedString)[], private options: cp.ExecOptions | undefined, private buildOptions: BuildOptions) { } async openAsync(_initialDimensions: TerminalDimensions | undefined): Promise { if (this.buildOptions.taskUsesActiveFile && !util.isCppOrCFile(window.activeTextEditor?.document.uri)) { 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; } // TODO: Remove when compiler query work goes in and we can determine the standard version from TypeScript if (this.buildOptions.taskUsesActiveFile && window.activeTextEditor?.document.languageId === 'cpp' && this.buildOptions.insertStd) { this.args.unshift('-std=gnu++14'); } telemetry.logLanguageServerEvent("cppBuildTaskStarted"); // At this point we can start using the terminal. this.writeEmitter.fire(localize("starting.build", "Starting build...") + this.endOfLine); await this.doBuild(); } open(_initialDimensions: TerminalDimensions | undefined): void { void this.openAsync(_initialDimensions).catch(logAndReturn.undefined); } close(): void { // The terminal has been closed. Shutdown the build. } private async doBuild(): Promise { // Do build. let resolvedCommand: string | util.IQuotedString | undefined; if (util.isString(this.command)) { resolvedCommand = util.resolveVariables(this.command); } else { resolvedCommand = { value: util.resolveVariables(this.command.value), quoting: this.command.quoting }; } // Create the exe folder path if it doesn't exist. const exePath: string | undefined = util.resolveVariables(util.findExePathInArgs(this.args)); util.createDirIfNotExistsSync(exePath); this.args.forEach((value, index) => { if (util.isString(value)) { this.args[index] = util.resolveVariables(value); } else { value.value = util.resolveVariables(value.value); } }); if (this.options === undefined) { this.options = {}; } if (this.options.cwd) { this.options.cwd = util.resolveVariables(this.options.cwd.toString()); } else { const editor: TextEditor | undefined = window.activeTextEditor; let folder: WorkspaceFolder | undefined = editor ? workspace.getWorkspaceFolder(editor.document.uri) : undefined; if (!folder && workspace.workspaceFolders) { // TODO: Use the workspace folder for the tasks.json? folder = workspace.workspaceFolders[0]; } if (folder) { this.options.cwd = folder.uri.fsPath; } } if (isEnvironmentOverrideApplied()) { // If the user has applied the developer environment to this workspace, it should apply to all newly opened terminals. // However, this does not apply to processes that we spawn ourselves in the Pseudoterminal, so we need to specify the // correct environment in order to emulate the terminal behavior properly. this.options.env = getEffectiveEnvironment(); telemetry.logLanguageServerEvent('buildUsesEnvironmentOverride'); } const splitWriteEmitter = (lines: string | Buffer) => { const splitLines: string[] = lines.toString().split(/\r?\n/g); for (let i: number = 0; i < splitLines.length; i++) { let line: string = splitLines[i]; // We may not get full lines. // Only output an endOfLine when a full line is detected. if (i !== splitLines.length - 1) { line += this.endOfLine; } this.writeEmitter.fire(line); } }; const activeCommand: string = util.buildShellCommandLine(resolvedCommand, this.command, this.args); this.writeEmitter.fire(activeCommand + this.endOfLine); let child: cp.ChildProcess | undefined; try { child = cp.exec(activeCommand, this.options); let error: string = ""; let stdout: string = ""; let stderr: string = ""; const spawnResult: number = await new Promise(resolve => { if (child) { child.on('error', err => { splitWriteEmitter(err.message); error = err.message; resolve(-1); }); child.stdout?.on('data', data => { const str: string = data.toString(); splitWriteEmitter(str); stdout += str; }); child.stderr?.on('data', data => { const str: string = 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); } else { resolve(result); } }); } }); const result: number = this.printBuildSummary(error, stdout, stderr, spawnResult); this.closeEmitter.fire(result); } catch { this.closeEmitter.fire(-1); } } private printBuildSummary(error: string, stdout: string, stderr: string, spawnResult: number): number { if (spawnResult !== 0) { this.writeEmitter.fire(localize("build.finished.with.error", "Build finished with error(s).") + this.endOfLine); return -1; } if (error || (!stdout && stderr && stderr.includes("error")) || (stdout && (stdout.includes("error C") || stdout.includes("LINK : fatal error")))) { // cl.exe compiler errors this.writeEmitter.fire(localize("build.finished.with.error", "Build finished with error(s).") + this.endOfLine); return -1; } else if ((!stdout && stderr) || // gcc/clang (stdout && stdout.includes("warning C"))) { // cl.exe compiler warnings this.writeEmitter.fire(localize("build.finished.with.warnings", "Build finished with warning(s).") + this.endOfLine); return 0; } else { this.writeEmitter.fire(localize("build.finished.successfully", "Build finished successfully.") + this.endOfLine); return 0; } } }