UNPKG

@specs-feup/clava

Version:

A C/C++ source-to-source compiler written in Typescript

470 lines (382 loc) 12.6 kB
import Io from "@specs-feup/lara/api/lara/Io.js"; import Platforms from "@specs-feup/lara/api/lara/Platforms.js"; import { arrayFromArgs, debug, debugObject, } from "@specs-feup/lara/api/lara/core/LaraCore.js"; import { JavaClasses } from "@specs-feup/lara/api/lara/util/JavaTypes.js"; import ProcessExecutor from "@specs-feup/lara/api/lara/util/ProcessExecutor.js"; import { FileJp } from "../../Joinpoints.js"; import Clava from "../Clava.js"; import CMakerSources from "./CMakerSources.js"; import CMakerUtils from "./CMakerUtils.js"; import CMakeCompiler from "./compilers/CMakeCompiler.js"; import BenchmarkCompilationEngine from "@specs-feup/lara/api/lara/benchmark/BenchmarkCompilationEngine.js"; /** * Builds CMake configurations. * * @example * new Cmaker() * .setMinimumVersion("3.0.2") // Could have a standard minimum version * .addSources(Io.getPaths(srcFolder, "*.cpp")) * .addCxxFlags("-O3", "-std=c++11") * .addLibs("stdc++") * .addIncludes(srcFolder); * .setVariable(name, value) * cmaker.getCMake() * cmaker.build(Io.getPath(srcFolder, "build")); */ export default class CMaker extends BenchmarkCompilationEngine { private static MINIMUM_VERSION: string = "3.10"; private static EXE_VAR: string = "EXE_NAME"; private static DEFAULT_BIN_FOLDER: string = "bin"; makeCommand: string = "make"; generator: string | undefined = undefined; minimumVersion = CMaker.MINIMUM_VERSION; cxxFlags: string[] = []; cFlags: string[] = []; libs: string[] = []; sources: CMakerSources; includeFolders: Set<string> = new Set(); printToConsole: boolean = false; lastMakeOutput: ProcessExecutor | undefined = undefined; compiler: CMakeCompiler | undefined = undefined; customCMakeCode: string | undefined = undefined; constructor( name: string = "cmaker-project", disableWeaving: boolean = false ) { super(name, disableWeaving); this.sources = new CMakerSources(this.disableWeaving); } /** * Custom CMake code that will be appended to the end of the CMake file. * * @param customCMakeCode - The code to append at the end of the CMake file. */ setCustomCMakeCode(customCMakeCode: string) { this.customCMakeCode = customCMakeCode; return this; } copy() { const newCmaker = new CMaker(this.toolName, this.disableWeaving); newCmaker.makeCommand = this.makeCommand; newCmaker.generator = this.generator; newCmaker.minimumVersion = this.minimumVersion; newCmaker.cxxFlags = this.cxxFlags.slice(); newCmaker.cFlags = this.cFlags.slice(); newCmaker.libs = this.libs.slice(); newCmaker.sources = this.sources.copy(); newCmaker.includeFolders = structuredClone(this.includeFolders); newCmaker.printToConsole = this.printToConsole; newCmaker.lastMakeOutput = this.lastMakeOutput; newCmaker.compiler = this.compiler; return newCmaker; } /** * @returns Object that can be used to specify the sources. */ getSources(): CMakerSources { return this.sources; } /** * Sets the minimum version of the CMake file. * * @param version - String with minimum CMake version */ setMinimumVersion(version: string) { this.minimumVersion = version; return this; } /** * Sets the name of the executable. * * @param name - String with the name of the executable. */ setName(name: string) { this.toolName = name; return this; } setCompiler( compiler: Parameters<typeof CMakerUtils.getCompiler>[0] | CMakeCompiler ) { if (typeof compiler === "string") { this.compiler = CMakerUtils.getCompiler(compiler); } else { this.compiler = compiler; } return this; } /** * Sets if output of external tools (e.g., cmake, make) should appear in the console. By default it is off. */ setPrintToolsOutput(printToolsOutput: boolean = false) { this.printToConsole = printToolsOutput; return this; } setGenerator(generator: string) { this.generator = generator; return this; } setMakeCommand(makeCommand: string) { this.makeCommand = makeCommand; return this; } /** * Adds a variable number of Strings, one for each flag. * */ addCxxFlags(...args: string[]) { const flags = arrayFromArgs(args) as string[]; for (const flag of flags) { this.cxxFlags.push(flag); } return this; } /** * Adds a variable number of Strings, one for each flag. * */ addCFlags(...args: string[]) { const flags = arrayFromArgs(args) as string[]; for (const flag of flags) { this.cFlags.push(flag); } return this; } addFlags(...args: string[]) { const flags = arrayFromArgs(args) as string[]; this.addCFlags(...flags); this.addCxxFlags(...flags); return this; } /** * Adds link-time libraries (e.g., m for math.h). * * @param arguments - a sequence of String with the name of the link-time libraries, as CMake would accept (e.g., "m"). */ addLibs(...args: string[]) { const libs = arrayFromArgs(args) as string[]; this.libs.push(...libs); return this; } getMakeOutput(): string | undefined { if (this.lastMakeOutput === undefined) { console.log("CMaker.getMakeOutput: there is not make output yet"); return undefined; } return this.lastMakeOutput.getConsoleOutput(); } /** * @param includeFolder - String representing an include folder */ addIncludeFolder(includeFolder: string | JavaClasses.File) { const parsedFolder = CMakerUtils.parsePath(includeFolder); this.includeFolders.add(parsedFolder); return this; } addCurrentAst() { for (const userInclude of Clava.getData().getUserIncludes()) { console.log("[" + this.toolName + "] Adding include: " + userInclude); this.addIncludeFolder(userInclude); } // Write current version of the files to a temporary folder and add them const currentAstFolder = Io.getPath( Io.getTempFolder(), "tempfolder_current_ast" ); // Clean folder Io.deleteFolderContents(currentAstFolder); // Create and populate source folder const srcFolder = Io.getPath(currentAstFolder, "src"); for (const $jp of Clava.getProgram().getDescendants("file")) { const $file = $jp as FileJp; const destFolder = srcFolder; const filepath = $file.write(destFolder.toString()); if (!$file.isHeader) { this.getSources().addSource(filepath); } } // Add src folder as include this.addIncludeFolder(srcFolder); return this; } /** * @returns The name of the executable that will be generated */ private getExecutableName() { let executable = this.toolName; if (Platforms.isWindows()) { executable += ".exe"; } return executable; } /** * Builds the program currently defined in the CMaker object. * * @param cmakelistsFolder - The folder where the CMakeList files will be written * @param builderFolder - The folder where the program will be built * @param cmakeFlags - Additional flags that will be passed to CMake execution * * @returns File to the executable compiled by the build. */ build( cmakelistsFolder: string | JavaClasses.File = Io.newRandomFolder(), builderFolder: string | JavaClasses.File = Io.getPath( cmakelistsFolder, "build" ), cmakeFlags?: string ): JavaClasses.File | undefined { // Generate CMakeLists.txt const cmakeFile = Io.getPath(cmakelistsFolder, "CMakeLists.txt"); Io.writeFile(cmakeFile, this.getCode()); const builderFolderpath = Io.mkdir(builderFolder).getAbsolutePath(); // Execute CMake let cmakeCmd = [ "cmake", `"${cmakeFile.getParentFile().getAbsolutePath()}"`, ]; if (cmakeFlags !== undefined) { cmakeCmd.push(cmakeFlags); } if (this.generator !== undefined) { cmakeCmd.push(`-G`); cmakeCmd.push(`"${this.generator}"`); } if (this.compiler !== undefined) { cmakeCmd.push(this.compiler.getCommandArgs()); } debug( () => `Executing CMake, calling '${cmakeCmd.join(" ")}' at '${builderFolderpath}'` ); const cmakeOutput = new ProcessExecutor(); cmakeOutput .setPrintToConsole(this.printToConsole) .setWorkingDir(builderFolderpath) .execute(...cmakeCmd); const consoleOutput = cmakeOutput.getConsoleOutput(); if (cmakeOutput.getReturnValue() === 0 && consoleOutput != undefined) { debug("CMake output:"); debug(consoleOutput); } else { console.log( "Cmaker.build: Could not generate makefile\n" + consoleOutput ); return; } // Execute make debug( `Building, calling '${this.makeCommand}' at ' ${builderFolderpath} '` ); this.lastMakeOutput = new ProcessExecutor(); this.lastMakeOutput .setPrintToConsole(this.printToConsole) .setWorkingDir(builderFolderpath) .execute(this.makeCommand); debug("Make output:"); debugObject(this.lastMakeOutput.getConsoleOutput()); const binPath = Io.getPath(builderFolderpath, CMaker.DEFAULT_BIN_FOLDER); const executableFile = Io.getPath(binPath, this.getExecutableName()); if (!Io.isFile(executableFile)) { console.log( `Cmaker.build: Could not generate executable file '${this.getExecutableName()}', expected to be in the path '${executableFile.getAbsolutePath()}'` ); return; } return executableFile; } /*** CODE FUNCTIONS ***/ /** * @returns The CMake corresponding to the current configuration */ getCode() { let code = ""; // Minimum version code += "cmake_minimum_required(VERSION " + this.minimumVersion + ")\n"; // Project code += "project(" + this.toolName + ")\n\n"; // Executable code += "set (" + CMaker.EXE_VAR + ' "' + this.toolName + '")\n\n'; // Flags code += this.getCxxFlagsCode(); code += this.getCFlagsCode(); // Directories code += this.getProjectDirectoriesCode(); // Sources code += this.sources.getCode() + "\n\n"; // Include folders code += this.getIncludeFoldersCode(); // Executable code += "add_executable(${" + CMaker.EXE_VAR + "} "; code += "${" + this.sources.getSourceVariables().join("} ${") + "}"; code += ")\n\n"; // Libs this.addLibs(...Clava.getProgram().extraLibs); code += "target_link_libraries(${" + CMaker.EXE_VAR + "} "; code += '"' + this.libs.join('" "') + '")\n\n'; // binary directories code += ` set_target_properties(\${EXE_NAME} PROPERTIES ARCHIVE_OUTPUT_DIRECTORY "\${CMAKE_BINARY_DIR}/${CMaker.DEFAULT_BIN_FOLDER}" LIBRARY_OUTPUT_DIRECTORY "\${CMAKE_BINARY_DIR}/${CMaker.DEFAULT_BIN_FOLDER}" RUNTIME_OUTPUT_DIRECTORY "\${CMAKE_BINARY_DIR}/${CMaker.DEFAULT_BIN_FOLDER}" ) `; // Custom code if (this.customCMakeCode !== undefined) { code += this.customCMakeCode; } return code; } private getProjectDirectoriesCode() { const subDirs = Clava.getProgram().extraProjects; return subDirs.reduce((acc, subDir) => { return acc + `add_subdirectory("${subDir}" "${subDir}/bin")\n`; }, ""); } private getCxxFlagsCode() { if (this.cxxFlags.length === 0) { return ""; } return `set (CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} ${this.cxxFlags.join( " " )}")\n\n`; } private getCFlagsCode() { if (this.cFlags.length === 0) { return ""; } return `set (CMAKE_C_FLAGS "\${CMAKE_C_FLAGS} ${this.cFlags.join( " " )}")\n\n`; } private getIncludeFoldersCode() { // Add user includes const includes = Array.from(this.includeFolders.values()); // Add external includes if weaving is not disabled if (!this.disableWeaving) { this.addExtraIncludes(includes); } if (includes.length === 0) { return ""; } return `include_directories("${includes.join('" "')}")\n\n`; } private addExtraIncludes(includes: string[]) { const extraIncludes = Clava.getProgram().extraIncludes; for (const extraInclude of extraIncludes) { if (Io.isFolder(extraInclude)) { debug(`[CMAKER] Adding external include '${extraInclude}'`); includes.push(CMakerUtils.parsePath(extraInclude)); } else { console.log( `[CMAKER] Extra include ' ${extraInclude} ' is not a folder` ); } } } }