Files
vscode-cpptools/Extension/src/LanguageServer/cppBuildTaskProvider.ts
T

422 lines
20 KiB
TypeScript

/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
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.LocalizeFunc = nls.loadMessageBundle();
export interface CppBuildTaskDefinition extends TaskDefinition {
type: string;
label: string; // The label appears in tasks.json file.
command: string;
args: string[];
options: cp.ExecOptions | 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(): Promise<CppBuildTask[]> {
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 = <any>_task.definition;
_task = this.getTask(definition.command, false, definition.args ? definition.args : [], definition, _task.detail);
return _task;
}
return undefined;
}
// 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): Promise<CppBuildTask[]> {
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 fileExtLower: string = fileExt.toLowerCase();
const isHeader: boolean = !fileExt || [".cuh", ".hpp", ".hh", ".hxx", ".h++", ".hp", ".h", ".ii", ".inl", ".idl", ""].some(ext => fileExtLower === ext);
if (isHeader) {
return emptyTasks;
}
// Don't offer tasks if the active file's extension is not a recognized C/C++ extension.
let fileIsCpp: boolean;
let fileIsC: boolean;
if (fileExt === ".C") { // ".C" file extensions are both C and 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;
}
// Get compiler paths.
const isWindows: boolean = os.platform() === 'win32';
let activeClient: Client;
try {
activeClient = ext.getActiveClient();
} catch (e) {
if (!e || e.message !== ext.intelliSenseDisabledError) {
console.error("Unknown error calling getActiveClient().");
}
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("/")) { // TODO: Add WSL compiler support.
userCompilerPath = undefined;
} else {
userCompilerPath = userCompilerPath.replace(/\\\\/g, "\\");
}
}
}
const isCompilerValid: boolean = userCompilerPath ? await util.checkFileExists(userCompilerPath) : false;
// 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.
const knownCompilerPathsSet: Set<string> = new Set();
let knownCompilers: configs.KnownCompiler[] | undefined = await activeClient.getKnownCompilers();
if (knownCompilers) {
knownCompilers = knownCompilers.filter(info =>
((fileIsCpp && !info.isC) || (fileIsC && info.isC)) &&
(!isCompilerValid || (userCompilerPathAndArgs &&
(path.basename(info.path) !== userCompilerPathAndArgs.compilerName))) &&
(!isWindows || !info.path.startsWith("/"))); // TODO: Add WSL compiler support.
knownCompilers.map<void>(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
let result: CppBuildTask[] = [];
// Tasks for known compiler paths
if (knownCompilerPaths) {
result = knownCompilerPaths.map<Task>(compilerPath => this.getTask(compilerPath, appendSourceToName, undefined));
}
// Task for valid user compiler path setting
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: string, appendSourceToName: boolean, compilerArgs?: string[], definition?: CppBuildTaskDefinition, detail?: string) => {
const compilerPathBase: string = path.basename(compilerPath);
const isCl: boolean = compilerPathBase.toLowerCase() === "cl.exe";
// Double-quote the command if it is not already double-quoted.
let resolvedcompilerPath: string = isCl ? compilerPathBase : compilerPath;
if (resolvedcompilerPath && !resolvedcompilerPath.startsWith("\"") && resolvedcompilerPath.includes(" ")) {
resolvedcompilerPath = "\"" + resolvedcompilerPath + "\"";
}
if (!definition) {
const taskLabel: string = ((appendSourceToName && !compilerPathBase.startsWith(CppBuildTaskProvider.CppBuildSourceStr)) ?
CppBuildTaskProvider.CppBuildSourceStr + ": " : "") + compilerPathBase + " " + localize("build_active_file", "build active file");
const filePath: string = path.join('${fileDirname}', '${fileBasenameNoExtension}');
const isWindows: boolean = os.platform() === 'win32';
let args: string[] = isCl ? ['/Zi', '/EHsc', '/nologo', '/Fe:', filePath + '.exe', '${file}'] : ['-g', '${file}', '-o', filePath + (isWindows ? '.exe' : '')];
if (compilerArgs && compilerArgs.length > 0) {
args = args.concat(compilerArgs);
}
const cwd: string = isWindows && !isCl && !process.env.PATH?.includes(path.dirname(compilerPath)) ? path.dirname(compilerPath) : "${fileDirname}";
const options: cp.ExecOptions | undefined = { cwd: cwd };
definition = {
type: CppBuildTaskProvider.CppBuildScriptType,
label: taskLabel,
command: isCl ? compilerPathBase : compilerPath,
args: args,
options: options
};
}
const editor: TextEditor | undefined = window.activeTextEditor;
const folder: WorkspaceFolder | undefined = editor ? workspace.getWorkspaceFolder(editor.document.uri) : undefined;
// Check uri exists (single-mode files are ignored).
if (folder) {
const activeClient: Client = ext.getActiveClient();
const uri: Uri | undefined = 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 scope: WorkspaceFolder | TaskScope = folder ? folder : TaskScope.Workspace;
const task: CppBuildTask = new Task(definition, scope, definition.label, CppBuildTaskProvider.CppBuildSourceStr,
new CustomExecution(async (resolvedDefinition: TaskDefinition): Promise<Pseudoterminal> =>
// When the task is executed, this callback will run. Here, we setup for running the task.
new CustomBuildTaskTerminal(resolvedcompilerPath, resolvedDefinition.args, resolvedDefinition.options)
), isCl ? '$msCompile' : '$gcc');
task.group = TaskGroup.Build;
task.detail = detail ? detail : localize("compiler_details", "compiler:") + " " + resolvedcompilerPath;
return task;
};
public async getJsonTasks(): Promise<CppBuildTask[]> {
const rawJson: any = await this.getRawTasksJson();
const rawTasksJson: any = (!rawJson.tasks) ? new Array() : rawJson.tasks;
const buildTasksJson: CppBuildTask[] = rawTasksJson.map((task: any) => {
if (!task.label) {
return null;
}
const definition: CppBuildTaskDefinition = {
type: task.type,
label: task.label,
command: task.command,
args: task.args,
options: task.options
};
const cppBuildTask: CppBuildTask = new Task(definition, TaskScope.Workspace, task.label, "C/C++");
cppBuildTask.detail = task.detail;
return cppBuildTask;
});
return buildTasksJson.filter((task: CppBuildTask) => task !== null);
}
public async ensureBuildTaskExists(taskLabel: string): Promise<void> {
const rawTasksJson: any = await this.getRawTasksJson();
if (!rawTasksJson.tasks) {
rawTasksJson.tasks = new Array();
}
// Ensure that the task exists in the user's task.json. Task will not be found otherwise.
let selectedTask: any = rawTasksJson.tasks.find((task: any) => task.label && task.label === taskLabel);
if (selectedTask) {
return;
}
// Create the task which should be created based on the selected "debug configuration".
const buildTasks: CppBuildTask[] = await this.getTasks(true);
const normalizedLabel: string = (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";
// Modify the current default task
rawTasksJson.tasks.forEach((task: any) => {
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: any) => task.label === selectedTask?.definition.label)) {
const newTask: any = {
...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: OtherSettings = new OtherSettings();
const tasksJsonPath: string | undefined = 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: string): Promise<void> {
const launchJsonPath: string | undefined = this.getLaunchJsonPath();
if (!launchJsonPath) {
throw new Error("Failed to get launchJsonPath in ensureDebugConfigExists()");
}
const rawLaunchJson: any = await this.getRawLaunchJson();
// Ensure that the debug configurations exists in the user's launch.json. Config will not be found otherwise.
if (!rawLaunchJson || !rawLaunchJson.configurations) {
throw new Error(`Configuration '${configName}' is missing in 'launch.json'.`);
}
const selectedConfig: any | undefined = rawLaunchJson.configurations.find((config: any) => config.name && config.name === configName);
if (!selectedConfig) {
throw new Error(`Configuration '${configName}' is missing in 'launch.json'.`);
}
return;
}
// Provide a unique name for a newly defined tasks, which is different from tasks' names in tasks.json.
public provideUniqueTaskLabel(label: string, buildTasksJson: CppBuildTask[]): string {
const taskNameDictionary: {[key: string]: any} = {};
buildTasksJson.forEach(task => {
taskNameDictionary[task.definition.label] = {};
});
let newLabel: string = label;
let version: number = 0;
do {
version = version + 1;
newLabel = label + ` ver(${version})`;
} while (taskNameDictionary[newLabel]);
return newLabel;
}
private getLaunchJsonPath(): string | undefined {
return util.getJsonPath("launch.json");
}
private getTasksJsonPath(): string | undefined {
return util.getJsonPath("tasks.json");
}
public getRawLaunchJson(): Promise<any> {
const path: string | undefined = this.getLaunchJsonPath();
return util.getRawJson(path);
}
public getRawTasksJson(): Promise<any> {
const path: string | undefined = 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: string, private args: string[], private options: cp.ExecOptions | undefined) {
}
async open(_initialDimensions: TerminalDimensions | undefined): Promise<void> {
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();
}
close(): void {
// The terminal has been closed. Shutdown the build.
}
private async doBuild(): Promise<any> {
// Do build.
let activeCommand: string = util.resolveVariables(this.command);
this.args.forEach(value => {
let temp: string = util.resolveVariables(value);
if (temp && temp.includes(" ")) {
temp = "\"" + temp + "\"";
}
activeCommand = activeCommand + " " + temp;
});
if (this.options?.cwd) {
this.options.cwd = util.resolveVariables(this.options.cwd);
}
const splitWriteEmitter = (lines: string | Buffer) => {
for (const line of lines.toString().split(/\r?\n/g)) {
this.writeEmitter.fire(line + this.endOfLine);
}
};
this.writeEmitter.fire(activeCommand + this.endOfLine);
try {
const result: number = await new Promise<number>((resolve, reject) => {
cp.exec(activeCommand, this.options, (_error, stdout, _stderr) => {
const dot: string = ".";
const is_cl: boolean = (_stderr || _stderr === '') && stdout ? true : false;
if (is_cl) {
// cl.exe, header info may not appear if /nologo is used.
if (_stderr) {
splitWriteEmitter(_stderr); // compiler header info and command line D warnings (e.g. when /MTd and /MDd are both used)
}
splitWriteEmitter(stdout); // linker header info and potentially compiler C warnings
}
if (_error) {
if (stdout) {
// cl.exe
} else if (_stderr) {
splitWriteEmitter(_stderr); // gcc/clang
} else {
splitWriteEmitter(_error.message); // e.g. command executable not found
}
telemetry.logLanguageServerEvent("cppBuildTaskError");
this.writeEmitter.fire(localize("build_finished_with_error", "Build finished with error(s)") + dot + this.endOfLine);
resolve(-1);
} else if (_stderr && !stdout) { // gcc/clang
splitWriteEmitter(_stderr);
telemetry.logLanguageServerEvent("cppBuildTaskWarnings");
this.writeEmitter.fire(localize("build_finished_with_warnings", "Build finished with warning(s)") + dot + this.endOfLine);
resolve(0);
} else if (stdout && stdout.includes("warning C")) { // cl.exe, compiler warnings
telemetry.logLanguageServerEvent("cppBuildTaskWarnings");
this.writeEmitter.fire(localize("build_finished_with_warnings", "Build finished with warning(s)") + dot + this.endOfLine);
resolve(0);
} else {
this.writeEmitter.fire(localize("build finished successfully", "Build finished successfully.") + this.endOfLine);
resolve(0);
}
});
});
this.closeEmitter.fire(result);
} catch {
this.closeEmitter.fire(-1);
}
}
}