Skip to content

Instantly share code, notes, and snippets.

@roman-kirsanov
Last active September 16, 2020 14:37
Show Gist options
  • Save roman-kirsanov/a45620f4ddbac14d5562c158155bff7f to your computer and use it in GitHub Desktop.
Save roman-kirsanov/a45620f4ddbac14d5562c158155bff7f to your computer and use it in GitHub Desktop.
Clang Build System powered by Node.js
module.exports = options => {
options = Object.assign({
path: null,
name: null
}, options);
if (options.path === null || options.path === undefined)
throw new Error('BuildSystem: "path" is required');
const libcp = require('child_process');
const libfs = require('fs');
const libpath = require('path');
const fileSize = p => {
if (libfs.existsSync(p))
return libfs.statSync(p).size;
else throw new Error(`fileSize: File "${p}" not found`);
}
const fileModified = p => {
if (libfs.existsSync(p))
return libfs.statSync(p).mtime.getTime();
else throw new Error(`fileModified: File "${p}" not found`);
}
const removeDir = p => {
if (libfs.existsSync(p)) {
libfs.readdirSync(p).forEach(f => {
const pp = libpath.join(p, f);
if (libfs.lstatSync(pp).isDirectory()) removeDir(pp);
else libfs.unlinkSync(pp);
});
libfs.rmdirSync(p);
}
}
const changeExt = (p, e) => [...p.split('.').slice(0, -1), e].join('.');
const platform = require('os').platform();
const projectpath = libpath.normalize(options.path);
const projectname = options.name || libpath.basename(projectpath);
const projectext = (platform == 'win32') ? 'exe' : 'out';
const __tempPath = (platform == 'win32') ? 'C:\\Temp' : '/tmp';
const __searchPath = [];
const __libraryPath = [];
const __libraries = [];
const __frameworks = [];
const __sourceFiles = [];
const __arguments = [];
const __defines = [];
const __pchs = [];
let __compiler = 'clang';
let __errorLimit = null;
let __targetStandard = null;
let __debugMode = true;
let __runProgram = false;
let __lastClangCmd = '';
const clang = cmd => {
const p = libcp.spawnSync((__lastClangCmd = cmd.join(' ')), [], { shell: true });
const out = p.stdout.toString().trim();
const err = p.stderr.toString().trim();
return {
out: (out.length > 0) ? out : null,
err: (err.length > 0) ? err : null
}
}
const setCompiler = a => __compiler = a;
const addSearchPath = p => {
const pp = libpath.normalize(p);
if (libfs.existsSync(pp)) {
if (libfs.lstatSync(pp).isDirectory()) __searchPath.push(pp);
else throw new Error(`addSearchPath: "${pp}" is not a directory`);
} else throw new Error(`addSearchPath: "${pp}" path not found`);
}
const addLibraryPath = p => {
const pp = libpath.normalize(p);
if (libfs.existsSync(pp)) {
if (libfs.lstatSync(pp).isDirectory()) __libraryPath.push(pp);
else throw new Error(`addLibraryPath: "${pp}" is not a directory`);
} else throw new Error(`addLibraryPath: "${pp}" path not found`);
}
const addLibrary = a => __libraries.push(a);
const addFramework = a => __frameworks.push(a);
const addStandardWin32Libs = () => {
__libraries.push('user32.lib');
__libraries.push('gdi32.lib');
__libraries.push('shell32.lib');
__libraries.push('Winmm.lib');
}
const addStandardDarwinLibs = () => {
__frameworks.push('Cocoa');
}
const addSourceFile = p => {
const pp = libpath.normalize(p);
if (libfs.existsSync(pp)) {
if (!libfs.lstatSync(pp).isDirectory()) __sourceFiles.push(pp);
else throw new Error(`addSourceFile: "${pp}" is not a file`);
} else throw new Error(`addSourceFile: "${pp}" path not found`);
}
const addSourceFolder = (p, e) => {
libfs.readdirSync(p).forEach(f => {
if (e.includes(libpath.extname(f)))
addSourceFile(libpath.join(p, f));
});
}
const addArgument = a => __arguments.push(a);
const addDefine = a => __defines.push(a);
const addPCH = a => a.forEach(i => __pchs.push(i));
const setErrorLimit = a => __errorLimit = a;
const setStandard = a => __targetStandard = a;
const setRelease = a => __debugMode = !a;
const setRun = a => __runProgram = a;
const buildCommandLine = options => {
options = Object.assign({
compiler: true,
debugging: true,
includeSearch: true,
librarySearch: true,
frameworks: true,
libraries: true,
arguments: true,
defines: true
}, options);
const ret = [];
if (options.compiler)
ret.push(__compiler);
if (__targetStandard)
ret.push(`-std=${__targetStandard}`);
// if (platform == 'darwin')
// ret.push('-fobjc-arc');
if (options.debugging) {
if (__debugMode) {
ret.push('-g');
ret.push('-O0');
ret.push('-fsanitize=address');
if (__errorLimit)
ret.push(`-ferror-limit=${__errorLimit}`);
} else {
ret.push('-O3');
ret.push('-DNDEBUG');
}
}
if (options.includeSearch)
__searchPath.forEach(p => ret.push(`-I"${p}"`));
if (options.librarySearch)
__libraryPath.forEach(p => ret.push(`-L"${p}"`));
if (options.libraries)
__libraries.forEach(l => ret.push(`-l${l}`));
if (options.frameworks)
__frameworks.forEach(f => ret.push(`-framework ${f}`));
if (options.defines)
__defines.forEach(a => ret.push(`-D${a}`));
if (options.arguments)
__arguments.forEach(a => ret.push(a));
return ret;
}
const getFileDependencies = p => {
const getFileList = a => a
.split('\n')
.slice(1)
.map(ln => ln.trim())
.reduce((ret, ln) => {
const fn = ((ln.slice(-1) == '\\')
? libpath.normalize(ln.slice(0, -1))
: libpath.normalize(ln)).trim();
if (!ret.includes(fn))
ret.push(fn);
return ret;
}, []);
const getFileInfoList = a => a.map(fn => {
const stat = libfs.statSync(fn);
return {
filename: fn,
modified: stat.mtime.getTime(),
size: stat.size
}
});
if (libfs.existsSync(p)) {
const cmd = buildCommandLine({
debugging: false,
frameworks: false,
libraries: false,
librarySearch: false,
arguments: false
});
const res = clang([...cmd, '-MM', p]);
if (res.err) return {
status: 'error',
message: (res.err + '\n' + res.out)
}
const files = getFileList(res.out);
const info = getFileInfoList(files);
return {
status: 'success',
result: info
}
} else return {
status: 'error',
message: `File "${p}" not found`
}
}
const getLineCount = p => {
if (p === null || p === undefined)
throw new Error(`getLineCount: Specify first argument (input fileName)`);
if (libfs.existsSync(p)) {
let ret = 0;
const buf = libfs.readFileSync(p);
for (const byte of buf.values()) {
if (byte == '\n'.charCodeAt(0))
ret += 1;
}
return {
status: 'success',
result: ret
};
} else return {
status: 'error',
message: `File "${p}" not found`
}
}
const preprocessFile = (p, o) => {
if (p === null || p === undefined)
throw new Error(`preprocessFile: Specify first argument (input fileName)`);
if (o === null || o === undefined)
throw new Error(`preprocessFile: Specify second argument (output fileName)`);
if (libfs.existsSync(p)) {
const t_begin = (new Date()).getTime();
const cmd = buildCommandLine({
frameworks: false,
libraries: false,
librarySearch: false
});
const res = clang([...cmd, '-E', p, '-o', o]);
if (res.err) return {
status: 'error',
message: (res.err + '\n' + res.out)
}
const t_end = (new Date()).getTime();
if (!libfs.existsSync(o)) return {
status: 'error',
message: `File not created "${o}"`
}
return {
status: 'success',
filename: o,
duration: (t_end - t_begin)
}
} else return {
status: 'error',
message: `File "${p}" not found`
}
}
const compileFile = (p, o) => {
if (p === null || p === undefined)
throw new Error(`compileFile: Specify first argument (input fileName)`);
if (o === null || o === undefined)
throw new Error(`compileFile: Specify second argument (output fileName)`);
if (libfs.existsSync(p)) {
const t_begin = (new Date()).getTime();
const cmd = buildCommandLine({
frameworks: false,
libraries: false
});
const res = clang([...cmd, '-c', p, '-o', o]);
if (res.err) return {
status: 'error',
message: (res.err + '\n' + res.out)
};
const t_end = (new Date()).getTime();
if (!libfs.existsSync(o)) return {
status: 'error',
message: `File not created "${o}"`
};
return {
status: 'success',
filename: o,
duration: (t_end - t_begin)
}
} else return {
status: 'error',
message: `File "${p}" not found`
}
}
const linkFiles = (files, o) => {
if (files === null || files === undefined)
throw new Error(`linkFiles: Specify first argument (input fileName list)`);
if (!Array.isArray(files))
throw new Error(`linkFiles: First argument must be an array`);
if (o === null || o === undefined)
throw new Error(`linkFiles: Specify second argument (output fileName)`);
for (const fn of files)
if (!libfs.existsSync(fn))
return { status: 'error', message: `File "${fn}" not found` };
const cmd = buildCommandLine();
const t_begin = (new Date()).getTime();
const input = files.map(fn => `"${fn}"`);
const res = clang([...cmd, ...input, '-o', o]);
if (res.err) return {
status: 'error',
message: (res.err + '\n' + res.out)
};
const t_end = (new Date()).getTime();
if (!libfs.existsSync(o)) return {
status: 'error',
message: `File not created "${o}"`
};
return {
status: 'success',
filename: o,
duration: (t_end - t_begin)
}
}
const createPCH = (a, o) => {
if (a === null || a === undefined)
throw new Error(`createPCH: Specify first argument (list of headers)`);
if (!Array.isArray(a))
throw new Error(`createPCH: First argument must be an array`);
if (o === null || o === undefined)
throw new Error(`createPCH: Specify second argument (output fileName)`);
const hfile = a.map(h => `#include <${h}>`).join('\n');
const path_pch = o;
const path_h = changeExt(o, 'h');
libfs.writeFileSync(path_h, hfile);
const cmd = buildCommandLine({
frameworks: false,
libraries: false,
librarySearch: false
});
const t_begin = (new Date()).getTime();
const res = clang([...cmd, path_h, '-emit-pch', '-o', path_pch]);
if (res.err) return {
status: 'error',
message: (res.err + '\n' + res.out)
};
const t_end = (new Date()).getTime();
if (!libfs.existsSync(path_pch)) return {
status: 'error',
message: `File not created "${path_pch}"`
};
return {
status: 'success',
filename: path_pch,
duration: (t_end - t_begin)
}
}
const createMakeInfo = sinkpath => {
const getFolderName = filename => {
const dirname = libpath.dirname(filename);
const basename = libpath.basename(dirname);
if (basename.toUpperCase() != 'IMPL') return basename;
else {
const dirname2 = libpath.dirname(dirname);
const basename2 = libpath.basename(dirname2);
return basename2;
}
}
const makeInfo = {
duration: 0,
translationUnits: __sourceFiles.map(filename => ({
baseName: libpath.basename(filename),
displayName: '',
sourceFileName: filename,
targetFileName: libpath.join(
sinkpath,
libpath.basename(libpath.dirname(filename)) + '_' +
changeExt(libpath.basename(filename), 'o')
),
preprFileName: libpath.join(
sinkpath,
libpath.basename(libpath.dirname(filename)) + '_' +
changeExt(libpath.basename(filename), `preprocessed${libpath.extname(filename)}`)
),
infoFileName: libpath.join(
sinkpath,
libpath.basename(libpath.dirname(filename)) + '_' +
changeExt(libpath.basename(filename), 'makeinfo.json')
),
modified: fileModified(filename),
dependenciesTime: 0,
preprocessTime: 0,
compilationTime: 0,
unitTime: 0,
linesOfCode: 0,
linesOfCode_prep: 0,
unitSize: 0,
action: null,
dependencies: []
}))
};
makeInfo.translationUnits.forEach(tu => {
const o2 = makeInfo.translationUnits.find(t => t.targetFileName == tu.targetFileName && t != tu);
if (o2)
throw new Error(`BuildSystem: Duplicate file found "${t.targetFileName}"`);
const tu2 = makeInfo.translationUnits.find(t => t.baseName == tu.baseName && t != tu);
if (tu2) {
tu2.displayName = `${tu2.baseName} (${getFolderName(tu2.sourceFileName)})`;
tu.displayName = `${tu.baseName} (${getFolderName(tu.sourceFileName)})`;
} else {
tu.displayName = tu.baseName;
}
});
return makeInfo;
};
const prepareTranslationUnit = tu => {
if (tu === null || tu === undefined)
throw new Error(`prepareTranslationUnit: Specify first argument (TU object)`);
const t_begin = (new Date()).getTime();
tu.action = 'skip';
tu.dependencies = getFileDependencies(tu.sourceFileName).result;
const prevTU = libfs.existsSync(tu.infoFileName)
? JSON.parse(libfs.readFileSync(tu.infoFileName).toString())
: null;
if (prevTU) {
if (tu.modified == prevTU.modified) {
if (prevTU.dependencies.length == tu.dependencies.length) {
let all_skip = true;
for (let i = 0; i < tu.dependencies.length; i++) {
const dep = tu.dependencies[i];
const prevDep = prevTU.dependencies[i];
if (dep.filename == prevDep.filename) {
if (dep.modified == prevDep.modified) {
/* skip */
} else {
all_skip = false;
break;
}
} else {
all_skip = false;
break;
}
}
if (all_skip) {
tu.linesOfCode = prevTU.linesOfCode;
tu.linesOfCode_prep = prevTU.linesOfCode_prep;
tu.unitSize = prevTU.unitSize;
} else tu.action = 'compile';
} else tu.action = 'compile';
} else tu.action = 'compile';
} else tu.action = 'compile';
const t_end = (new Date()).getTime();
return {
duration: (t_end - t_begin)
}
}
const print = (a, b) => process.stdout.write(a.toString().padEnd(b || 0));
const clean = () => {
const path = libpath.join(__tempPath, projectname);
if (libfs.existsSync(path))
removeDir(path);
}
const make = () => {
const sinkpath = libpath.join(__tempPath, projectname, __debugMode ? 'Debug' : 'Release');
const outpath = libpath.join(sinkpath, `${projectname}.${projectext}`);
if (!__debugMode) {
if (libfs.existsSync(sinkpath))
removeDir(sinkpath);
libfs.mkdirSync(sinkpath, { recursive: true });
} else {
if (!libfs.existsSync(sinkpath))
libfs.mkdirSync(sinkpath, { recursive: true });
}
const makeInfo = createMakeInfo(sinkpath);
const maxFileName = Math.max.apply(null, makeInfo.translationUnits.map(tu => tu.displayName.length));
const NFormat = new Intl.NumberFormat('es-ES');
const formatLOC = a => NFormat.format(a).replace(/\./g, ' ');
const formatTime = a => `${(a / 1000).toFixed(2)} Sec`;
const formatSize = a => `${(a / 1024 / 1024).toFixed(2)} MiB`;
const COLUMN_FILENAME = (maxFileName + 4);
const COLUMN_ACTION = 11;
const COLUMN_TIME = 12;
const COLUMN_LOC = 11;
const COLUMN_SIZE = 10;
console.log(`COMPILATION (${__debugMode ? 'Debug' : 'Release'})\n`);
console.log(
'File Name'.padEnd(COLUMN_FILENAME) +
'-MM Time'.padEnd(COLUMN_TIME) +
'Action'.padEnd(COLUMN_ACTION) +
'LOC'.padEnd(COLUMN_LOC) +
'-E Time'.padEnd(COLUMN_TIME) +
'LOC (-E)'.padEnd(COLUMN_LOC) +
'-c Time'.padEnd(COLUMN_TIME) +
'TU Time'.padEnd(COLUMN_TIME) +
'TU Size'.padEnd(COLUMN_SIZE)
);
console.log(
'-'.padEnd(COLUMN_FILENAME, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_ACTION, '-') +
'-'.padEnd(COLUMN_LOC, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_LOC, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_SIZE, '-')
);
let total_mmt = 0;
let total_loc = 0;
let total_etime = 0;
let total_locpr = 0;
let total_ctime = 0;
let total_tutime = 0;
let error = null;
for (const tu of makeInfo.translationUnits) {
print(tu.displayName, COLUMN_FILENAME);
const res0 = prepareTranslationUnit(tu);
tu.dependenciesTime = res0.duration;
print(formatTime(tu.dependenciesTime), COLUMN_TIME);
print(tu.action, COLUMN_ACTION);
if (tu.action == 'compile') {
const res1 = getLineCount(tu.sourceFileName);
if (res1.status == 'error') {
error = res1.message;
break;
} else tu.linesOfCode = res1.result;
print(formatLOC(tu.linesOfCode), COLUMN_LOC);
const res2 = preprocessFile(tu.sourceFileName, tu.preprFileName);
if (res2.status == 'error') {
error = res2.message;
break;
} else tu.preprocessTime = res2.duration;
print(formatTime(tu.preprocessTime), COLUMN_TIME);
const res3 = getLineCount(tu.preprFileName);
if (res3.status == 'error') {
error = res3.message;
break;
} else tu.linesOfCode_prep = res3.result;
print(formatLOC(tu.linesOfCode_prep), COLUMN_LOC);
const res4 = compileFile(tu.preprFileName, tu.targetFileName);
if (res4.status == 'error') {
error = res4.message;
break;
} else tu.compilationTime = res4.duration;
print(formatTime(tu.compilationTime), COLUMN_TIME);
tu.unitSize = fileSize(tu.targetFileName);
} else {
print(formatLOC(tu.linesOfCode), COLUMN_LOC);
print('-', COLUMN_TIME);
print(formatLOC(tu.linesOfCode_prep), COLUMN_LOC);
print('-', COLUMN_TIME);
}
tu.unitTime = tu.dependenciesTime + tu.preprocessTime + tu.compilationTime;
print(formatTime(tu.unitTime), COLUMN_TIME);
print(formatSize(tu.unitSize), COLUMN_SIZE);
total_mmt += tu.dependenciesTime;
total_etime += tu.preprocessTime;
total_ctime += tu.compilationTime;
total_tutime += tu.unitTime;
total_loc += tu.linesOfCode;
total_locpr += tu.linesOfCode_prep;
libfs.writeFileSync(tu.infoFileName, JSON.stringify(tu));
console.log('');
}
if (!error) {
console.log(
'-'.padEnd(COLUMN_FILENAME, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_ACTION, '-') +
'-'.padEnd(COLUMN_LOC, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_LOC, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_SIZE, '-')
);
print('Total', COLUMN_FILENAME);
print(formatTime(total_mmt), COLUMN_TIME);
print('-', COLUMN_ACTION);
print(formatLOC(total_loc), COLUMN_LOC);
print(formatTime(total_etime), COLUMN_TIME);
print(formatLOC(total_locpr), COLUMN_LOC);
print(formatTime(total_ctime), COLUMN_TIME);
print(formatTime(total_tutime), COLUMN_TIME);
print('-', COLUMN_SIZE);
console.log('\n');
console.log(`LINKAGE (${__debugMode ? 'Debug' : 'Release'})\n`);
console.log(
'File Name'.padEnd(COLUMN_FILENAME) +
'Time'.padEnd(COLUMN_TIME) +
'Size'.padEnd(COLUMN_SIZE)
);
console.log(
'-'.padEnd(COLUMN_FILENAME, '-') +
'-'.padEnd(COLUMN_TIME, '-') +
'-'.padEnd(COLUMN_SIZE, '-')
);
print(libpath.basename(outpath), COLUMN_FILENAME);
const res5 = linkFiles(
makeInfo.translationUnits.map(tu => tu.targetFileName),
outpath
);
if (res5.status == 'error') {
console.log('*\n');
console.log(res5.message + '\n' + __lastClangCmd);
} else {
print(formatTime(res5.duration), COLUMN_TIME);
print(formatSize(fileSize(outpath)), COLUMN_SIZE);
console.log('\n');
if (__runProgram)
libcp.spawn(outpath, [], { stdio: 'inherit', shell: true });
}
} else {
console.log('*\n');
console.log(error + '\n' + __lastClangCmd);
}
}
return {
clean: clean,
make: make,
setCompiler,
addSearchPath,
addLibraryPath,
addLibrary,
addFramework,
addStandardDarwinLibs,
addStandardWin32Libs,
addSourceFile,
addSourceFolder,
addArgument,
addDefine,
addPCH,
setErrorLimit,
setStandard,
setRelease,
setRun,
getFileDependencies,
getLineCount,
preprocessFile,
compileFile,
linkFiles,
name: () => projectname,
platform: () => platform,
}
}
/* USAGE */
const libfs = require('fs');
const libpath = require('path');
const BuildSystem = require('../Path/To/clang-BuildSystem.js');
const builder = BuildSystem({ path: __dirname });
const libraryPath = libpath.join(builder.path(), '..', 'Library');
const chipmunkPath = libpath.join(libraryPath, '3rdParty', 'Chipmunk2D');
builder.addSearchPath(libraryPath);
builder.addSearchPath(libpath.join(chipmunkPath, 'include'));
builder.addSourceFolder(libpath.join(chipmunkPath, 'src'), ['.c']);
if (builder.platform() == 'win32') {
builder.addStandardWin32Libs();
builder.addSourceFile(libpath.join(libraryPath, 'Win32', 'Impl', 'File1.cc'));
builder.addSourceFile(libpath.join(libraryPath, 'Win32', 'Impl', 'File2.cc'));
} else if (builder.platform() == 'darwin') {
builder.addStandardDarwinLibs();
builder.addFramework('QuartzCore');
builder.addSourceFile(libpath.join(libraryPath, 'MacOS', 'Impl', 'File1.cc'));
builder.addSourceFile(libpath.join(libraryPath, 'MacOS', 'Impl', 'File2.cc'));
}
builder.setStandard('c++2a');
builder.addSourceFile(libpath.join(builder.path(), 'Source', 'main.cc'));
builder.setRelease(process.argv.includes('--release'));
builder.setRun(process.argv.includes('--run'));
builder.make();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment