crossbind 2.0.0-beta.73 → 2.0.0-beta.74
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/createLib.js +4 -1
- package/src/actions/licenses.js +94 -22
- package/src/actions/pageLicenses.js +89 -0
- 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/cppDts.js +19 -13
- package/src/utils/licenseReport.js +12 -5
- package/src/utils/packageLicense.js +14 -28
- package/src/utils/relinking.js +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crossbind",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.74",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"homepage": "https://crossbind.dev",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"types": "./src/app/index.d.ts",
|
|
28
28
|
"default": "./src/app/index.js"
|
|
29
29
|
},
|
|
30
|
+
"./licenses": {
|
|
31
|
+
"types": "./src/licenses/index.d.ts",
|
|
32
|
+
"default": "./src/licenses/index.js"
|
|
33
|
+
},
|
|
30
34
|
"./*": "./*"
|
|
31
35
|
},
|
|
32
36
|
"devDependencies": {
|
|
@@ -34,7 +38,7 @@
|
|
|
34
38
|
"vitest": "^5.0.1"
|
|
35
39
|
},
|
|
36
40
|
"dependencies": {
|
|
37
|
-
"@crossbind/extension-conan": "2.0.0-beta.
|
|
41
|
+
"@crossbind/extension-conan": "2.0.0-beta.74",
|
|
38
42
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
39
43
|
"@rollup/plugin-json": "^6.1.0",
|
|
40
44
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
"upath": "^3.0.8"
|
|
52
56
|
},
|
|
53
57
|
"optionalDependencies": {
|
|
54
|
-
"@crossbind/bridge-tools": "2.0.0-beta.
|
|
58
|
+
"@crossbind/bridge-tools": "2.0.0-beta.74"
|
|
55
59
|
},
|
|
56
60
|
"keywords": [
|
|
57
61
|
"webassembly",
|
package/src/actions/createLib.js
CHANGED
|
@@ -48,7 +48,10 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
48
48
|
// The lib dir is reused while the bridge list grows, so an early smaller nativeGlob build must not satisfy later sets: fingerprint the glob and miss on mismatch.
|
|
49
49
|
const fingerprintFile = `${libdir}/crossbind-nativeglob.fingerprint`;
|
|
50
50
|
// options.inputs lists what the sources compile against (a binding runtime's headers), so a change there rebuilds too.
|
|
51
|
-
|
|
51
|
+
// A header edit can leave a bridge's text as it was, so the key beside each bridge, the hash of what it was made from, counts too.
|
|
52
|
+
const fingerprint = nativeGlob
|
|
53
|
+
? getFilesFingerprint([...nativeGlob, ...nativeGlob.map((file) => `${file}.key`), ...(options.inputs ?? [])])
|
|
54
|
+
: null;
|
|
52
55
|
const fingerprintChanged = fingerprint !== null
|
|
53
56
|
&& (!fs.existsSync(fingerprintFile) || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint);
|
|
54
57
|
|
package/src/actions/licenses.js
CHANGED
|
@@ -113,6 +113,32 @@ function wasiToolchainRows() {
|
|
|
113
113
|
];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// A wasm module statically links Emscripten's runtime and system libraries (its musl-based libc among them) and LLVM's
|
|
117
|
+
// C++ runtime from the same toolchain image.
|
|
118
|
+
function wasmToolchainRows() {
|
|
119
|
+
const shared = {
|
|
120
|
+
npmName: null, version: null, nativeVersion: null, sha256: null, licenseSelected: null, licenseText: null, isCopyleft: false,
|
|
121
|
+
};
|
|
122
|
+
return [
|
|
123
|
+
{
|
|
124
|
+
...shared,
|
|
125
|
+
name: 'emscripten',
|
|
126
|
+
license: 'MIT OR NCSA',
|
|
127
|
+
licenseDeclared: 'MIT OR NCSA',
|
|
128
|
+
sourceUrl: 'https://github.com/emscripten-core/emscripten',
|
|
129
|
+
licenseNotes: "Emscripten's runtime and system libraries, its musl-based libc among them, statically linked into the module; license text: https://github.com/emscripten-core/emscripten/blob/main/LICENSE",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
...shared,
|
|
133
|
+
name: 'llvm-runtimes',
|
|
134
|
+
license: 'Apache-2.0 WITH LLVM-exception',
|
|
135
|
+
licenseDeclared: 'Apache-2.0 WITH LLVM-exception',
|
|
136
|
+
sourceUrl: 'https://github.com/llvm/llvm-project',
|
|
137
|
+
licenseNotes: "libc++, libc++abi and compiler-rt of Emscripten's LLVM, statically linked into the module; license text: https://github.com/llvm/llvm-project/blob/main/LICENSE.TXT",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
// The notices a build copied out of its toolchain image (a Windows addon's or app's does).
|
|
117
143
|
function toolchainNoticeTexts(platform) {
|
|
118
144
|
const build = state.config?.paths?.build;
|
|
@@ -160,25 +186,31 @@ function addonToolchainRows(platform) {
|
|
|
160
186
|
const versionOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version;
|
|
161
187
|
const textOf = (file) => fs.readFileSync(file, 'utf8').trim();
|
|
162
188
|
|
|
189
|
+
const RUNTIME_ROW = {
|
|
190
|
+
nativeVersion: null, sha256: null, licenseSelected: null, isCopyleft: false, isRuntime: true,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// crossbind's own code: what an addon and an app host compile in, or the JavaScript a wasm module's glue bundles.
|
|
194
|
+
function crossbindRow(notes) {
|
|
195
|
+
return {
|
|
196
|
+
...RUNTIME_ROW,
|
|
197
|
+
name: 'crossbind',
|
|
198
|
+
npmName: 'crossbind',
|
|
199
|
+
version: versionOf(CROSSBIND_ROOT),
|
|
200
|
+
license: 'MIT',
|
|
201
|
+
licenseDeclared: 'MIT',
|
|
202
|
+
sourceUrl: 'https://github.com/crossbind/crossbind',
|
|
203
|
+
licenseNotes: notes,
|
|
204
|
+
licenseText: textOf(path.join(CROSSBIND_ROOT, 'LICENSE')),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
163
208
|
// crossbind's runtime, which an addon and an app host compile in: embind-jsi, which adapts Emscripten's embind.
|
|
164
209
|
function runtimeRows(jsiRoot, notes) {
|
|
165
|
-
const shared = {
|
|
166
|
-
nativeVersion: null, sha256: null, licenseSelected: null, isCopyleft: false,
|
|
167
|
-
};
|
|
168
210
|
return [
|
|
211
|
+
crossbindRow(notes),
|
|
169
212
|
{
|
|
170
|
-
...
|
|
171
|
-
name: 'crossbind',
|
|
172
|
-
npmName: 'crossbind',
|
|
173
|
-
version: versionOf(CROSSBIND_ROOT),
|
|
174
|
-
license: 'MIT',
|
|
175
|
-
licenseDeclared: 'MIT',
|
|
176
|
-
sourceUrl: 'https://github.com/crossbind/crossbind',
|
|
177
|
-
licenseNotes: notes,
|
|
178
|
-
licenseText: textOf(path.join(CROSSBIND_ROOT, 'LICENSE')),
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
...shared,
|
|
213
|
+
...RUNTIME_ROW,
|
|
182
214
|
name: 'emscripten-embind',
|
|
183
215
|
npmName: '@crossbind/core-embind-jsi',
|
|
184
216
|
version: versionOf(jsiRoot),
|
|
@@ -420,10 +452,13 @@ async function buildRow(node) {
|
|
|
420
452
|
sourceUrl = null;
|
|
421
453
|
}
|
|
422
454
|
}
|
|
423
|
-
if (!sourceUrl) sourceUrl = manifest?.homepage || null;
|
|
424
|
-
|
|
425
455
|
const family = familyManifestOf(node);
|
|
426
|
-
|
|
456
|
+
// A project's own code is at its repository; its homepage is where its docs are.
|
|
457
|
+
if (!sourceUrl) sourceUrl = repositoryUrl(family?.manifest) || repositoryUrl(manifest) || manifest?.homepage || null;
|
|
458
|
+
|
|
459
|
+
// The upstream identity scanners match a component on: the project's own name, and its CPE where NVD has one.
|
|
460
|
+
const identity = family?.manifest?.crossbind?.upstream ?? null;
|
|
461
|
+
const upstream = identity?.license || null;
|
|
427
462
|
let license;
|
|
428
463
|
let licenseText;
|
|
429
464
|
if (upstream) {
|
|
@@ -437,6 +472,12 @@ async function buildRow(node) {
|
|
|
437
472
|
if (isComplete) licenseText = fromSource;
|
|
438
473
|
else if (shipped) licenseText = `=== ${path.basename(shipped)} ===\n\n${fs.readFileSync(shipped, 'utf8').trim()}`;
|
|
439
474
|
else licenseText = readLicenseTexts(upstream.files, () => null);
|
|
475
|
+
} else if (family && family.dir !== node.paths.project) {
|
|
476
|
+
// The package the alias names holds the code: a published binary's own license field is the expression of
|
|
477
|
+
// everything it links, which this row would otherwise repeat as its own.
|
|
478
|
+
license = family.manifest.license || manifest?.license || null;
|
|
479
|
+
const own = packageLicenseFile(family.dir) ?? packageLicenseFile(node.paths.project);
|
|
480
|
+
licenseText = own ? fs.readFileSync(own, 'utf8') : null;
|
|
440
481
|
} else {
|
|
441
482
|
license = manifest?.license || null;
|
|
442
483
|
const legacy = packageLicenseFile(node.paths.project);
|
|
@@ -444,10 +485,11 @@ async function buildRow(node) {
|
|
|
444
485
|
}
|
|
445
486
|
|
|
446
487
|
return {
|
|
447
|
-
name: node.general.name,
|
|
488
|
+
name: identity?.name ?? node.general.name,
|
|
448
489
|
npmName: manifest?.name || null,
|
|
449
490
|
version: manifest?.version || null,
|
|
450
491
|
nativeVersion,
|
|
492
|
+
cpe: identity?.cpe ? `cpe:2.3:a:${identity.cpe}:${nativeVersion ?? '*'}:*:*:*:*:*:*:*` : null,
|
|
451
493
|
license,
|
|
452
494
|
licenseDeclared: upstream?.declared || null,
|
|
453
495
|
licenseSelected: upstream?.selected || null,
|
|
@@ -456,9 +498,21 @@ async function buildRow(node) {
|
|
|
456
498
|
sourceUrl,
|
|
457
499
|
licenseText,
|
|
458
500
|
isCopyleft: isCopyleft(license),
|
|
501
|
+
isPort: Boolean(upstream),
|
|
502
|
+
shipsData: shipsData(node),
|
|
459
503
|
};
|
|
460
504
|
}
|
|
461
505
|
|
|
506
|
+
export function repositoryUrl(manifest) {
|
|
507
|
+
const repository = typeof manifest?.repository === 'string' ? manifest.repository : manifest?.repository?.url;
|
|
508
|
+
return repository ? repository.replace(/^git\+/, '').replace(/\.git$/, '') : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// The data a dependency declares (GDAL's and PROJ's) travels with the package that ships the module or its loader.
|
|
512
|
+
function shipsData(node) {
|
|
513
|
+
return (node.targetSpecs ?? []).some((entry) => entry?.specs?.data && Object.keys(entry.specs.data).length > 0);
|
|
514
|
+
}
|
|
515
|
+
|
|
462
516
|
// The facts of a package source's package come from its recipe, and its texts from the licenses/ its package ships.
|
|
463
517
|
function sourcePackageRow(node) {
|
|
464
518
|
const { name: source, package: pkg } = node.general.packageSource;
|
|
@@ -497,9 +551,22 @@ function assertSourcesStaged() {
|
|
|
497
551
|
});
|
|
498
552
|
}
|
|
499
553
|
|
|
554
|
+
// The libraries whose recipes vendor copies for some platform, which only a listing for that platform includes.
|
|
555
|
+
export async function vendoringLibraries() {
|
|
556
|
+
const names = [];
|
|
557
|
+
for (const node of [state.config, ...state.config.allDependencies]) {
|
|
558
|
+
if (!node?.paths?.project || node.general?.packageSource) continue;
|
|
559
|
+
const recipe = await loadJs(node.paths.project, 'crossbind.build');
|
|
560
|
+
const isVendoring = Object.values(recipe?.bundled ?? {}).some((entries) => entries?.length > 0);
|
|
561
|
+
if (isVendoring && !names.includes(node.general.name)) names.push(node.general.name);
|
|
562
|
+
}
|
|
563
|
+
return names;
|
|
564
|
+
}
|
|
565
|
+
|
|
500
566
|
// platform (optional, e.g. 'wasi') additionally includes what that platform's
|
|
501
567
|
// artifact statically links beyond the package graph: recipe-declared vendored
|
|
502
|
-
// copies and the toolchain runtime
|
|
568
|
+
// copies and the toolchain runtime, and for wasm crossbind's runtime, which the
|
|
569
|
+
// module's glue bundles. runtimeEnv 'node' adds the crossbind runtime an
|
|
503
570
|
// addon of that platform compiles in, or without a platform the one its loader bundles;
|
|
504
571
|
// runtimeEnv 'app' adds what an app's host carries.
|
|
505
572
|
export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
|
|
@@ -513,12 +580,16 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
|
|
|
513
580
|
const nodes = [state.config, ...state.config.allDependencies]
|
|
514
581
|
.filter((node, index) => node?.paths?.project
|
|
515
582
|
&& (node.general?.alias?.package || node.general?.packageSource || (index > 0 && node.package?.name && !node.package.private)));
|
|
583
|
+
// A project can depend on a library's packages for several platforms; the report names the one its platform links.
|
|
584
|
+
const target = platform ? state.targets?.find((t) => t.platform === platform) : null;
|
|
585
|
+
const serves = (node) => !target || node.functions?.isEnabled?.(target) !== false;
|
|
516
586
|
const rows = [];
|
|
517
587
|
const seen = new Set();
|
|
518
|
-
for (const
|
|
519
|
-
const key = keyOf(
|
|
588
|
+
for (const listed of nodes) {
|
|
589
|
+
const key = keyOf(listed);
|
|
520
590
|
if (seen.has(key)) continue;
|
|
521
591
|
seen.add(key);
|
|
592
|
+
const node = nodes.find((other) => keyOf(other) === key && serves(other)) ?? listed;
|
|
522
593
|
rows.push(node.general.packageSource ? sourcePackageRow(node) : await buildRow(node));
|
|
523
594
|
// A package source's package folder is written from what its recipe produced: nothing there is run.
|
|
524
595
|
if (platform && !node.general.packageSource) {
|
|
@@ -527,6 +598,7 @@ export default async function collectLicenseRows(platform = null, { runtimeEnv =
|
|
|
527
598
|
}
|
|
528
599
|
}
|
|
529
600
|
if (platform === 'wasi') rows.push(...wasiToolchainRows());
|
|
601
|
+
if (platform === 'wasm') rows.push(...wasmToolchainRows(), crossbindRow("crossbind's JavaScript runtime, which the module's glue bundles"));
|
|
530
602
|
if (['linux', 'linuxmusl', 'win32'].includes(platform)) rows.push(...addonToolchainRows(platform));
|
|
531
603
|
if (runtimeEnv === 'node') rows.push(...nodeRuntimeRows({ isAddon: platform !== null }));
|
|
532
604
|
if (runtimeEnv === 'app') {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import collectLicenseRows, { repositoryUrl } from './licenses.js';
|
|
5
|
+
|
|
6
|
+
const FILE = 'crossbind-licenses.json';
|
|
7
|
+
// crossbind/licenses, from whichever copy of crossbind the page has.
|
|
8
|
+
const HELPER = /[\\/]crossbind[\\/]src[\\/]licenses[\\/]index\.js$/;
|
|
9
|
+
const TEXT_FILE = /^(licen[cs]e|copying|notice|ofl)([._-].*)?$/i;
|
|
10
|
+
|
|
11
|
+
// What a bundle holds: its chunks' modules, and the files its assets came from, as the font a stylesheet's url() takes.
|
|
12
|
+
function bundledFiles(bundle, root) {
|
|
13
|
+
return Object.values(bundle)
|
|
14
|
+
.flatMap((output) => (output.type === 'chunk'
|
|
15
|
+
? output.moduleIds
|
|
16
|
+
: (output.originalFileNames ?? []).map((name) => path.resolve(root, name))))
|
|
17
|
+
.map((id) => id.split('?')[0]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function manifestIn(dir, manifests) {
|
|
21
|
+
const file = path.join(dir, 'package.json');
|
|
22
|
+
if (!manifests.has(file)) {
|
|
23
|
+
try {
|
|
24
|
+
manifests.set(file, fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : null);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw new Error(`crossbind: ${file} holds no JSON: ${error.message}`, { cause: error });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return manifests.get(file);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The package a file is in: the nearest folder up from it whose package.json names one, as a package's subfolder can
|
|
33
|
+
// hold one that names none.
|
|
34
|
+
function packageOf(file, manifests) {
|
|
35
|
+
if (!path.isAbsolute(file)) return null;
|
|
36
|
+
for (let dir = path.dirname(path.resolve(file)); dir !== path.dirname(dir); dir = path.dirname(dir)) {
|
|
37
|
+
const manifest = manifestIn(dir, manifests);
|
|
38
|
+
if (manifest?.name) return { dir, manifest };
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function npmEntry({ dir, manifest }) {
|
|
44
|
+
const texts = fs.readdirSync(dir, { withFileTypes: true })
|
|
45
|
+
.filter((entry) => entry.isFile() && TEXT_FILE.test(entry.name))
|
|
46
|
+
.map((entry) => entry.name)
|
|
47
|
+
.sort()
|
|
48
|
+
.map((file) => `=== ${file} ===\n\n${fs.readFileSync(path.join(dir, file), 'utf8').trim()}`);
|
|
49
|
+
return {
|
|
50
|
+
name: manifest.name,
|
|
51
|
+
version: manifest.version ?? null,
|
|
52
|
+
license: typeof manifest.license === 'string' ? manifest.license : manifest.license?.type ?? null,
|
|
53
|
+
source: repositoryUrl(manifest) ?? manifest.homepage ?? null,
|
|
54
|
+
text: texts.length > 0 ? texts.join('\n\n') : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The npm packages the page carries, but for the project's own code: its package and private ones.
|
|
59
|
+
function npmEntriesOf(files) {
|
|
60
|
+
const manifests = new Map();
|
|
61
|
+
const project = path.resolve(state.config.paths.project);
|
|
62
|
+
const packages = new Map(files
|
|
63
|
+
.map((file) => packageOf(file, manifests))
|
|
64
|
+
.filter((found) => found && found.dir !== project && !found.manifest.private)
|
|
65
|
+
.map((found) => [found.dir, found]));
|
|
66
|
+
return [...packages.values()].map(npmEntry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nativeEntry = (row) => ({
|
|
70
|
+
name: row.name,
|
|
71
|
+
version: row.nativeVersion ?? row.version ?? null,
|
|
72
|
+
license: row.license ?? null,
|
|
73
|
+
source: row.sourceUrl ?? null,
|
|
74
|
+
text: row.licenseText ?? null,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// The list a page reads with crossbind/licenses, which its build writes beside it if it does: what the module links and
|
|
78
|
+
// an app carries, as `crossbind licenses` lists them, and the npm packages the page's bundle holds, each once. bundle is
|
|
79
|
+
// what Rollup or Vite hands generateBundle, root what its assets' original file names are relative to.
|
|
80
|
+
export default async function pageLicensesAsset(bundle, { root, platform, runtimeEnv = null }) {
|
|
81
|
+
const files = bundledFiles(bundle, root);
|
|
82
|
+
if (!files.some((file) => HELPER.test(file))) return null;
|
|
83
|
+
const native = (await collectLicenseRows(platform, { runtimeEnv })).map(nativeEntry);
|
|
84
|
+
const listed = new Set();
|
|
85
|
+
const entries = [...native, ...npmEntriesOf(files)]
|
|
86
|
+
.filter(({ name, version }) => !listed.has(`${name}@${version}`) && listed.add(`${name}@${version}`))
|
|
87
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
88
|
+
return { fileName: FILE, source: JSON.stringify(entries) };
|
|
89
|
+
}
|
package/src/bin.js
CHANGED
|
@@ -48,9 +48,9 @@ import { cargoHome } from './utils/runCargo.js';
|
|
|
48
48
|
import { containerRootOf, sourceRoot } from './utils/runPackageSource.js';
|
|
49
49
|
import { importSchemesOf, sourceNamesOf } from './utils/packageSources.js';
|
|
50
50
|
import { cleanDepsCache } from './utils/dependencyRebuild.js';
|
|
51
|
-
import collectLicenseRows from './actions/licenses.js';
|
|
51
|
+
import collectLicenseRows, { vendoringLibraries } from './actions/licenses.js';
|
|
52
52
|
import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
|
|
53
|
-
import { SBOM_FILE, nodePackageMeta, writeNodePackageLicense } from './utils/packageLicense.js';
|
|
53
|
+
import { SBOM_FILE, nodePackageMeta, shippedRows, writeNodePackageLicense } from './utils/packageLicense.js';
|
|
54
54
|
import findFiles from './utils/findFiles.js';
|
|
55
55
|
|
|
56
56
|
const platforms = [...new Set(state.targets.map(target => target.platform).filter(t => t))];
|
|
@@ -240,11 +240,17 @@ program.command('licenses')
|
|
|
240
240
|
});
|
|
241
241
|
const copyleftRows = rows.filter((row) => row.isCopyleft);
|
|
242
242
|
if (copyleftRows.length > 0) {
|
|
243
|
-
console.log(`\n! ${copyleftRows.length} copyleft-licensed ${copyleftRows.length === 1 ? 'dependency' : 'dependencies'} (${copyleftRows.map((row) => row.name).join(', ')}) — see docs/playbooks/licensing-lgpl.md`);
|
|
243
|
+
console.log(`\n! ${copyleftRows.length} copyleft-licensed ${copyleftRows.length === 1 ? 'dependency' : 'dependencies'} (${copyleftRows.map((row) => row.name).join(', ')}) — see https://github.com/crossbind/crossbind/blob/main/docs/playbooks/licensing-lgpl.md`);
|
|
244
|
+
}
|
|
245
|
+
if ((options.notices || options.sbom) && !options.platform) {
|
|
246
|
+
const vendoring = await vendoringLibraries();
|
|
247
|
+
if (vendoring.length > 0) {
|
|
248
|
+
console.warn(`crossbind: ${vendoring.join(', ')} vendor${vendoring.length === 1 ? 's' : ''} copies of other libraries per platform; pass --platform <platform> so the notices and SBOM list them.`);
|
|
249
|
+
}
|
|
244
250
|
}
|
|
245
251
|
if (options.notices) {
|
|
246
252
|
const file = typeof options.notices === 'string' ? options.notices : `${state.config.paths.project}/THIRD-PARTY-NOTICES.md`;
|
|
247
|
-
fs.writeFileSync(file, formatNoticesMarkdown(rows));
|
|
253
|
+
fs.writeFileSync(file, formatNoticesMarkdown(rows, nodePackageMeta(state.config)));
|
|
248
254
|
logger.info(`crossbind: wrote ${file}`);
|
|
249
255
|
}
|
|
250
256
|
if (options.sbom) {
|
|
@@ -265,7 +271,8 @@ program.command('licenses')
|
|
|
265
271
|
logger.info(`crossbind: wrote ${file}`);
|
|
266
272
|
}
|
|
267
273
|
if (options.package) {
|
|
268
|
-
const
|
|
274
|
+
const meta = nodePackageMeta(state.config);
|
|
275
|
+
const expression = writeNodePackageLicense(state.config.paths.project, { ...meta, rows: shippedRows(rows, meta) });
|
|
269
276
|
logger.info(`crossbind: wrote LICENSE and ${SBOM_FILE} (${expression})`);
|
|
270
277
|
}
|
|
271
278
|
if (options.check) {
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
export { checkInstaller, shipApp } from './actions/appInstaller.js';
|
|
10
10
|
export { findAppPageApi, resolveAppPageApi } from './utils/resolveAppHost.js';
|
|
11
11
|
export { default as isAppBuild } from './utils/appBuild.js';
|
|
12
|
+
export { default as pageLicensesAsset } from './actions/pageLicenses.js';
|
|
12
13
|
export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
|
|
13
14
|
export {
|
|
14
15
|
default as openOnIosDevice, buildForIosDevice, connectedIosDevice, iosDeviceAddress, iosDevServer,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** A component a page or the app it runs in carries: a library the native module links, a part of an app's host or crossbind's runtime, or an npm package in the page's bundle. */
|
|
2
|
+
export interface License {
|
|
3
|
+
/** The name its project gives it. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** The version of the code the page or app carries. */
|
|
6
|
+
version: string | null;
|
|
7
|
+
/** An SPDX expression. */
|
|
8
|
+
license: string | null;
|
|
9
|
+
/** Where its code is. */
|
|
10
|
+
source: string | null;
|
|
11
|
+
/** Its license texts as it carries them, each under its file's name. */
|
|
12
|
+
text: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The components the page and its app carry, by name, from the crossbind-licenses.json a build with crossbind's Vite or Rollup plugin writes beside the page. Rejects on a dev server, which has none. */
|
|
16
|
+
export function licenses(): Promise<License[]>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The licenses of what a page and the app it runs in carry, from the crossbind-licenses.json a build of the page with
|
|
2
|
+
// crossbind's Vite or Rollup plugin writes beside it.
|
|
3
|
+
const FILE = 'crossbind-licenses.json';
|
|
4
|
+
|
|
5
|
+
export async function licenses() {
|
|
6
|
+
// Vite gives the base it serves the page from; elsewhere the file is beside the page.
|
|
7
|
+
const url = new URL(`${import.meta.env?.BASE_URL ?? './'}${FILE}`, document.baseURI);
|
|
8
|
+
const response = await fetch(url);
|
|
9
|
+
// A dev server answers a file it lacks with the page.
|
|
10
|
+
const list = response.ok ? await response.json().catch(() => null) : null;
|
|
11
|
+
if (!Array.isArray(list)) {
|
|
12
|
+
throw new Error(`crossbind: ${url} holds no license list. A build of the page with crossbind's Vite or Rollup plugin writes it; the dev server has none.`);
|
|
13
|
+
}
|
|
14
|
+
return list;
|
|
15
|
+
}
|
package/src/state/loadConfig.js
CHANGED
|
@@ -146,6 +146,8 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
146
146
|
app: config.app || {},
|
|
147
147
|
// What an app's host builds in beside its own extensions.
|
|
148
148
|
extensions: config.extensions || [],
|
|
149
|
+
// The license files crossbind licenses writes: sourceTag is the git tag a published package's source is at.
|
|
150
|
+
licenses: config.licenses || {},
|
|
149
151
|
package: null,
|
|
150
152
|
functions: config.functions || {},
|
|
151
153
|
};
|
|
@@ -153,6 +155,9 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
153
155
|
if (!['sync', 'promise'].includes(newConfig.dts)) {
|
|
154
156
|
throw new Error(`crossbind: "dts" must be 'sync' or 'promise' (got '${newConfig.dts}').`);
|
|
155
157
|
}
|
|
158
|
+
if (newConfig.licenses.sourceTag !== undefined && typeof newConfig.licenses.sourceTag !== 'string') {
|
|
159
|
+
throw new Error(`crossbind: "licenses.sourceTag" must be a string such as 'v{version}' (got '${newConfig.licenses.sourceTag}').`);
|
|
160
|
+
}
|
|
156
161
|
if (typeof newConfig.types !== 'boolean') {
|
|
157
162
|
throw new Error(`crossbind: "types" must be true or false (got '${newConfig.types}').`);
|
|
158
163
|
}
|
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
|
}
|
|
@@ -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
|
+
}
|