Skip to content

Commit ecefbdd

Browse files
authored
Add helper scripts for updating headers and symbols.js (#7)
1 parent 9b9e2b7 commit ecefbdd

File tree

6 files changed

+449
-0
lines changed

6 files changed

+449
-0
lines changed

.github/workflows/sync-headers.yml

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Header Sync
2+
3+
on:
4+
workflow_dispatch: null
5+
schedule:
6+
- cron: "0 0 * * *"
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
name: Update headers from nodejs/node
16+
steps:
17+
- uses: actions/checkout@v3
18+
- uses: actions/setup-node@v3
19+
with:
20+
node-version: 18
21+
- shell: bash
22+
id: check-changes
23+
name: Check Changes
24+
run: |
25+
COMMIT_MESSAGE=$(npm run --silent update-headers)
26+
VERSION=${COMMIT_MESSAGE##* }
27+
echo $COMMIT_MESSAGE
28+
npm run --silent write-symbols
29+
CHANGED_FILES=$(git diff --name-only)
30+
BRANCH_NAME="update-headers/${VERSION}"
31+
if [ -z "$CHANGED_FILES" ]; then
32+
echo "No changes exist. Nothing to do."
33+
else
34+
echo "Changes exist. Checking if branch exists: $BRANCH_NAME"
35+
if git ls-remote --exit-code --heads $GITHUB_SERVER_URL/$GITHUB_REPOSITORY $BRANCH_NAME >/dev/null; then
36+
echo "Branch exists. Nothing to do."
37+
else
38+
echo "Branch does not exists."
39+
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
40+
echo "COMMIT_MESSAGE=$COMMIT_MESSAGE" >> $GITHUB_OUTPUT
41+
fi
42+
fi
43+
- name: Create Pull Request
44+
uses: peter-evans/create-pull-request@v4
45+
if: ${{ steps.check-changes.outputs.BRANCH_NAME }}
46+
with:
47+
branch: ${{ steps.check-changes.outputs.BRANCH_NAME }}
48+
commit-message: ${{ steps.check-changes.outputs.COMMIT_MESSAGE }}
49+
title: ${{ steps.check-changes.outputs.COMMIT_MESSAGE }}
50+
author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
51+
body: null
52+
delete-branch: true

.npmignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
scripts/
2+
.github/

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
"url": "git://github.com/nodejs/node-api-headers.git"
4747
},
4848
"scripts": {
49+
"update-headers": "node --no-warnings scripts/update-headers.js",
50+
"write-symbols": "node --no-warnings scripts/write-symbols.js"
4951
},
5052
"version": "0.0.2",
5153
"support": true

scripts/clang-utils.js

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
'use strict';
2+
3+
const { spawn } = require('child_process');
4+
5+
/**
6+
* @param {Array<string>} [args]
7+
* @returns {Promise<{exitCode: number | null, stdout: string, stderr: string}>}
8+
*/
9+
async function runClang(args = []) {
10+
try {
11+
const { exitCode, stdout, stderr } = await new Promise((resolve, reject) => {
12+
const spawned = spawn('clang',
13+
['-Xclang', ...args]
14+
);
15+
16+
let stdout = '';
17+
let stderr = '';
18+
19+
spawned.stdout?.on('data', (data) => {
20+
stdout += data.toString('utf-8');
21+
});
22+
spawned.stderr?.on('data', (data) => {
23+
stderr += data.toString('utf-8');
24+
});
25+
26+
spawned.on('exit', function (exitCode) {
27+
resolve({ exitCode, stdout, stderr });
28+
});
29+
30+
spawned.on('error', function (err) {
31+
reject(err);
32+
});
33+
});
34+
35+
if (exitCode !== 0) {
36+
throw new Error(`clang exited with non-zero exit code ${exitCode}. stderr: ${stderr ? stderr : '<empty>'}`);
37+
}
38+
39+
return { exitCode, stdout, stderr };
40+
} catch (err) {
41+
if (err.code === 'ENOENT') {
42+
throw new Error('This tool requires clang to be installed.');
43+
}
44+
throw err;
45+
}
46+
}
47+
48+
module.exports = {
49+
runClang
50+
};

scripts/update-headers.js

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
'use strict';
2+
3+
const { writeFile } = require('fs/promises');
4+
const { Readable } = require('stream');
5+
const { resolve } = require('path');
6+
const { parseArgs } = require('util')
7+
const { createInterface } = require('readline');
8+
const { inspect } = require('util');
9+
const { runClang } = require('./clang-utils');
10+
11+
/**
12+
* @returns {Promise<string>} Version string, eg. `'v19.6.0'`.
13+
*/
14+
async function getLatestReleaseVersion() {
15+
const response = await fetch('https://nodejs.org/download/release/index.json');
16+
const json = await response.json();
17+
return json[0].version;
18+
}
19+
20+
/**
21+
* @param {NodeJS.ReadableStream} stream
22+
* @param {string} destination
23+
* @param {boolean} verbose
24+
* @returns {Promise<void>} The `writeFile` Promise.
25+
*/
26+
function removeExperimentals(stream, destination, verbose = false) {
27+
return new Promise((resolve, reject) => {
28+
const debug = (...args) => {
29+
if (verbose) {
30+
console.log(...args);
31+
}
32+
};
33+
const rl = createInterface(stream);
34+
35+
/** @type {Array<'write' | 'ignore'>} */
36+
let mode = ['write'];
37+
38+
/** @type {Array<string>} */
39+
const macroStack = [];
40+
41+
/** @type {RegExpMatchArray | null} */
42+
let matches;
43+
44+
let lineNumber = 0;
45+
let toWrite = '';
46+
47+
rl.on('line', function lineHandler(line) {
48+
++lineNumber;
49+
if (matches = line.match(/^\s*#if(n)?def\s+([A-Za-z_][A-Za-z0-9_]*)/)) {
50+
const negated = Boolean(matches[1]);
51+
const identifier = matches[2];
52+
macroStack.push(identifier);
53+
54+
debug(`Line ${lineNumber} Pushed ${identifier}`);
55+
56+
if (identifier === 'NAPI_EXPERIMENTAL') {
57+
if (negated) {
58+
mode.push('write');
59+
} else {
60+
mode.push('ignore');
61+
}
62+
return;
63+
} else {
64+
mode.push('write');
65+
}
66+
67+
}
68+
else if (matches = line.match(/^\s*#if\s+(.+)$/)) {
69+
const identifier = matches[1];
70+
macroStack.push(identifier);
71+
mode.push('write');
72+
73+
debug(`Line ${lineNumber} Pushed ${identifier}`);
74+
}
75+
else if (line.match(/^#else(?:\s+|$)/)) {
76+
const identifier = macroStack[macroStack.length - 1];
77+
78+
debug(`Line ${lineNumber} Peeked ${identifier}`);
79+
80+
if (!identifier) {
81+
rl.off('line', lineHandler);
82+
reject(new Error(`Macro stack is empty handling #else on line ${lineNumber}`));
83+
return;
84+
}
85+
86+
if (identifier === 'NAPI_EXPERIMENTAL') {
87+
const lastMode = mode[mode.length - 1];
88+
mode[mode.length - 1] = (lastMode === 'ignore') ? 'write' : 'ignore';
89+
return;
90+
}
91+
}
92+
else if (line.match(/^\s*#endif(?:\s+|$)/)) {
93+
const identifier = macroStack.pop();
94+
mode.pop();
95+
96+
debug(`Line ${lineNumber} Popped ${identifier}`);
97+
98+
if (!identifier) {
99+
rl.off('line', lineHandler);
100+
reject(new Error(`Macro stack is empty handling #endif on line ${lineNumber}`));
101+
}
102+
103+
if (identifier === 'NAPI_EXPERIMENTAL') {
104+
return;
105+
}
106+
}
107+
108+
if (mode.length === 0) {
109+
rl.off('line', lineHandler);
110+
reject(new Error(`Write mode empty handling #endif on line ${lineNumber}`));
111+
return;
112+
}
113+
114+
if (mode[mode.length - 1] === 'write') {
115+
toWrite += `${line}\n`;
116+
}
117+
});
118+
119+
rl.on('close', () => {
120+
if (macroStack.length > 0) {
121+
reject(new Error(`Macro stack is not empty at EOF: ${inspect(macroStack)}`));
122+
}
123+
else if (mode.length > 1) {
124+
reject(new Error(`Write mode greater than 1 at EOF: ${inspect(mode)}`));
125+
}
126+
else if (toWrite.match(/^\s*#if(?:n)?def\s+NAPI_EXPERIMENTAL/m)) {
127+
reject(new Error(`Output has match for NAPI_EXPERIMENTAL`));
128+
}
129+
else {
130+
resolve(writeFile(destination, toWrite));
131+
}
132+
});
133+
});
134+
}
135+
136+
/**
137+
* Validate syntax for a file using clang.
138+
* @param {string} path Path for file to validate with clang.
139+
*/
140+
async function validateSyntax(path) {
141+
try {
142+
await runClang(['-fsyntax-only', path]);
143+
} catch (e) {
144+
throw new Error(`Syntax validation failed for ${path}: ${e}`);
145+
}
146+
}
147+
148+
async function main() {
149+
const { values: { tag, verbose } } = parseArgs({
150+
options: {
151+
tag: {
152+
type: 'string',
153+
short: 't',
154+
default: await getLatestReleaseVersion()
155+
},
156+
verbose: {
157+
type: 'boolean',
158+
short: 'v',
159+
},
160+
},
161+
});
162+
163+
console.log(`Update headers from nodejs/node tag ${tag}`);
164+
165+
const files = ['js_native_api_types.h', 'js_native_api.h', 'node_api_types.h', 'node_api.h'];
166+
167+
for (const filename of files) {
168+
const url = `https://raw.githubusercontent.com/nodejs/node/${tag}/src/${filename}`;
169+
const path = resolve(__dirname, '..', 'include', filename);
170+
171+
if (verbose) {
172+
console.log(` ${url} -> ${path}`);
173+
}
174+
175+
const response = await fetch(url);
176+
if (!response.ok) {
177+
throw new Error(`Fetch of ${url} returned ${response.status} ${response.statusText}`);
178+
}
179+
180+
await removeExperimentals(Readable.fromWeb(response.body), path, verbose);
181+
182+
await validateSyntax(path);
183+
}
184+
}
185+
186+
main().catch(e => {
187+
console.error(e);
188+
process.exitCode = 1;
189+
});

0 commit comments

Comments
 (0)