crossbind 2.0.0-beta.73 → 2.0.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -3
- package/src/actions/buildWasm.js +16 -1
- package/src/actions/createInterface.js +266 -38
- package/src/actions/createLib.js +4 -1
- package/src/actions/getCmakeParameters.js +0 -1
- package/src/actions/licenses.js +94 -22
- package/src/actions/pageLicenses.js +89 -0
- package/src/actions/run.js +70 -5
- package/src/assets/js-runtime/adapters/worker-comlink.js +11 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +12 -5
- package/src/index.js +1 -0
- package/src/licenses/index.d.ts +16 -0
- package/src/licenses/index.js +15 -0
- package/src/state/loadConfig.js +5 -0
- package/src/utils/bridgeRuntimeFixes.js +24 -11
- package/src/utils/cppDts.js +19 -13
- package/src/utils/embindPointerOwners.js +42 -0
- package/src/utils/licenseReport.js +12 -5
- package/src/utils/packageLicense.js +14 -28
- package/src/utils/relinking.js +50 -0
- package/src/utils/swigInterface.js +22 -7
- package/src/utils/typeClosure.js +170 -0
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
// A const char* followed by an integer may be a byte span, with embedded NULs or no terminator.
|
|
2
|
+
// Preserve its address; JavaScript can read exactly the reported length with readBuffer. The argument keeps its const for
|
|
3
|
+
// callbackArgument, which takes it by reference: a callback's const std::vector<std::string> & binds to no other.
|
|
4
|
+
const CALLBACK_ARGUMENT_AT = `template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
|
|
5
|
+
using Arg = std::remove_reference_t<decltype(std::get<I>(values))>;
|
|
6
|
+
using A = std::remove_cv_t<Arg>;
|
|
7
|
+
if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
|
|
8
|
+
using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
|
|
9
|
+
if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
|
|
10
|
+
else return callbackArgument<Arg>(std::get<I>(values));
|
|
11
|
+
} else return callbackArgument<Arg>(std::get<I>(values));
|
|
12
|
+
}`;
|
|
13
|
+
// The form a bridge cached before the argument kept its const still holds.
|
|
14
|
+
const CALLBACK_ARGUMENT_AT_WITHOUT_CONST = `template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
|
|
15
|
+
using A = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I>(values))>>;
|
|
16
|
+
if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
|
|
17
|
+
using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
|
|
18
|
+
if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
|
|
19
|
+
else return callbackArgument<A>(std::get<I>(values));
|
|
20
|
+
} else return callbackArgument<A>(std::get<I>(values));
|
|
21
|
+
}`;
|
|
22
|
+
|
|
1
23
|
// Adapt the runtime emitted by the pinned SWIG fork. Kept in the generator so cache rebuilds
|
|
2
24
|
// produce the same runtime in every bridge, including builds on a remote runner.
|
|
3
25
|
export default function fixBridgeRuntime(text) {
|
|
@@ -22,16 +44,7 @@ template<typename T> constexpr bool isPassedByAddress =`);
|
|
|
22
44
|
}
|
|
23
45
|
if (!text.includes('auto callbackArgumentAt(')) {
|
|
24
46
|
text = '#include <tuple>\n' + text;
|
|
25
|
-
text = text.replace('#ifndef __EMSCRIPTEN__\nstruct RuntimeCallbacks {',
|
|
26
|
-
// Preserve its address; JavaScript can read exactly the reported length with readBuffer.
|
|
27
|
-
template<size_t I, typename Tuple> auto callbackArgumentAt(Tuple values) {
|
|
28
|
-
using A = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I>(values))>>;
|
|
29
|
-
if constexpr (std::is_same_v<A, const char *> && I + 1 < std::tuple_size_v<Tuple>) {
|
|
30
|
-
using Next = std::remove_cv_t<std::remove_reference_t<decltype(std::get<I + 1>(values))>>;
|
|
31
|
-
if constexpr (std::is_integral_v<Next>) return toHandle(std::get<I>(values));
|
|
32
|
-
else return callbackArgument<A>(std::get<I>(values));
|
|
33
|
-
} else return callbackArgument<A>(std::get<I>(values));
|
|
34
|
-
}
|
|
47
|
+
text = text.replace('#ifndef __EMSCRIPTEN__\nstruct RuntimeCallbacks {', `${CALLBACK_ARGUMENT_AT}
|
|
35
48
|
#ifndef __EMSCRIPTEN__
|
|
36
49
|
struct RuntimeCallbacks {`);
|
|
37
50
|
text = text.replaceAll('template<typename R, typename... A> R invokeCallback(size_t slot, A... args) {',
|
|
@@ -44,5 +57,5 @@ struct RuntimeCallbacks {`);
|
|
|
44
57
|
}
|
|
45
58
|
template<typename F> struct Callback {`);
|
|
46
59
|
}
|
|
47
|
-
return text;
|
|
60
|
+
return text.replace(CALLBACK_ARGUMENT_AT_WITHOUT_CONST, CALLBACK_ARGUMENT_AT);
|
|
48
61
|
}
|
package/src/utils/cppDts.js
CHANGED
|
@@ -200,18 +200,15 @@ export function parseCppSurface(source, log = console.log) {
|
|
|
200
200
|
|
|
201
201
|
export function emitCppDts(model, exportNames, mode = 'sync') {
|
|
202
202
|
const wrap = (t) => (mode === 'promise' ? `Promise<${t}>` : t);
|
|
203
|
-
const out = ['// Generated by crossbind - do not edit. Values are usable after
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
out.push(` delete(): ${wrap('void')};`);
|
|
213
|
-
out.push('}');
|
|
214
|
-
}
|
|
203
|
+
const out = ['// Generated by crossbind - do not edit. Values are usable after initNative() resolves.', ''];
|
|
204
|
+
// The module's toVector and toArray take vectors on every runtime, so the proxy's type is always declared.
|
|
205
|
+
out.push('/** An embind std::vector proxy. Convert with Module.toArray / Module.toVector. */');
|
|
206
|
+
out.push('export interface CppVector<T> {');
|
|
207
|
+
out.push(` size(): ${wrap('number')};`);
|
|
208
|
+
out.push(` get(index: number): ${wrap('T')};`);
|
|
209
|
+
out.push(` push_back(value: T): ${wrap('void')};`);
|
|
210
|
+
out.push(` delete(): ${wrap('void')};`);
|
|
211
|
+
out.push('}');
|
|
215
212
|
const emitted = new Set();
|
|
216
213
|
for (const cls of model.classes) {
|
|
217
214
|
if (!exportNames.includes(cls.name)) continue;
|
|
@@ -233,7 +230,16 @@ export function emitCppDts(model, exportNames, mode = 'sync') {
|
|
|
233
230
|
if (!emitted.has(name)) out.push(`export declare const ${name}: any;`);
|
|
234
231
|
}
|
|
235
232
|
out.push('export declare let AllSymbols: Record<string, unknown>;');
|
|
236
|
-
|
|
233
|
+
// A runtime's own members (FS in a wasm build, the file helpers in a browser) differ by runtime; the index signature
|
|
234
|
+
// keeps them reachable while the members every runtime has stay typed.
|
|
235
|
+
out.push('/** What initNative resolves to: the bound classes, toVector and toArray, and the members of the runtime it runs on. */');
|
|
236
|
+
out.push('export interface NativeModule {');
|
|
237
|
+
out.push(' [name: string]: unknown;');
|
|
238
|
+
for (const name of emitted) out.push(` ${name}: typeof ${name};`);
|
|
239
|
+
out.push(` toVector<T>(classOrName: unknown, array?: T[]): ${wrap('CppVector<T>')};`);
|
|
240
|
+
out.push(` toArray<T>(vector: CppVector<T> | T[]): ${wrap('T[]')};`);
|
|
241
|
+
out.push('}');
|
|
242
|
+
out.push('export declare function initNative(config?: Record<string, unknown>): Promise<NativeModule>;');
|
|
237
243
|
out.push('');
|
|
238
244
|
return out.join('\n');
|
|
239
245
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A pointer a member function or property hands out can point into the object it was called on, as a getter of a
|
|
2
|
+
// member behind a unique_ptr does, or name that object, as a fluent call does. JavaScript frees the object once
|
|
3
|
+
// nothing reaches its handle (utils/embindObjectLifetime.js), and the pointer then reads freed memory. The handle of
|
|
4
|
+
// such a pointer holds a clone of the object's handle, on the count the pointer's clones share, so the object goes
|
|
5
|
+
// with the last of them. Only a raw pointer, a NativePointer and an emscripten::val can borrow: a copy and a smart
|
|
6
|
+
// pointer hold what they point at. Static methods and free functions have no object to hold.
|
|
7
|
+
|
|
8
|
+
const HELPERS = 'function crossbindMayBorrow(type){if(!type)return false;if(type.name==="emscripten::val")return true;'
|
|
9
|
+
+ 'var cls=type.registeredClass;return!!cls&&!type.isReference&&(!type.isSmartPointer||cls.name==="NativePointer")}'
|
|
10
|
+
+ 'function crossbindKeepOwner(value,owner){var $$=value&&value.$$;var held=owner&&owner.$$;'
|
|
11
|
+
+ 'if(!$$||!$$.count||$$.count.owner||!held||!held.ptr||held.preservePointerOnDelete||$$.count===held.count)return value;'
|
|
12
|
+
+ 'if($$.smartPtr?$$.ptrType.registeredClass.name==="NativePointer":!$$.count.owned)$$.count.owner=owner.clone();return value}'
|
|
13
|
+
+ 'function crossbindKeepsOwner(fn,type){if(!crossbindMayBorrow(type))return fn;'
|
|
14
|
+
+ 'var kept=function(...args){var owner=this;var value=fn.apply(owner,args);'
|
|
15
|
+
+ 'return value instanceof Promise?value.then(resolved=>crossbindKeepOwner(resolved,owner)):crossbindKeepOwner(value,owner)};'
|
|
16
|
+
+ 'Object.defineProperty(kept,"name",{value:fn.name});return kept}';
|
|
17
|
+
|
|
18
|
+
// The release glue spells each on one line, the debug glue with spaces and quoted property names. A release glue
|
|
19
|
+
// names embind's registrations by the minified keys of the wasm imports, so a glue that registers class methods is
|
|
20
|
+
// known by the pure virtual check of the method registration, and one that registers properties by the getter's
|
|
21
|
+
// `this` check.
|
|
22
|
+
const CRAFT = /function craftInvokerFunction\s*\(/;
|
|
23
|
+
const MEMBER = /(var memberFunction\s*=\s*)(craftInvokerFunction\(\s*humanName,\s*argTypes,\s*classType,\s*rawInvoker,\s*context(?:,\s*isAsync)?\s*\))(\s*;)/g;
|
|
24
|
+
const GETTER = /return (getterReturnType(?:\.fromWireType|\[['"]fromWireType['"]\])\(\s*getter\(\s*getterContext,\s*ptr\s*\)\s*\))/g;
|
|
25
|
+
const METHODS = /pureVirtualFunctions\.push\(\s*methodName\s*\)/;
|
|
26
|
+
const PROPERTIES = /humanName\s*\+\s*['"] getter['"]/;
|
|
27
|
+
|
|
28
|
+
const count = (text, pattern) => text.match(pattern)?.length ?? 0;
|
|
29
|
+
|
|
30
|
+
// Returns the rewritten glue and whether it binds class methods or properties this rewrite does not recognise.
|
|
31
|
+
export function keepPointerOwners(glue) {
|
|
32
|
+
const hasMethods = METHODS.test(glue);
|
|
33
|
+
const hasProperties = PROPERTIES.test(glue);
|
|
34
|
+
if (!hasMethods && !hasProperties) return { text: glue, missed: false };
|
|
35
|
+
const recognised = CRAFT.test(glue) && (!hasMethods || count(glue, MEMBER) === 1) && (!hasProperties || count(glue, GETTER) === 1);
|
|
36
|
+
if (!recognised) return { text: glue, missed: true };
|
|
37
|
+
const text = glue
|
|
38
|
+
.replace(CRAFT, (craft) => `${HELPERS}${craft}`)
|
|
39
|
+
.replace(MEMBER, (_, assign, call, end) => `${assign}crossbindKeepsOwner(${call},argTypes[0])${end}`)
|
|
40
|
+
.replace(GETTER, (_, read) => `return crossbindKeepOwner(${read},this)`);
|
|
41
|
+
return { text, missed: false };
|
|
42
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import spdxParse from 'spdx-expression-parse';
|
|
2
|
+
import { lgplRows, relinkSection } from './relinking.js';
|
|
2
3
|
|
|
3
4
|
const COPYLEFT_PATTERN = /\b(AGPL|LGPL|GPL|MPL|EPL|EUPL|CDDL)\b/i;
|
|
4
5
|
|
|
@@ -43,16 +44,20 @@ export function formatNoticeSections(rows) {
|
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
// meta, the package that publishes the artifact (packageMeta), adds the relinking steps an LGPL library asks for.
|
|
48
|
+
export function formatNoticesMarkdown(rows, meta = null) {
|
|
47
49
|
const sections = formatNoticeSections(rows);
|
|
50
|
+
const relinked = meta?.repository ? lgplRows(rows) : [];
|
|
48
51
|
|
|
49
52
|
return [
|
|
50
53
|
'# Third-Party Notices',
|
|
51
54
|
'',
|
|
52
|
-
'GENERATED by `crossbind licenses`
|
|
55
|
+
'GENERATED by `crossbind licenses` - do not edit. This file states facts and is not legal advice; the',
|
|
56
|
+
'compliance assessment is yours.',
|
|
53
57
|
'',
|
|
54
58
|
'This artifact bundles the following native libraries through crossbind packages.',
|
|
55
59
|
'',
|
|
60
|
+
...(relinked.length ? [relinkSection({ ...meta, artifact: 'module', rows: relinked }), ''] : []),
|
|
56
61
|
sections.join('\n\n'),
|
|
57
62
|
'',
|
|
58
63
|
].join('\n');
|
|
@@ -89,11 +94,13 @@ export function formatCycloneDxSbom(rows, target = {}) {
|
|
|
89
94
|
},
|
|
90
95
|
components: [...rows]
|
|
91
96
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
92
|
-
.map((row) => ({
|
|
97
|
+
.map((row) => ({ row, version: row.nativeVersion ?? row.version }))
|
|
98
|
+
.map(({ row, version }) => ({
|
|
93
99
|
type: 'library',
|
|
94
100
|
name: row.name,
|
|
95
|
-
...(
|
|
96
|
-
...(
|
|
101
|
+
...(version ? { version } : {}),
|
|
102
|
+
...(version ? { purl: row.purl ?? `pkg:generic/${row.name}@${version}` } : {}),
|
|
103
|
+
...(row.cpe ? { cpe: row.cpe } : {}),
|
|
97
104
|
...(row.license ? { licenses: [{ expression: row.license }] } : {}),
|
|
98
105
|
...(row.sha256 ? { hashes: [{ alg: 'SHA-256', content: row.sha256 }] } : {}),
|
|
99
106
|
...(row.sourceUrl ? { externalReferences: [{ type: 'distribution', url: row.sourceUrl }] } : {}),
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { deriveLicenseExpression, formatCycloneDxSbom, formatNoticeSections } from './licenseReport.js';
|
|
4
|
+
import { lgplRows, relinkSection } from './relinking.js';
|
|
5
|
+
|
|
6
|
+
export { lgplRows };
|
|
4
7
|
|
|
5
8
|
export const SBOM_FILE = 'sbom.cdx.json';
|
|
6
9
|
const RULE = '-'.repeat(70);
|
|
7
10
|
|
|
8
|
-
export function lgplRows(rows) {
|
|
9
|
-
return rows.filter((row) => /\bLGPL\b/.test(row.license ?? ''));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
11
|
// The LLVM exception waives the notice for runtime parts compiled into a binary; every other
|
|
13
12
|
// component needs its text in the package.
|
|
14
13
|
function assertTexts(name, rows) {
|
|
@@ -22,36 +21,20 @@ function assertTexts(name, rows) {
|
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
const repo = repository.replace(/\.git$/, '');
|
|
30
|
-
return [
|
|
31
|
-
'## Relinking the LGPL libraries',
|
|
32
|
-
'',
|
|
33
|
-
`This addon links ${listOf(rows.map((row) => row.name))} statically; their sections below give their licenses.`,
|
|
34
|
-
'You may replace them with modified versions and relink the addon:',
|
|
35
|
-
'',
|
|
36
|
-
`1. Get the corresponding source. This package was built from ${repo} at the tag ${tag}:`,
|
|
37
|
-
` ${repo}/tree/${tag}`,
|
|
38
|
-
' The upstream sources are the tarballs the sections below list, with their SHA-256.',
|
|
39
|
-
`2. In ${projectPath}, point the library at your modified source with a crossbind.overrides.js file`,
|
|
40
|
-
' (docs/playbooks/override-dependencies.md in that repository).',
|
|
41
|
-
`3. Build the addon with \`pnpm install\` and \`pnpm --dir ${projectPath} build\`. Linux and Windows`,
|
|
42
|
-
' addons build in Docker, macOS addons on a Mac.',
|
|
43
|
-
`4. The build writes ${addonFile} in ${projectPath}; it replaces the file of that name in this package.`,
|
|
44
|
-
].join('\n');
|
|
24
|
+
// What a Node-API package's license covers: an addon package what its addon links; a package without an addon crossbind's
|
|
25
|
+
// JavaScript runtime, the bindings of the project's own code and the data of the libraries that declare some.
|
|
26
|
+
export function shippedRows(rows, { addonFile }) {
|
|
27
|
+
return addonFile ? rows : rows.filter((row) => row.isRuntime || !row.isPort || row.shipsData);
|
|
45
28
|
}
|
|
46
29
|
|
|
47
|
-
export function nodePackageLicense({ name, version, repository, projectPath, addonFile = null, rows }) {
|
|
30
|
+
export function nodePackageLicense({ name, version, repository, projectPath, addonFile = null, sourceTag = null, rows }) {
|
|
48
31
|
assertTexts(name, rows);
|
|
49
32
|
const expression = deriveLicenseExpression(rows);
|
|
50
33
|
const relinked = addonFile ? lgplRows(rows) : [];
|
|
51
34
|
const lines = [
|
|
52
35
|
addonFile
|
|
53
36
|
? `${name} ships a Node-API addon that links the components below statically.`
|
|
54
|
-
: `${name} ships the JavaScript bindings, types and data of the components below
|
|
37
|
+
: `${name} ships the JavaScript bindings, types and data of the components below. Each of its platform packages carries the LICENSE of the addon it holds.`,
|
|
55
38
|
'The full license text of every component follows.',
|
|
56
39
|
'',
|
|
57
40
|
'Effective license expression (also package.json "license"):',
|
|
@@ -65,7 +48,9 @@ export function nodePackageLicense({ name, version, repository, projectPath, add
|
|
|
65
48
|
'',
|
|
66
49
|
RULE,
|
|
67
50
|
'',
|
|
68
|
-
...(relinked.length ? [relinkSection({
|
|
51
|
+
...(relinked.length ? [relinkSection({
|
|
52
|
+
name, version, repository, projectPath, sourceTag, artifact: 'addon', file: addonFile, rows: relinked,
|
|
53
|
+
}), '', RULE, ''] : []),
|
|
69
54
|
formatNoticeSections(rows).join('\n\n'),
|
|
70
55
|
'',
|
|
71
56
|
];
|
|
@@ -91,7 +76,7 @@ export function writeNodePackageLicense(dir, meta) {
|
|
|
91
76
|
|
|
92
77
|
// What a Node-API package's license names, from its manifest and its place in the repository it builds
|
|
93
78
|
// from: an addon package's is the addon its own build writes.
|
|
94
|
-
export function nodePackageMeta({ package: manifest = {}, paths }) {
|
|
79
|
+
export function nodePackageMeta({ package: manifest = {}, paths, licenses }) {
|
|
95
80
|
const repository = typeof manifest.repository === 'string' ? manifest.repository : (manifest.repository?.url ?? '');
|
|
96
81
|
return {
|
|
97
82
|
name: manifest.name,
|
|
@@ -99,5 +84,6 @@ export function nodePackageMeta({ package: manifest = {}, paths }) {
|
|
|
99
84
|
repository,
|
|
100
85
|
projectPath: path.relative(paths.base, paths.project).split(path.sep).join('/') || '.',
|
|
101
86
|
addonFile: manifest.os && /\.node$/.test(manifest.main ?? '') ? manifest.main : null,
|
|
87
|
+
sourceTag: licenses?.sourceTag ?? null,
|
|
102
88
|
};
|
|
103
89
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// What a package that links LGPL libraries statically tells its user: where the corresponding source is and how to
|
|
2
|
+
// build the artifact again with a modified library. A Node-API addon's LICENSE and a wasm module's notices both carry it.
|
|
3
|
+
export const OVERRIDE_PLAYBOOK = 'https://github.com/crossbind/crossbind/blob/main/docs/playbooks/override-dependencies.md';
|
|
4
|
+
export const DEFAULT_SOURCE_TAG = '{name}@{version}';
|
|
5
|
+
|
|
6
|
+
export function lgplRows(rows) {
|
|
7
|
+
return rows.filter((row) => /\bLGPL\b/.test(row.license ?? ''));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const sourceTagOf = (format, name, version) => (format || DEFAULT_SOURCE_TAG)
|
|
11
|
+
.replaceAll('{name}', name)
|
|
12
|
+
.replaceAll('{version}', version);
|
|
13
|
+
|
|
14
|
+
const listOf = (names) => (names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names[0]);
|
|
15
|
+
|
|
16
|
+
const ARTIFACTS = {
|
|
17
|
+
addon: (file, projectPath) => ({
|
|
18
|
+
noun: 'addon',
|
|
19
|
+
verb: 'relink the addon',
|
|
20
|
+
where: 'Linux and Windows addons build in Docker, macOS addons on a Mac.',
|
|
21
|
+
result: `The build writes ${file} in ${projectPath}; it replaces the file of that name in this package.`,
|
|
22
|
+
}),
|
|
23
|
+
module: (_file, projectPath) => ({
|
|
24
|
+
noun: 'module',
|
|
25
|
+
verb: 'build the module again',
|
|
26
|
+
where: "The build runs in crossbind's toolchain: Docker, a remote runner or crossbind cloud.",
|
|
27
|
+
result: `The build writes the module's files in ${projectPath}/dist; they replace the files of those names in this package.`,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function relinkSection({ name, version, repository, projectPath, rows, sourceTag, artifact = 'addon', file = null }) {
|
|
32
|
+
const tag = sourceTagOf(sourceTag, name, version);
|
|
33
|
+
const repo = repository.replace(/\.git$/, '');
|
|
34
|
+
const { noun, verb, where, result } = ARTIFACTS[artifact](file, projectPath);
|
|
35
|
+
return [
|
|
36
|
+
'## Relinking the LGPL libraries',
|
|
37
|
+
'',
|
|
38
|
+
`This ${noun} links ${listOf(rows.map((row) => row.name))} statically; their sections below give their licenses.`,
|
|
39
|
+
`You may replace them with modified versions and ${verb}:`,
|
|
40
|
+
'',
|
|
41
|
+
`1. Get the corresponding source. This package was built from ${repo} at the tag ${tag}:`,
|
|
42
|
+
` ${repo}/tree/${tag}`,
|
|
43
|
+
' The upstream sources are the tarballs the sections below list, with their SHA-256.',
|
|
44
|
+
`2. In ${projectPath}, point the library at your modified source with a crossbind.overrides.js file`,
|
|
45
|
+
` (${OVERRIDE_PLAYBOOK}).`,
|
|
46
|
+
`3. Build the packages the ${noun} links first, as their dist/ folders are not in git, then the ${noun} with`,
|
|
47
|
+
` \`pnpm install\` and \`pnpm --dir ${projectPath} build\`. ${where}`,
|
|
48
|
+
`4. ${result}`,
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
@@ -65,7 +65,7 @@ export function interfaceIncludes(headerPath, prelude = []) {
|
|
|
65
65
|
// wraps just the imported header. The ignored declarations follow the macros, so the retry without macros keeps them.
|
|
66
66
|
export function buildInterfaceContent({
|
|
67
67
|
moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [], constants = [], preamble = [], functions = ALL_NAMES,
|
|
68
|
-
extras = [],
|
|
68
|
+
classes = ALL_NAMES, extras = [],
|
|
69
69
|
}) {
|
|
70
70
|
const includes = [...new Set([...interfaceIncludes(headerPath, prelude), ...completing])]
|
|
71
71
|
.map((header) => `#include "${header}"`)
|
|
@@ -89,7 +89,7 @@ ${extraBlock}%{
|
|
|
89
89
|
${includes}
|
|
90
90
|
%}
|
|
91
91
|
|
|
92
|
-
${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
|
|
92
|
+
${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}${classRequests(classes)}%feature("shared_ptr");
|
|
93
93
|
%feature("polymorphic_shared_ptr");
|
|
94
94
|
|
|
95
95
|
${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
|
|
@@ -104,17 +104,32 @@ function constantRequests(constants) {
|
|
|
104
104
|
return constants.length ? `${constants.map((name) => `%feature("embind:constant") ${name};`).join('\n')}\n\n` : '';
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay.
|
|
108
|
-
//
|
|
107
|
+
// SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. A
|
|
108
|
+
// function declared through a function typedef (OpenSSL's `OSSL_provider_init_fn OSSL_provider_init;`) reads as a
|
|
109
|
+
// variable, so global variables are ignored too, as rule 10 binds a mutable one only when imported. The package's own
|
|
110
|
+
// ignores come after, since SWIG applies the later of two renames of a name.
|
|
109
111
|
function functionRequests(functions) {
|
|
110
112
|
if (functions === ALL_NAMES) return '';
|
|
111
113
|
const names = [...new Set(functions)].sort();
|
|
112
|
-
return `%rename($ignore, %$isfunction, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
|
|
114
|
+
return `%rename($ignore, %$isfunction, %$isglobal) "";\n%rename($ignore, %$isvariable, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
//
|
|
117
|
+
// SWIG ignores every class, then wraps the classes the app's imports reach (see typeClosure.js) again. A class outside
|
|
118
|
+
// any namespace is wrapped as a class only, so a C function sharing its name (struct stat and stat()) stays out; SWIG
|
|
119
|
+
// matches a namespace's class by its full name only without that condition.
|
|
120
|
+
function classRequests(classes) {
|
|
121
|
+
if (classes === ALL_NAMES) return '';
|
|
122
|
+
const lines = [...new Set(classes)].sort()
|
|
123
|
+
.map((name) => (name.includes('::') ? `%rename("%s") ${name};\n` : `%rename("%s", %$isclass) ${name};\n`));
|
|
124
|
+
return `%rename($ignore, %$isclass) "";\n${lines.join('')}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Editor declarations and SWIG's parse for the bindings plan need the whole header even when the compiled bridge
|
|
128
|
+
// selects functions and classes by import.
|
|
116
129
|
export function interfaceWithAllFunctions(content) {
|
|
117
|
-
return content
|
|
130
|
+
return content
|
|
131
|
+
.replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\(\$ignore, %\$isvariable, %\$isglobal\) "";\n)?(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '')
|
|
132
|
+
.replace(/%rename\(\$ignore, %\$isclass\) "";\n(?:%rename\("%s"(?:, %\$isclass)?\) [A-Za-z_][\w:]*;\n)*\n/g, '');
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
export function withoutSwigMacros(content) {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { ALL_NAMES } from './headerImports.js';
|
|
2
|
+
|
|
3
|
+
// SWIG's own parse of a header (swig -xml), read for the classes a bridge has to register. embind registers each class
|
|
4
|
+
// once, in the bridge of the header defining it, and a binding naming a class no bridge registers fails only when it is
|
|
5
|
+
// called. So a header binds the classes its imported names reach: through their signatures, the bases of those classes
|
|
6
|
+
// and the signatures of their public members, in this header and in the headers defining them.
|
|
7
|
+
|
|
8
|
+
// SWIG leaves `>` unescaped inside attribute values (std::vector<(T)>), so a tag ends only outside a quoted value.
|
|
9
|
+
const TAG = /<(\/?)([A-Za-z_][\w-]*)((?:\s+[\w:-]+="[^"]*")*)\s*(\/?)>/g;
|
|
10
|
+
const ATTRIBUTE = /([\w:-]+)="([^"]*)"/g;
|
|
11
|
+
const NAME = /[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*/g;
|
|
12
|
+
const ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
|
|
13
|
+
// SWIG's type strings spell pointers, references, qualifiers, functions and arrays as p. r. z. q(...). f(...). a(...).
|
|
14
|
+
const NOT_TYPES = new Set([
|
|
15
|
+
'p', 'r', 'z', 'q', 'f', 'a', 'const', 'volatile', 'struct', 'class', 'union', 'enum', 'typename',
|
|
16
|
+
'unsigned', 'signed', 'char', 'short', 'int', 'long', 'float', 'double', 'void', 'bool', 'wchar_t', 'size_t',
|
|
17
|
+
]);
|
|
18
|
+
const EMPTY_TREE = { types: new Map(), values: new Map(), enumerators: new Map() };
|
|
19
|
+
|
|
20
|
+
const decode = (text) => text.replace(/&(lt|gt|amp|quot|apos);/g, (entity, name) => ENTITIES[name]);
|
|
21
|
+
|
|
22
|
+
function parse(xml) {
|
|
23
|
+
const root = { tag: 'root', attributes: {}, children: [] };
|
|
24
|
+
const open = [root];
|
|
25
|
+
for (const [, closing, tag, rest, selfClosing] of xml.matchAll(TAG)) {
|
|
26
|
+
if (closing) {
|
|
27
|
+
open.pop();
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const attributes = Object.fromEntries([...rest.matchAll(ATTRIBUTE)].map(([, key, value]) => [key, decode(value)]));
|
|
31
|
+
const node = { tag, attributes, children: [] };
|
|
32
|
+
open.at(-1).children.push(node);
|
|
33
|
+
if (!selfClosing) open.push(node);
|
|
34
|
+
}
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A node's own attributes are the <attribute> elements of its first <attributelist>.
|
|
39
|
+
const listOf = (node) => node.children.find((child) => child.tag === 'attributelist')?.children ?? [];
|
|
40
|
+
const attributesOf = (node) => Object.fromEntries(listOf(node).filter((child) => child.tag === 'attribute')
|
|
41
|
+
.map((child) => [child.attributes.name, child.attributes.value]));
|
|
42
|
+
const basesOf = (node) => listOf(node).filter((child) => child.tag === 'baselist')
|
|
43
|
+
.flatMap((list) => list.children.filter((child) => child.tag === 'base').map((base) => base.attributes.name));
|
|
44
|
+
const parametersOf = (node) => listOf(node).filter((child) => child.tag === 'parmlist')
|
|
45
|
+
.flatMap((list) => list.children.filter((child) => child.tag === 'parm').map((parm) => attributesOf(parm).type ?? ''));
|
|
46
|
+
|
|
47
|
+
// What a header declares: its classes, enums and typedefs by qualified name, each with the type strings it uses, its free
|
|
48
|
+
// functions and variables by name, and the enum of each enumerator. SWIG binds no nested class and no member a class
|
|
49
|
+
// keeps private, so neither reaches anything.
|
|
50
|
+
export function readSwigTree(xml) {
|
|
51
|
+
const types = new Map();
|
|
52
|
+
const values = new Map();
|
|
53
|
+
const enumerators = new Map();
|
|
54
|
+
// C names a struct and its typedef alike (`typedef struct PJ_INFO PJ_INFO;` ahead of `struct PJ_INFO {...}`), and the
|
|
55
|
+
// name stands for the struct.
|
|
56
|
+
const declare = (name, entry) => {
|
|
57
|
+
if (!types.has(name) || (types.get(name).kind === 'typedef' && entry.kind !== 'typedef')) types.set(name, entry);
|
|
58
|
+
};
|
|
59
|
+
const visit = (node, namespace, owner) => {
|
|
60
|
+
// %$isclass leaves class templates alone, so the selection does too.
|
|
61
|
+
if (node.tag === 'template') return;
|
|
62
|
+
const own = node.tag === 'root' ? {} : attributesOf(node);
|
|
63
|
+
const isPublic = own.access !== 'private' && own.access !== 'protected';
|
|
64
|
+
// SWIG spells a class or enum by its full name; a typedef inside a namespace or class may be spelled without it.
|
|
65
|
+
const qualified = (name) => (name.includes('::') || !namespace.length ? name : [...namespace, name].join('::'));
|
|
66
|
+
if (node.tag === 'namespace') {
|
|
67
|
+
node.children.forEach((child) => visit(child, own.name ? [...namespace, own.name] : namespace, null));
|
|
68
|
+
} else if (node.tag === 'class') {
|
|
69
|
+
if (!own.name || !isPublic || own.nested === '1') return;
|
|
70
|
+
const name = qualified(own.name);
|
|
71
|
+
// The name SWIG binds it under: a C struct's typedef name (X509V3_CONF_METHOD for X509V3_CONF_METHOD_st).
|
|
72
|
+
const bound = own.sym_name ?? own['sym:name'] ?? own.tdname ?? name.split('::').at(-1);
|
|
73
|
+
const entry = { kind: 'class', scope: name.split('::'), bases: basesOf(node), uses: [], bound };
|
|
74
|
+
declare(name, entry);
|
|
75
|
+
node.children.forEach((child) => visit(child, namespace, entry));
|
|
76
|
+
} else if (node.tag === 'enum') {
|
|
77
|
+
if (!own.name || !isPublic) return;
|
|
78
|
+
const name = owner && !own.name.includes('::') ? [...owner.scope, own.name].join('::') : qualified(own.name);
|
|
79
|
+
declare(name, { kind: 'enum', scope: name.split('::'), bases: [], uses: [] });
|
|
80
|
+
node.children.filter((child) => child.tag === 'enumitem').forEach((item) => {
|
|
81
|
+
const enumerator = attributesOf(item).name;
|
|
82
|
+
if (enumerator && !enumerators.has(enumerator)) enumerators.set(enumerator, name);
|
|
83
|
+
});
|
|
84
|
+
} else if (node.tag === 'cdecl') {
|
|
85
|
+
if (!own.name || !isPublic || own.storage === 'friend') return;
|
|
86
|
+
const uses = [own.type ?? '', own.decl ?? '', ...parametersOf(node)];
|
|
87
|
+
if (own.storage === 'typedef') {
|
|
88
|
+
const name = owner ? [...owner.scope, own.name].join('::') : qualified(own.name);
|
|
89
|
+
declare(name, { kind: 'typedef', scope: name.split('::'), bases: [], uses });
|
|
90
|
+
} else if (owner) {
|
|
91
|
+
owner.uses.push(...uses);
|
|
92
|
+
} else {
|
|
93
|
+
// An import of an overloaded name binds every overload.
|
|
94
|
+
values.set(own.name, [...(values.get(own.name) ?? []), ...uses]);
|
|
95
|
+
}
|
|
96
|
+
} else if (node.tag === 'constructor') {
|
|
97
|
+
if (owner && isPublic) owner.uses.push(own.decl ?? '', ...parametersOf(node));
|
|
98
|
+
} else {
|
|
99
|
+
node.children.forEach((child) => visit(child, namespace, owner));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
visit(parse(xml), [], null);
|
|
103
|
+
return { types, values, enumerators };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The classes and enums `names` reach from `header`, by qualified name, each with its kind and the header defining it.
|
|
107
|
+
// `treeOf` reads a header's parse; a name no header read so far declares is looked up through `definingHeader`, whose
|
|
108
|
+
// header is read in turn. ALL_NAMES starts from everything the header declares, as a header bound whole does.
|
|
109
|
+
export function typeClosure(header, names, { treeOf, definingHeader }) {
|
|
110
|
+
const trees = new Map();
|
|
111
|
+
const read = (file) => {
|
|
112
|
+
if (!trees.has(file)) trees.set(file, treeOf(file) ?? EMPTY_TREE);
|
|
113
|
+
return trees.get(file);
|
|
114
|
+
};
|
|
115
|
+
const root = read(header);
|
|
116
|
+
const lookedUp = new Set();
|
|
117
|
+
const inTrees = (candidates, wanted) => {
|
|
118
|
+
for (const candidate of candidates) {
|
|
119
|
+
for (const [file, tree] of trees) {
|
|
120
|
+
const entry = tree.types.get(candidate);
|
|
121
|
+
if (entry && wanted(entry)) return { name: candidate, file, entry };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
};
|
|
126
|
+
const isClassOrEnum = (entry) => entry.kind !== 'typedef';
|
|
127
|
+
// A class or enum wins over a typedef of the same name, even one another header declares first (a forward header's
|
|
128
|
+
// `typedef struct Foo Foo;`), so a header that may define the name is read before a typedef stands for it.
|
|
129
|
+
const find = (candidates) => {
|
|
130
|
+
const found = inTrees(candidates, isClassOrEnum);
|
|
131
|
+
if (found) return found;
|
|
132
|
+
for (const candidate of candidates) {
|
|
133
|
+
if (lookedUp.has(candidate)) continue;
|
|
134
|
+
lookedUp.add(candidate);
|
|
135
|
+
const file = definingHeader(candidate);
|
|
136
|
+
if (file && !trees.has(file)) read(file);
|
|
137
|
+
}
|
|
138
|
+
return inTrees(candidates, isClassOrEnum) ?? inTrees(candidates, () => true);
|
|
139
|
+
};
|
|
140
|
+
const queue = [];
|
|
141
|
+
const reach = (text, scope) => {
|
|
142
|
+
for (const [name] of text.matchAll(NAME)) {
|
|
143
|
+
if (!NOT_TYPES.has(name)) queue.push({ name, scope });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const start = names === ALL_NAMES ? [...root.values.keys(), ...root.types.keys()] : names;
|
|
147
|
+
for (const name of start) {
|
|
148
|
+
if (root.values.has(name)) root.values.get(name).forEach((use) => reach(use, []));
|
|
149
|
+
else queue.push({ name: root.enumerators.get(name) ?? name, scope: [] });
|
|
150
|
+
}
|
|
151
|
+
const reached = new Map();
|
|
152
|
+
const visited = new Set();
|
|
153
|
+
while (queue.length) {
|
|
154
|
+
const { name, scope } = queue.pop();
|
|
155
|
+
// As in C++ lookup, a name used inside a class or namespace is tried there first.
|
|
156
|
+
const candidates = [...scope.map((_, depth) => [...scope.slice(0, scope.length - depth), name].join('::')), name];
|
|
157
|
+
const found = find(candidates);
|
|
158
|
+
if (!found || visited.has(found.name)) continue;
|
|
159
|
+
visited.add(found.name);
|
|
160
|
+
const { entry } = found;
|
|
161
|
+
if (entry.kind === 'class') reached.set(found.name, { kind: entry.kind, header: found.file, bound: entry.bound });
|
|
162
|
+
else if (entry.kind === 'enum') reached.set(found.name, { kind: entry.kind, header: found.file });
|
|
163
|
+
const enclosing = entry.scope.slice(0, -1);
|
|
164
|
+
// SWIG binds an enum declared in a class only with that class.
|
|
165
|
+
if (entry.kind === 'enum' && enclosing.length) queue.push({ name: enclosing.join('::'), scope: [] });
|
|
166
|
+
entry.bases.forEach((base) => queue.push({ name: base, scope: enclosing }));
|
|
167
|
+
entry.uses.forEach((use) => reach(use, entry.kind === 'class' ? entry.scope : enclosing));
|
|
168
|
+
}
|
|
169
|
+
return reached;
|
|
170
|
+
}
|