@specs-feup/clava
Version:
A C/C++ source-to-source compiler written in Typescript
470 lines (382 loc) • 12.6 kB
text/typescript
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`
);
}
}
}
}