blob: 888d6ec4f47b5d73ad59b09e5bad95a55bded052 [file] [log] [blame]
'use strict';
const { writeFile } = require('fs/promises');
const { Readable } = require('stream');
const { resolve } = require('path');
const { parseArgs } = require('util')
const { createInterface } = require('readline');
const { inspect } = require('util');
const { runClang } = require('../lib/clang-utils');
const { evaluate } = require('../lib/parse-utils');
/**
* @returns {Promise<string>} Version string, eg. `'v19.6.0'`.
*/
async function getLatestReleaseVersion() {
const response = await fetch('https://nodejs.org/download/release/index.json');
const json = await response.json();
return json[0].version;
}
/**
* @param {NodeJS.ReadableStream} stream
* @param {string} destination
* @param {boolean} verbose
* @returns {Promise<void>} The `writeFile` Promise.
*/
function removeExperimentals(stream, destination, verbose = false) {
return new Promise((resolve, reject) => {
const debug = (...args) => {
if (verbose) {
console.log(...args);
}
};
const rl = createInterface(stream);
/** @type {Array<'write' | 'ignore' | 'preprocessor'>} */
const mode = ['write'];
/** @type {Array<string>} */
const preprocessor = [];
/** @type {Array<string>} */
const macroStack = [];
/** @type {RegExpMatchArray | null} */
let matches;
let lineNumber = 0;
let toWrite = '';
const handlePreprocessor = (expression) => {
const result = evaluate(expression);
macroStack.push(expression);
if (result === false) {
debug(`Line ${lineNumber} Ignored '${expression}'`);
mode.push('ignore');
return false;
} else {
debug(`Line ${lineNumber} Pushed '${expression}'`);
mode.push('write');
return true;
}
};
rl.on('line', function lineHandler(line) {
++lineNumber;
if (matches = line.match(/^\s*#if(n)?def\s+([A-Za-z_][A-Za-z0-9_]*)/)) {
const negated = Boolean(matches[1]);
const identifier = matches[2];
macroStack.push(identifier);
if (mode.length && mode[mode.length - 1] === 'ignore') {
debug(`Line ${lineNumber} Continued-Ignored ${identifier}`);
mode.push('ignore');
return;
}
debug(`Line ${lineNumber} Pushed ${identifier}`);
if (identifier === 'NAPI_EXPERIMENTAL') {
if (negated) {
mode.push('write');
} else {
mode.push('ignore');
}
return;
} else {
mode.push('write');
}
}
else if (matches = line.match(/^\s*#if\s+(.+)$/)) {
const expression = matches[1];
if (expression.endsWith('\\')) {
if (preprocessor.length) {
reject(new Error(`Unexpected preprocessor continuation on line ${lineNumber}`));
return;
}
preprocessor.push(expression.substring(0, expression.length - 1));
mode.push('preprocessor');
return;
} else {
if (!handlePreprocessor(expression)) {
return;
}
}
}
else if (line.match(/^#else(?:\s+|$)/)) {
const identifier = macroStack[macroStack.length - 1];
debug(`Line ${lineNumber} Peeked ${identifier}`);
if (!identifier) {
rl.off('line', lineHandler);
reject(new Error(`Macro stack is empty handling #else on line ${lineNumber}`));
return;
}
if (identifier.indexOf('NAPI_EXPERIMENTAL') > -1) {
const lastMode = mode[mode.length - 1];
mode[mode.length - 1] = (lastMode === 'ignore') ? 'write' : 'ignore';
return;
}
}
else if (line.match(/^\s*#endif(?:\s+|$)/)) {
const identifier = macroStack.pop();
mode.pop();
debug(`Line ${lineNumber} Popped ${identifier}`);
if (!identifier) {
rl.off('line', lineHandler);
reject(new Error(`Macro stack is empty handling #endif on line ${lineNumber}`));
return;
}
if (identifier.indexOf('NAPI_EXPERIMENTAL') > -1) {
return;
}
}
if (mode.length === 0) {
rl.off('line', lineHandler);
reject(new Error(`Write mode empty handling #endif on line ${lineNumber}`));
return;
}
if (mode[mode.length - 1] === 'write') {
toWrite += `${line}\n`;
} else if (mode[mode.length - 1] === 'preprocessor') {
if (!preprocessor) {
reject(new Error(`Preprocessor mode without preprocessor on line ${lineNumber}`));
return;
}
if (line.endsWith('\\')) {
preprocessor.push(line.substring(0, line.length - 1));
return;
}
preprocessor.push(line);
const expression = preprocessor.join('');
preprocessor.length = 0;
mode.pop();
if (!handlePreprocessor(expression)) {
return;
}
}
});
rl.on('close', () => {
if (macroStack.length > 0) {
reject(new Error(`Macro stack is not empty at EOF: ${inspect(macroStack)}`));
}
else if (mode.length > 1) {
reject(new Error(`Write mode greater than 1 at EOF: ${inspect(mode)}`));
}
else if (toWrite.match(/^\s*#if(?:n)?def\s+NAPI_EXPERIMENTAL/m)) {
reject(new Error(`Output has match for NAPI_EXPERIMENTAL`));
}
else {
resolve(writeFile(destination, toWrite));
}
});
});
}
/**
* Validate syntax for a file using clang.
* @param {string} path Path for file to validate with clang.
*/
async function validateSyntax(path) {
try {
await runClang(['-fsyntax-only', path]);
} catch (e) {
throw new Error(`Syntax validation failed for ${path}: ${e}`);
}
}
async function main() {
const { values: { tag, verbose } } = parseArgs({
options: {
tag: {
type: 'string',
short: 't',
default: await getLatestReleaseVersion()
},
verbose: {
type: 'boolean',
short: 'v',
},
},
});
console.log(`feat: update headers from nodejs/node tag ${tag}`);
const files = ['js_native_api_types.h', 'js_native_api.h', 'node_api_types.h', 'node_api.h'];
for (const filename of files) {
const url = `https://raw.githubusercontent.com/nodejs/node/${tag}/src/${filename}`;
const path = resolve(__dirname, '..', 'include', filename);
if (verbose) {
console.log(` ${url} -> ${path}`);
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Fetch of ${url} returned ${response.status} ${response.statusText}`);
}
await removeExperimentals(Readable.fromWeb(response.body), path, verbose);
await validateSyntax(path);
}
}
main().catch(e => {
console.error(e);
process.exitCode = 1;
});