@specs-feup/clava
Version:
A C/C++ source-to-source compiler written in Typescript
174 lines (157 loc) • 5.6 kB
text/typescript
import fs from "fs";
import path from "path";
import Sandbox from "../Sandbox.js";
import { fileURLToPath } from "url";
export default class ClangPlugin {
static clangPluginDir = "clang-plugin-binaries";
static pluginNamePrefix = "ClangASTDumper";
/**
* Validate that the executable provided is indeed a clang executable
*
* **Sanitize the input before calling this method**
*
* @param executableName - String containing the command the user uses to compile their code normally
* @returns String containing the clang executable name
*/
static validateClangExecutable(executableName: string): Promise<void> {
return new Promise((resolve, reject) => {
const commandRegex = /(?:^|\s)(\S*clang\S*)(?=\s|$)/;
const clangExecutable = commandRegex.exec(executableName);
if (clangExecutable) {
resolve();
} else {
reject(new Error("Could not find clang executable"));
}
});
}
/**
* Gets the clang version number from a clang executable
*
* **Sanitize the input before calling this method**
*
* @param clangExecutable - String containing the clang executable name
* @returns String containing the clang version number (e.g. 14.0.0)
*/
static getClangVersionNumberFromExecutable(
clangExecutable: string
): Promise<string> {
return new Promise((resolve, reject) => {
try {
const output = Sandbox.executeSandboxedCommand(clangExecutable, [
"--version",
]);
const versionRegex = /clang version (\d+\.\d+\.\d+)/;
const version = versionRegex.exec(output);
if (version) {
resolve(version[1]);
} else {
reject(new Error("Could not find clang version"));
}
} catch (error) {
reject(error);
}
});
}
/**
* Gets the clang version number from a compiler executable
*
* **Sanitize the input before calling this method**
*
* @param executableName - String containing the executable the user uses to compile their code normally
* @returns String containing the clang version number (e.g. 14.0.0)
*/
static async getClangVersion(executableName: string): Promise<string> {
await this.validateClangExecutable(executableName);
return this.getClangVersionNumberFromExecutable(executableName);
}
/**
* Searches the 'clang-plugin-binaries' directory for available clang plugins
* and returns a map of the available plugins.
*
* @returns Map of available clang plugins
*/
static getAvailablePlugins(): Promise<{ [version: string]: string }> {
return new Promise((resolve, reject) => {
const basedir = path.dirname(
path.dirname(path.dirname(fileURLToPath(import.meta.url)))
);
const dir = path.join(basedir, this.clangPluginDir);
const regexPattern = new RegExp(
`${this.pluginNamePrefix}_(\\d+\\.\\d+\\.\\d+)\\.so`
);
if (!fs.existsSync(dir)) {
reject(new Error("Could not find 'clang-plugin-binaries' directory"));
}
const files = fs.readdirSync(dir);
const map: { [version: string]: string } = {};
for (const file of files) {
const match = file.match(regexPattern);
if (match) {
const version = match[1];
map[version] = path.join(dir, file);
}
}
resolve(map);
});
}
/**
* Gets the absolute path to the clang plugin for a compiler
*
* @param executableName - Name of the compiler executable
* @returns The absolute path to the clang plugin or throws an error if no compatible plugin found
*/
static async getPluginPath(executableName: string): Promise<string> {
const [clangVersion, availablePlugins] = await Promise.all([
this.getClangVersion(executableName),
this.getAvailablePlugins(),
]);
if (clangVersion in availablePlugins) {
return availablePlugins[clangVersion];
} else {
throw new Error("Could not find plugin for provided clang executable");
}
}
/**
* Executes the clang plugin in a sandboxed environment with the given arguments
* and returns a pipe to the output.
*
* @param commandList - Command used to execute the compiler by the user
* @param pluginPath - Path to the clang plugin aligned with the compiler version
* @param args - Arguments to pass to the clang plugin
*/
static async executeClangPlugin(
commandList: (string | number)[]
): Promise<string> {
const sanitizedCommand = await Sandbox.sanitizeCommand(commandList);
const [command, args, env] = await Sandbox.splitCommandArgsEnv(
sanitizedCommand
);
const pluginPath = await this.getPluginPath(command);
const envMap = env.reduce((acc, curr) => {
const [key, value] = curr.split("=");
return acc.set(key, value);
}, new Map<string, string>());
const commandArgs = [
...args.map((arg) => this.ensureAbsolutePath(arg.toString())),
"-Xclang",
"-load",
"-Xclang",
pluginPath,
];
return Sandbox.executeSandboxedCommand(command, commandArgs, envMap);
}
/**
* Ensures that the argument is an absolute path if it exists
* otherwise returns the argument itself
*
* @param argument - String containing the argument to check
* @returns The absolute path to the argument if it exists, otherwise the argument itself
*/
private static ensureAbsolutePath(argument: string): string {
if (fs.existsSync(argument)) {
return path.resolve(argument);
} else {
return argument;
}
}
}