Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper scripts for updating headers and symbols.js #7

Merged
merged 10 commits into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .github/workflows/sync-headers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Header Sync

on:
workflow_dispatch: null
schedule:
- cron: "0 0 * * *"

permissions:
contents: write
pull-requests: write

jobs:
build:
runs-on: ubuntu-latest
name: Update headers from nodejs/node
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- shell: bash
id: check-changes
name: Check Changes
run: |
COMMIT_MESSAGE=$(npm run --silent update-headers)
VERSION=${COMMIT_MESSAGE##* }
echo $COMMIT_MESSAGE
npm run --silent write-symbols
CHANGED_FILES=$(git diff --name-only)
BRANCH_NAME="update-headers/${VERSION}"
if [ -z "$CHANGED_FILES" ]; then
echo "No changes exist. Nothing to do."
else
echo "Changes exist. Checking if branch exists: $BRANCH_NAME"
if git ls-remote --exit-code --heads $GITHUB_SERVER_URL/$GITHUB_REPOSITORY $BRANCH_NAME >/dev/null; then
echo "Branch exists. Nothing to do."
else
echo "Branch does not exists."
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
echo "COMMIT_MESSAGE=$COMMIT_MESSAGE" >> $GITHUB_OUTPUT
fi
fi
- name: Create Pull Request
uses: peter-evans/create-pull-request@v4
if: ${{ steps.check-changes.outputs.BRANCH_NAME }}
with:
branch: ${{ steps.check-changes.outputs.BRANCH_NAME }}
commit-message: ${{ steps.check-changes.outputs.COMMIT_MESSAGE }}
title: ${{ steps.check-changes.outputs.COMMIT_MESSAGE }}
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
body: null
delete-branch: true
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
scripts/
.github/
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"url": "git://github.com/nodejs/node-api-headers.git"
},
"scripts": {
"update-headers": "node --no-warnings scripts/update-headers.js",
"write-symbols": "node --no-warnings scripts/write-symbols.js"
},
"version": "0.0.2",
"support": true
Expand Down
50 changes: 50 additions & 0 deletions scripts/clang-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

const { spawn } = require('child_process');

/**
* @param {Array<string>} [args]
* @returns {Promise<{exitCode: number | null, stdout: string, stderr: string}>}
*/
async function runClang(args = []) {
try {
const { exitCode, stdout, stderr } = await new Promise((resolve, reject) => {
const spawned = spawn('clang',
['-Xclang', ...args]
);

let stdout = '';
let stderr = '';

spawned.stdout?.on('data', (data) => {
stdout += data.toString('utf-8');
});
spawned.stderr?.on('data', (data) => {
stderr += data.toString('utf-8');
});

spawned.on('exit', function (exitCode) {
resolve({ exitCode, stdout, stderr });
});

spawned.on('error', function (err) {
reject(err);
});
});

if (exitCode !== 0) {
throw new Error(`clang exited with non-zero exit code ${exitCode}. stderr: ${stderr ? stderr : '<empty>'}`);
}

return { exitCode, stdout, stderr };
} catch (err) {
if (err.code === 'ENOENT') {
throw new Error('This tool requires clang to be installed.');
}
throw err;
}
}

module.exports = {
runClang
};
189 changes: 189 additions & 0 deletions scripts/update-headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
'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('./clang-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'>} */
let mode = ['write'];

/** @type {Array<string>} */
const macroStack = [];

/** @type {RegExpMatchArray | null} */
let matches;

let lineNumber = 0;
let toWrite = '';

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);

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 identifier = matches[1];
macroStack.push(identifier);
mode.push('write');

debug(`Line ${lineNumber} Pushed ${identifier}`);
}
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 === 'NAPI_EXPERIMENTAL') {
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}`));
}

if (identifier === 'NAPI_EXPERIMENTAL') {
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`;
}
});

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(`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;
});
Loading