mirror of https://github.com/jkjoy/sunpeiwen.git
739 lines
18 KiB
JavaScript
739 lines
18 KiB
JavaScript
'use strict';
|
|
|
|
// TODO: take `compilerParams`, headers into account in cache for all cached
|
|
// results
|
|
// TODO: debug output
|
|
|
|
const { spawnSync } = require('child_process');
|
|
const { unlinkSync, writeFileSync } = require('fs');
|
|
const { tmpdir } = require('os');
|
|
const { win32: path } = require('path');
|
|
const { inspect } = require('util');
|
|
|
|
const isWindows = (process.platform === 'win32');
|
|
const findVS = require('./findvs.js');
|
|
|
|
const RE_HEADER_DECORATED = /^(?:"(.+)")|(?:<(.+)>)$/;
|
|
|
|
const genWinTmpFilenames = (() => {
|
|
let instance = 1;
|
|
return () => {
|
|
const base =
|
|
path.resolve(tmpdir(), `_buildcheck-${process.pid}-${instance++}`);
|
|
return {
|
|
input: `${base}.in.tmp`,
|
|
object: `${base}.out.obj`,
|
|
output: `${base}.out.tmp`,
|
|
};
|
|
};
|
|
})();
|
|
|
|
function getKind(prop) {
|
|
const spawnOpts = {
|
|
encoding: 'utf8',
|
|
stdio: 'pipe',
|
|
windowsHide: true,
|
|
};
|
|
|
|
const lang = (prop === '_cc' ? 'c' : 'c++');
|
|
|
|
if (isWindows) {
|
|
spawnOpts.stdio = [ 'ignore', 'pipe', 'pipe' ];
|
|
writeFileSync(this._tmpInFile, [
|
|
'_MSC_VER',
|
|
].join(' '));
|
|
const result = spawnSync(
|
|
this[prop],
|
|
[ '-EP', `-T${lang === 'c' ? 'c' : 'p'}`, this._tmpInFile],
|
|
spawnOpts
|
|
);
|
|
unlinkSync(this._tmpInFile);
|
|
|
|
if (result.status === 0) {
|
|
const values = result.stdout.trim().split(' ');
|
|
if (values.length === 1 && /^\d+$/.test(values[0])) {
|
|
this[`${prop}Kind`] = 'msvc';
|
|
this[`${prop}Version`] = values[0];
|
|
this[`${prop}SpawnOpts`] = spawnOpts;
|
|
this._debug(
|
|
`>>> Detected MSVC ${values[0]} for ${lang.toUpperCase()} language`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
const result = spawnSync(
|
|
this[prop],
|
|
[ '-E', '-P', '-x', lang, '-' ],
|
|
{
|
|
...spawnOpts,
|
|
input: [
|
|
'__clang__',
|
|
'__GNUC__',
|
|
'__GNUC_MINOR__',
|
|
'__GNUC_PATCHLEVEL__',
|
|
'__clang_major__',
|
|
'__clang_minor__',
|
|
'__clang_patchlevel__',
|
|
].join(' '),
|
|
}
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
const values = result.stdout.trim().split(' ');
|
|
if (values.length === 7) {
|
|
let kind;
|
|
let version;
|
|
if (values[0] === '1') {
|
|
kind = 'clang';
|
|
version = values.slice(4).map((v) => +v);
|
|
} else {
|
|
kind = 'gnu';
|
|
version = values.slice(1, 4).map((v) => +v);
|
|
}
|
|
let good = true;
|
|
for (const part of version) {
|
|
if (!isFinite(part) || part < 0) {
|
|
good = false;
|
|
break;
|
|
}
|
|
}
|
|
if (good) {
|
|
this[`${prop}Kind`] = kind;
|
|
this[`${prop}Version`] = version;
|
|
this[`${prop}SpawnOpts`] = spawnOpts;
|
|
const verStr = version.join('.');
|
|
this._debug(
|
|
`>>> Detected ${kind} ${verStr} for ${lang.toUpperCase()} language`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error('Unable to detect compiler type');
|
|
}
|
|
|
|
class BuildEnvironment {
|
|
constructor(cfg) {
|
|
if (typeof cfg !== 'object' || cfg === null)
|
|
cfg = {};
|
|
|
|
this._debug = (typeof cfg.debug === 'function' ? cfg.debug : () => {});
|
|
|
|
let cc;
|
|
let cxx;
|
|
if (isWindows) {
|
|
const versions = findVS();
|
|
this._debug(
|
|
`>>> Detected MSVS installations: ${inspect(versions, false, 10)}`
|
|
);
|
|
if (versions.length === 0)
|
|
throw new Error('Unable to detect compiler type');
|
|
let selected_msvs;
|
|
if (cfg.msvs_version
|
|
&& (typeof cfg.msvs_version === 'string'
|
|
|| typeof cfg.msvs_version === 'number')) {
|
|
this._debug(`>>> Explicit MSVS requested: ${cfg.msvs_version}`);
|
|
// Try to select compiler by year
|
|
const msvs_version = cfg.msvs_version.toString();
|
|
for (const vs of versions) {
|
|
if (vs.year.toString() === msvs_version) {
|
|
selected_msvs = vs;
|
|
break;
|
|
}
|
|
}
|
|
if (selected_msvs === undefined)
|
|
throw new Error(`Unable to find MSVS with year '${msvs_version}'`);
|
|
} else {
|
|
selected_msvs = versions[0]; // Use newest
|
|
}
|
|
this._debug(`>>> Using MSVS: ${selected_msvs.year}`);
|
|
cc = selected_msvs.cl;
|
|
cxx = cc;
|
|
this._includePaths = selected_msvs.includePaths;
|
|
this._libPaths = selected_msvs.libPaths;
|
|
// Add (newest) SDK paths if we have them
|
|
for (const sdk of selected_msvs.sdks) {
|
|
this._debug(`>>> Using Windows SDK: ${sdk.fullVersion}`);
|
|
this._includePaths = this._includePaths.concat(sdk.includePaths);
|
|
this._libPaths = this._libPaths.concat(sdk.libPaths);
|
|
break;
|
|
}
|
|
|
|
const { input, object, output } = genWinTmpFilenames();
|
|
this._tmpInFile = input;
|
|
this._tmpObjFile = object;
|
|
this._tmpOutFile = output;
|
|
} else {
|
|
cc = ((typeof cfg.compilerC === 'string' && cfg.compilerC)
|
|
|| process.env.CC
|
|
|| 'cc');
|
|
cxx = ((typeof cfg.compilerCXX === 'string' && cfg.compilerCXX)
|
|
|| process.env.CXX
|
|
|| 'c++');
|
|
this._debug(`>>> Using C compiler: ${cc}`);
|
|
this._debug(`>>> Using C++ compiler: ${cxx}`);
|
|
}
|
|
|
|
this._cc = cc;
|
|
this._ccKind = undefined;
|
|
this._ccVersion = undefined;
|
|
this._ccSpawnOpts = undefined;
|
|
|
|
this._cxx = cxx;
|
|
this._cxxKind = undefined;
|
|
this._cxxVersion = undefined;
|
|
this._cxxSpawnOpts = undefined;
|
|
|
|
if (cfg.cache !== false) {
|
|
this._cache = new Map(Object.entries({
|
|
c: new Map(),
|
|
cxx: new Map(),
|
|
}));
|
|
} else {
|
|
this._cache = null;
|
|
}
|
|
}
|
|
|
|
checkDeclared(type, symbolName, opts) {
|
|
validateType(type);
|
|
if (typeof symbolName !== 'string' || !symbolName)
|
|
throw new Error(`Invalid symbol name: ${inspect(symbolName)}`);
|
|
|
|
const cached = getCachedValue(type, this._cache, 'declared', symbolName);
|
|
if (cached !== undefined) {
|
|
this._debug(
|
|
`>>> Checking if '${symbolName}' is declared... ${cached} (cached)`
|
|
);
|
|
return cached;
|
|
}
|
|
|
|
if (typeof opts !== 'object' || opts === null)
|
|
opts = {};
|
|
|
|
const { headers, searchLibs } = opts;
|
|
const headersList = renderHeaders(Array.isArray(headers)
|
|
? headers
|
|
: getDefaultHeaders(this, type));
|
|
|
|
const declName = symbolName.replace(/ *\(.*/, '');
|
|
const declUse = symbolName.replace(/\(/, '((')
|
|
.replace(/\)/, ') 0)')
|
|
.replace(/,/g, ') 0, (');
|
|
|
|
const libs = [
|
|
'',
|
|
...(Array.isArray(searchLibs) ? searchLibs : [])
|
|
];
|
|
|
|
for (let lib of libs) {
|
|
if (typeof lib !== 'string')
|
|
continue;
|
|
lib = lib.trim();
|
|
|
|
const code = `
|
|
${headersList}
|
|
|
|
int
|
|
main ()
|
|
{
|
|
#ifndef ${declName}
|
|
#ifdef __cplusplus
|
|
(void) ${declUse};
|
|
#else
|
|
(void) ${declName};
|
|
#endif
|
|
#endif
|
|
|
|
;
|
|
return 0;
|
|
}`;
|
|
const compilerParams = (lib ? [`-l${lib}`] : []);
|
|
const result = this.tryCompile(type, code, compilerParams);
|
|
|
|
this._debug(
|
|
`>>> Checking if '${symbolName}' is declared `
|
|
+ `(using ${lib ? `'${lib}'` : 'no'} library)... ${result === true}`
|
|
);
|
|
|
|
if (result !== true) {
|
|
this._debug('... check failed with compiler output:');
|
|
this._debug(result.output);
|
|
}
|
|
|
|
if (result === true) {
|
|
setCachedValue(
|
|
type,
|
|
this._cache,
|
|
'declared',
|
|
symbolName,
|
|
(result === true)
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
checkFeature(name) {
|
|
const cached = getCachedValue('features', this._cache, null, name);
|
|
if (cached !== undefined) {
|
|
if (typeof cached === 'object'
|
|
&& cached !== null
|
|
&& typeof cached.val !== undefined) {
|
|
return cached.val;
|
|
}
|
|
|
|
return cached;
|
|
}
|
|
|
|
const feature = features.get(name);
|
|
if (feature === undefined)
|
|
throw new Error(`Invalid feature: ${name}`);
|
|
|
|
let result = feature(this);
|
|
if (result === undefined)
|
|
result = null;
|
|
|
|
setCachedValue('features', this._cache, null, name, result);
|
|
|
|
if (typeof result === 'object'
|
|
&& result !== null
|
|
&& typeof result.val !== undefined) {
|
|
return result.val;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
checkFunction(type, funcName, opts) {
|
|
validateType(type);
|
|
if (typeof funcName !== 'string' || !funcName)
|
|
throw new Error(`Invalid function name: ${inspect(funcName)}`);
|
|
|
|
const cached = getCachedValue(type, this._cache, 'functions', funcName);
|
|
if (cached !== undefined) {
|
|
this._debug(
|
|
`>>> Checking if function '${funcName}' exists... true (cached)`
|
|
);
|
|
return true;
|
|
}
|
|
|
|
if (typeof opts !== 'object' || opts === null)
|
|
opts = {};
|
|
|
|
const { searchLibs } = opts;
|
|
const libs = [
|
|
'',
|
|
...(Array.isArray(searchLibs) ? searchLibs : [])
|
|
];
|
|
|
|
for (let lib of libs) {
|
|
if (typeof lib !== 'string')
|
|
continue;
|
|
lib = lib.trim();
|
|
|
|
const code = `
|
|
/* Define ${funcName} to an innocuous variant, in case <limits.h> declares
|
|
${funcName}.
|
|
For example, HP-UX 11i <limits.h> declares gettimeofday. */
|
|
#define ${funcName} innocuous_${funcName}
|
|
/* System header to define __stub macros and hopefully few prototypes,
|
|
which can conflict with char ${funcName} (); below. */
|
|
#include <limits.h>
|
|
#undef ${funcName}
|
|
/* Override any GCC internal prototype to avoid an error.
|
|
Use char because int might match the return type of a GCC
|
|
builtin and then its argument prototype would still apply. */
|
|
#ifdef __cplusplus
|
|
extern "C"
|
|
#endif
|
|
char ${funcName} ();
|
|
/* The GNU C library defines this for functions which it implements
|
|
to always fail with ENOSYS. Some functions are actually named
|
|
something starting with __ and the normal name is an alias. */
|
|
#if defined __stub_${funcName} || defined __stub___${funcName}
|
|
choke me
|
|
#endif
|
|
|
|
int
|
|
main ()
|
|
{
|
|
return ${funcName} ();
|
|
;
|
|
return 0;
|
|
}`;
|
|
|
|
const compilerParams = (lib ? [`-l${lib}`] : []);
|
|
const result = this.tryCompile(type, code, compilerParams);
|
|
|
|
this._debug(
|
|
`>>> Checking if function '${funcName}' exists `
|
|
+ `(using ${lib ? `'${lib}'` : 'no'} library)... ${result === true}`
|
|
);
|
|
if (result !== true) {
|
|
this._debug('... check failed with compiler output:');
|
|
this._debug(result.output);
|
|
}
|
|
|
|
if (result === true) {
|
|
setCachedValue(
|
|
type,
|
|
this._cache,
|
|
'functions',
|
|
funcName,
|
|
compilerParams
|
|
);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
checkHeader(type, header) {
|
|
validateType(type);
|
|
const cached = getCachedValue(
|
|
type,
|
|
this._cache,
|
|
'headers',
|
|
normalizeHeader(header)
|
|
);
|
|
if (cached !== undefined) {
|
|
this._debug(
|
|
`>>> Checking if header '${header}' exists... ${cached} (cached)`
|
|
);
|
|
return cached;
|
|
}
|
|
|
|
const headersList = renderHeaders([header]);
|
|
|
|
const code = `
|
|
${headersList}
|
|
|
|
int
|
|
main ()
|
|
{
|
|
return 0;
|
|
}`;
|
|
|
|
const result = this.tryCompile(type, code);
|
|
setCachedValue(
|
|
type,
|
|
this._cache,
|
|
'headers',
|
|
normalizeHeader(header),
|
|
(result === true)
|
|
);
|
|
this._debug(
|
|
`>>> Checking if header '${header}' exists... ${result === true}`
|
|
);
|
|
if (result !== true) {
|
|
this._debug('... check failed with compiler output:');
|
|
this._debug(result.output);
|
|
}
|
|
return (result === true);
|
|
}
|
|
|
|
defines(type, rendered) {
|
|
if (this._cache === null)
|
|
return [];
|
|
|
|
const defines = new Map();
|
|
|
|
let types;
|
|
if (!['c', 'c++'].includes(type))
|
|
types = ['c', 'c++'];
|
|
else
|
|
types = [type];
|
|
|
|
for (const t of types) {
|
|
const typeCache = this._cache.get(t);
|
|
if (!typeCache)
|
|
continue;
|
|
|
|
for (const [subtype, entries] of typeCache) {
|
|
for (let name of entries.keys()) {
|
|
if (subtype === 'headers')
|
|
name = name.replace(RE_HEADER_DECORATED, '$1$2');
|
|
defines.set(makeDefine(name, rendered), 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
const featuresCache = this._cache.get('features');
|
|
if (featuresCache) {
|
|
for (const result of featuresCache.values()) {
|
|
if (typeof result === 'object'
|
|
&& result !== null
|
|
&& Array.isArray(result.defines)) {
|
|
for (const define of result.defines)
|
|
defines.set(makeDefine(define, rendered), 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(defines.keys());
|
|
}
|
|
|
|
libs(type) {
|
|
if (this._cache === null)
|
|
return [];
|
|
|
|
const libs = new Map();
|
|
|
|
let types;
|
|
if (!['c', 'c++'].includes(type))
|
|
types = ['c', 'c++'];
|
|
else
|
|
types = [type];
|
|
|
|
for (const t of types) {
|
|
const typeCache = this._cache.get(t);
|
|
if (!typeCache)
|
|
continue;
|
|
|
|
const functionsCache = typeCache.get('functions');
|
|
if (!functionsCache)
|
|
continue;
|
|
|
|
for (const compilerParams of functionsCache.values()) {
|
|
for (const param of compilerParams)
|
|
libs.set(param, 1);
|
|
}
|
|
}
|
|
|
|
{
|
|
const featuresCache = this._cache.get('features');
|
|
if (featuresCache) {
|
|
for (const result of featuresCache.values()) {
|
|
if (typeof result === 'object'
|
|
&& result !== null
|
|
&& Array.isArray(result.libs)) {
|
|
for (const lib of result.libs)
|
|
libs.set(lib, 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Array.from(libs.keys());
|
|
}
|
|
|
|
tryCompile(type, code, compilerParams) {
|
|
validateType(type);
|
|
if (typeof code !== 'string')
|
|
throw new TypeError('Invalid code argument');
|
|
|
|
type = (type === 'c' ? 'c' : 'c++');
|
|
const prop = (type === 'c' ? '_cc' : '_cxx');
|
|
|
|
if (this[`${prop}Kind`] === undefined)
|
|
getKind.call(this, prop);
|
|
|
|
if (!Array.isArray(compilerParams))
|
|
compilerParams = [];
|
|
|
|
let result;
|
|
if (this[`${prop}Kind`] === 'msvc') {
|
|
const cmpOpts = [`-Fo${this._tmpObjFile}`];
|
|
for (const includePath of this._includePaths)
|
|
cmpOpts.push('-I', includePath);
|
|
const lnkOpts = [];
|
|
for (const libPath of this._libPaths)
|
|
lnkOpts.push(`-LIBPATH:${libPath}`);
|
|
for (const opt of compilerParams) {
|
|
let m;
|
|
if (m = /^[-/]l(.+)$/.exec(opt))
|
|
lnkOpts.push(m[1]);
|
|
else
|
|
cmpOpts.push(opt);
|
|
}
|
|
try {
|
|
writeFileSync(this._tmpInFile, code);
|
|
const args = [
|
|
...cmpOpts,
|
|
`-T${prop === '_cc' ? 'c' : 'p'}`,
|
|
this._tmpInFile,
|
|
'-link',
|
|
`-out:${this._tmpOutFile}`,
|
|
...lnkOpts,
|
|
];
|
|
result = spawnSync(
|
|
this[prop],
|
|
args,
|
|
this[`${prop}SpawnOpts`]
|
|
);
|
|
unlinkSync(this._tmpInFile);
|
|
// Overwrite stderr with stdout because MSVC seems to print
|
|
// errors to stdout instead for some reason
|
|
result.stderr = result.stdout;
|
|
} catch (ex) {
|
|
// We had trouble writing or deleting a temp file, fake
|
|
// the result
|
|
result = { status: Infinity, stderr: ex.stack };
|
|
}
|
|
try { unlinkSync(this._tmpObjFile); } catch {}
|
|
try { unlinkSync(this._tmpOutFile); } catch {}
|
|
} else {
|
|
result = spawnSync(
|
|
this[prop],
|
|
[
|
|
'-x', type,
|
|
'-o', '/dev/null',
|
|
'-',
|
|
...compilerParams,
|
|
],
|
|
{
|
|
...this[`${prop}SpawnOpts`],
|
|
input: code,
|
|
}
|
|
);
|
|
}
|
|
|
|
if (result.status === 0)
|
|
return true;
|
|
|
|
const err = new Error('Compilation failed');
|
|
err.output = result.stderr;
|
|
return err;
|
|
}
|
|
}
|
|
|
|
function validateType(type) {
|
|
if (!['c', 'c++'].includes(type))
|
|
throw new Error('Invalid type argument');
|
|
}
|
|
|
|
function getCachedValue(type, cache, subtype, key) {
|
|
if (cache === null)
|
|
return;
|
|
|
|
const typeCache = cache.get(type);
|
|
if (!typeCache)
|
|
return;
|
|
|
|
const subtypeCache = (typeof subtype !== 'string'
|
|
? typeCache
|
|
: typeCache.get(subtype));
|
|
if (!subtypeCache)
|
|
return;
|
|
|
|
return subtypeCache.get(key);
|
|
}
|
|
|
|
function setCachedValue(type, cache, subtype, key, value) {
|
|
if (cache === null)
|
|
return;
|
|
|
|
let typeCache = cache.get(type);
|
|
if (!typeCache)
|
|
cache.set(type, typeCache = new Map());
|
|
|
|
let subtypeCache = (typeof subtype !== 'string'
|
|
? typeCache
|
|
: typeCache.get(subtype));
|
|
if (!subtypeCache)
|
|
typeCache.set(subtype, subtypeCache = new Map());
|
|
|
|
subtypeCache.set(key, value);
|
|
}
|
|
|
|
function renderHeaders(headers) {
|
|
let ret = '';
|
|
|
|
if (Array.isArray(headers)) {
|
|
for (const header of headers) {
|
|
if (typeof header !== 'string' || !header)
|
|
throw new Error(`Invalid header: ${inspect(header)}`);
|
|
ret += `#include ${normalizeHeader(header)}\n`;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function normalizeHeader(header) {
|
|
if (!RE_HEADER_DECORATED.test(header))
|
|
header = `<${header}>`;
|
|
return header;
|
|
}
|
|
|
|
const DEFAULT_HEADERS_POSIX = [
|
|
'stdio.h',
|
|
'sys/types.h',
|
|
'sys/stat.h',
|
|
'stdlib.h',
|
|
'stddef.h',
|
|
'memory.h',
|
|
'string.h',
|
|
'strings.h',
|
|
'inttypes.h',
|
|
'stdint.h',
|
|
'unistd.h'
|
|
];
|
|
const DEFAULT_HEADERS_MSVC = [
|
|
'windows.h',
|
|
];
|
|
function getDefaultHeaders(be, type) {
|
|
const prop = (type === 'c' ? '_cc' : '_cxx');
|
|
if (be[`${prop}Kind`] === undefined)
|
|
getKind.call(be, prop);
|
|
|
|
let headers;
|
|
if (be[`${prop}Kind`] === 'msvc')
|
|
headers = DEFAULT_HEADERS_MSVC;
|
|
else
|
|
headers = DEFAULT_HEADERS_POSIX;
|
|
|
|
return headers.filter((hdr) => be.checkHeader(type, hdr));
|
|
}
|
|
|
|
const features = new Map(Object.entries({
|
|
'strerror_r': (be) => {
|
|
const defines = [];
|
|
let returnsCharPtr = false;
|
|
|
|
const declared = be.checkDeclared('c', 'strerror_r');
|
|
if (declared) {
|
|
const code = `
|
|
${renderHeaders(getDefaultHeaders(be, 'c'))}
|
|
|
|
int
|
|
main ()
|
|
{
|
|
|
|
char buf[100];
|
|
char x = *strerror_r (0, buf, sizeof buf);
|
|
char *p = strerror_r (0, buf, sizeof buf);
|
|
return !p || x;
|
|
|
|
;
|
|
return 0;
|
|
}`;
|
|
returnsCharPtr = (be.tryCompile('c', code) === true);
|
|
if (returnsCharPtr)
|
|
defines.push('STRERROR_R_CHAR_P');
|
|
}
|
|
|
|
return {
|
|
defines,
|
|
val: { declared, returnsCharPtr }
|
|
};
|
|
},
|
|
}));
|
|
|
|
function makeDefine(name, rendered) {
|
|
name = name.replace(/[*]/g, 'P')
|
|
.replace(/[^_A-Za-z0-9]/g, '_')
|
|
.toUpperCase();
|
|
return (rendered ? `HAVE_${name}=1` : name);
|
|
}
|
|
|
|
module.exports = BuildEnvironment;
|