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
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/actions/run.js
CHANGED
|
@@ -26,6 +26,31 @@ export const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
|
|
|
26
26
|
// Containers share the memory of Docker's VM, and one compile at full -j can take most of a desktop VM
|
|
27
27
|
// (GDAL's bindings peak at 7.5 of 7.7 GiB), so the exclusive steps of parallel builds take turns.
|
|
28
28
|
const DOCKER_COMPILE_LOCK = path.join(os.homedir(), '.crossbind', 'docker-compile.lock');
|
|
29
|
+
// With CROSSBIND_SCCACHE set, C and C++ compiles go through sccache, which its SCCACHE_* variables configure, by CMake's
|
|
30
|
+
// launchers. A wasm build compiles without it: sccache 0.19 recognizes neither em++ nor emscripten's clang, which has
|
|
31
|
+
// no default target, and cannot cache the -Xclang flags emcc passes that clang.
|
|
32
|
+
const SCCACHE_LAUNCHERS = ['CMAKE_C_COMPILER_LAUNCHER', 'CMAKE_CXX_COMPILER_LAUNCHER'];
|
|
33
|
+
// sccache's key leaves out the target llvm-mingw's compilers take from their own names, so an x64 and an arm64 compile of
|
|
34
|
+
// one source would share it; the buster, which sccache hashes into every key, names the target.
|
|
35
|
+
const SCCACHE_BUSTER = 'SCCACHE_C_CUSTOM_CACHE_BUSTER';
|
|
36
|
+
const sccacheBuster = (target) => [process.env[SCCACHE_BUSTER], target?.platform, target?.arch].filter(Boolean).join('-');
|
|
37
|
+
const SCCACHE_SETTING =/^(SCCACHE_\w+|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|AWS_SESSION_TOKEN)$/;
|
|
38
|
+
const SCCACHE_IN_IMAGE = 'command -v sccache >/dev/null || { echo "crossbind: CROSSBIND_SCCACHE needs toolchain images that carry sccache, '
|
|
39
|
+
+ 'image family 1.0.21 or later." >&2; exit 1; }; ';
|
|
40
|
+
// A docker run's sccache server ends with its container, so the command stops it first, which waits for the cache writes
|
|
41
|
+
// it holds; a DOCKER_EXEC container's server serves the commands after it.
|
|
42
|
+
const SCCACHE_IN_CONTAINER = {
|
|
43
|
+
DOCKER_RUN: `${SCCACHE_IN_IMAGE}"$0" "$@"; status=$?; sccache --stop-server >/dev/null 2>&1; exit $status`,
|
|
44
|
+
DOCKER_EXEC: `${SCCACHE_IN_IMAGE}exec "$0" "$@"`,
|
|
45
|
+
};
|
|
46
|
+
const isSccacheOn = () => /^(1|true)$/i.test(process.env.CROSSBIND_SCCACHE ?? '');
|
|
47
|
+
const sccacheSettings = () => Object.keys(process.env).filter((key) => SCCACHE_SETTING.test(key)).sort();
|
|
48
|
+
// Where a container finds a local sccache directory: a docker run mounts it here, a long-lived container sees the project.
|
|
49
|
+
const SCCACHE_CONTAINER_DIR = '/tmp/crossbind/sccache';
|
|
50
|
+
const isInside = (parent, child) => {
|
|
51
|
+
const relative = path.relative(parent, child);
|
|
52
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
53
|
+
};
|
|
29
54
|
const envParams = (env) => Object.entries(env).flatMap(([variable, value]) => ['-e', `${variable}=${value}`]);
|
|
30
55
|
const CROSSCOMPILER_ARM64 = `aarch64-linux-android${ANDROID_API_LEVEL}`;
|
|
31
56
|
const CROSSCOMPILER_x86_64 = `x86_64-linux-android${ANDROID_API_LEVEL}`;
|
|
@@ -139,6 +164,15 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
139
164
|
? { runner: 'LOCAL' }
|
|
140
165
|
: runnerFor(role, state.config.system);
|
|
141
166
|
const isDocker = runner === 'DOCKER_RUN' || runner === 'DOCKER_EXEC';
|
|
167
|
+
const isSccache = isSccacheOn() && target?.platform !== 'wasm';
|
|
168
|
+
if (isSccache && runner === 'REMOTE') {
|
|
169
|
+
throw new Error('crossbind: CROSSBIND_SCCACHE works with the DOCKER_RUN, DOCKER_EXEC and LOCAL runners; a remote runner would need your '
|
|
170
|
+
+ 'cache\'s keys. Unset it for a remote build.');
|
|
171
|
+
}
|
|
172
|
+
const sccacheDir = isSccache && isDocker ? process.env.SCCACHE_DIR : undefined;
|
|
173
|
+
if (sccacheDir && runner === 'DOCKER_EXEC' && !isInside(state.config.paths.base, sccacheDir)) {
|
|
174
|
+
throw new Error(`crossbind: a DOCKER_EXEC container reaches the project alone, so SCCACHE_DIR (${sccacheDir}) must sit inside ${state.config.paths.base}.`);
|
|
175
|
+
}
|
|
142
176
|
const wasiHostSdk = target?.platform === 'wasi' ? resolveWasiSdkPath(state.config.system) : null;
|
|
143
177
|
if (isDocker) {
|
|
144
178
|
// Google ships the linux NDK for x86_64 only, so the android index carries no arm64 leaf;
|
|
@@ -212,10 +246,10 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
212
246
|
if (dProgram === 'cmake') {
|
|
213
247
|
platformParams = [];
|
|
214
248
|
if (dParams[0] !== '--build' && dParams[0] !== '--install') {
|
|
249
|
+
// CMake's own iOS platform with Makefiles: the Xcode generator spent 40-55 s configuring even a
|
|
250
|
+
// small project. Not the app host's toolchain file, whose searches find the Mac's own packages.
|
|
215
251
|
dParams = [
|
|
216
252
|
...dParams,
|
|
217
|
-
'-G',
|
|
218
|
-
'Xcode',
|
|
219
253
|
'-DBUILD_SHARED_LIBS=OFF',
|
|
220
254
|
'-DFRAMEWORK=TRUE',
|
|
221
255
|
`-DCMAKE_OSX_DEPLOYMENT_TARGET=${IOS_DEPLOYMENT_TARGET}`,
|
|
@@ -227,6 +261,11 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
227
261
|
'-DCMAKE_SYSTEM_PROCESSOR=arm64',
|
|
228
262
|
`-DCMAKE_C_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
|
|
229
263
|
`-DCMAKE_CXX_FLAGS=${target.arch === 'iphoneos' ? '-fembed-bitcode' : '-fembed-bitcode-marker'}`,
|
|
264
|
+
// The objects carry bitcode as before; a link, as of a configure check or a recipe's tests,
|
|
265
|
+
// takes none, since Xcode's linker reads -fembed-bitcode-marker's mode as a file.
|
|
266
|
+
'-DCMAKE_EXE_LINKER_FLAGS=-fembed-bitcode=off',
|
|
267
|
+
'-DCMAKE_SHARED_LINKER_FLAGS=-fembed-bitcode=off',
|
|
268
|
+
'-DCMAKE_MODULE_LINKER_FLAGS=-fembed-bitcode=off',
|
|
230
269
|
"-DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY='iPhone Developer'",
|
|
231
270
|
`-DCMAKE_XCODE_ATTRIBUTE_DEVELOPMENT_TEAM=${state.config.system.XCODE_DEVELOPMENT_TEAM}`,
|
|
232
271
|
];
|
|
@@ -355,6 +394,18 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
355
394
|
const containerOptions = { cwd: buildPath, stdio: dockerOptions.console ? 'inherit' : ['ignore', 'pipe', 'pipe'], maxBuffer: EXEC_MAX_BUFFER };
|
|
356
395
|
let fileExecParams;
|
|
357
396
|
if (runner === 'LOCAL') {
|
|
397
|
+
if (isSccache) {
|
|
398
|
+
// By its full path: a macOS build narrows the PATH below.
|
|
399
|
+
const sccache = findExecutable('sccache', process.env.PATH ?? '');
|
|
400
|
+
if (!sccache) throw new Error('crossbind: CROSSBIND_SCCACHE needs sccache on the PATH: install it with `brew install sccache`, or unset CROSSBIND_SCCACHE.');
|
|
401
|
+
SCCACHE_LAUNCHERS.forEach((key) => {
|
|
402
|
+
env[key] = sccache;
|
|
403
|
+
});
|
|
404
|
+
sccacheSettings().forEach((key) => {
|
|
405
|
+
env[key] = process.env[key];
|
|
406
|
+
});
|
|
407
|
+
env[SCCACHE_BUSTER] = sccacheBuster(target);
|
|
408
|
+
}
|
|
358
409
|
env.PATH = `/opt/homebrew/bin:${env.PATH}`;
|
|
359
410
|
// A package's *-config script on the PATH (nc-config, sfcgal-config, ...) would compile that
|
|
360
411
|
// package into the archives. cmake is the one host tool a darwin build takes from the PATH.
|
|
@@ -392,6 +443,11 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
392
443
|
if (runner === 'DOCKER_RUN') {
|
|
393
444
|
imageOrContainer = getDockerImage(role);
|
|
394
445
|
runnerParams = ['run', '--rm', ...DOCKER_RUN_SECURITY_ARGS, '-v', `${state.config.paths.base}:/tmp/crossbind/live`];
|
|
446
|
+
if (sccacheDir) {
|
|
447
|
+
// Made here, as docker would make a missing one owned by root.
|
|
448
|
+
fs.mkdirSync(sccacheDir, { recursive: true });
|
|
449
|
+
runnerParams.push('-v', `${sccacheDir}:${SCCACHE_CONTAINER_DIR}`);
|
|
450
|
+
}
|
|
395
451
|
if (target?.platform === 'android') {
|
|
396
452
|
// Google ships the linux NDK for x86_64 only; use the amd64 leaf ref (classic store holds one platform per digest).
|
|
397
453
|
pullDockerImage(role, 'linux/amd64');
|
|
@@ -403,6 +459,16 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
403
459
|
runnerParams = ['exec'];
|
|
404
460
|
}
|
|
405
461
|
|
|
462
|
+
// The settings go by name, docker taking their values from its own environment, so no key sits in the arguments.
|
|
463
|
+
const insideDir = sccacheDir && (runner === 'DOCKER_RUN' ? SCCACHE_CONTAINER_DIR : replaceBasePathForDocker(sccacheDir));
|
|
464
|
+
const sccache = isSccache ? [
|
|
465
|
+
...SCCACHE_LAUNCHERS.flatMap((key) => ['-e', `${key}=sccache`]),
|
|
466
|
+
...sccacheSettings().filter((key) => !(sccacheDir && key === 'SCCACHE_DIR') && key !== SCCACHE_BUSTER).flatMap((key) => ['-e', key]),
|
|
467
|
+
'-e',
|
|
468
|
+
`${SCCACHE_BUSTER}=${sccacheBuster(target)}`,
|
|
469
|
+
...(sccacheDir ? ['-e', `SCCACHE_DIR=${insideDir}`] : []),
|
|
470
|
+
] : [];
|
|
471
|
+
const command = isSccache ? ['sh', '-c', SCCACHE_IN_CONTAINER[runner], dProgram, ...dParams] : [dProgram, ...dParams];
|
|
406
472
|
const args = [
|
|
407
473
|
...runnerParams,
|
|
408
474
|
'--user',
|
|
@@ -410,10 +476,9 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
410
476
|
'--workdir',
|
|
411
477
|
replaceBasePathForDocker(dockerOptions.workdir || buildPath),
|
|
412
478
|
...replaceBasePathForDocker(dockerEnv),
|
|
413
|
-
|
|
479
|
+
...sccache,
|
|
414
480
|
imageOrContainer,
|
|
415
|
-
replaceBasePathForDocker(
|
|
416
|
-
...replaceBasePathForDocker(dParams),
|
|
481
|
+
...replaceBasePathForDocker(command),
|
|
417
482
|
];
|
|
418
483
|
fileExecParams = ['docker', args, options];
|
|
419
484
|
}
|
|
@@ -45,8 +45,19 @@ const ownsObject = (obj) => {
|
|
|
45
45
|
// frees nothing.
|
|
46
46
|
const moduleObjects = new WeakSet();
|
|
47
47
|
|
|
48
|
+
// A pointer a member handed out holds a clone of the handle of the object it points into
|
|
49
|
+
// (utils/embindPointerOwners.js), which goes once no handle shares the pointer's count.
|
|
50
|
+
const releaseOwner = (obj) => {
|
|
51
|
+
const count = obj?.$$?.count;
|
|
52
|
+
if (!count?.owner || count.value > (obj.isDeleted() ? 0 : 1)) return;
|
|
53
|
+
const { owner } = count;
|
|
54
|
+
count.owner = undefined;
|
|
55
|
+
if (!owner.isDeleted()) owner.delete();
|
|
56
|
+
};
|
|
57
|
+
|
|
48
58
|
const freeIfOwned = (obj) => {
|
|
49
59
|
if (ownsObject(obj) && !moduleObjects.has(obj) && !obj.isDeleted()) obj.delete();
|
|
60
|
+
releaseOwner(obj);
|
|
50
61
|
};
|
|
51
62
|
|
|
52
63
|
// The worker side of a handle the main thread no longer reaches: its id goes, and the object with the last id.
|
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
2
|
+
"version": "1.0.21",
|
|
3
3
|
"registry": "ghcr.io/crossbind",
|
|
4
4
|
"toolchains": {
|
|
5
5
|
"rust": "1.99.1"
|
|
6
6
|
},
|
|
7
7
|
"images": {
|
|
8
8
|
"rust-sysroot": {
|
|
9
|
-
"index": "sha256:
|
|
9
|
+
"index": "sha256:de0e2969f74bb458d134f796d04f60ab2b2342ddc4f7d336a2ac945973c628a0",
|
|
10
10
|
"platforms": {
|
|
11
|
-
"linux/
|
|
12
|
-
"linux/
|
|
11
|
+
"linux/amd64": "sha256:2b4b338f185a44dfffd28eece9a79eedf7cef27fdd613ac4428bded4b394e773",
|
|
12
|
+
"linux/arm64": "sha256:8f0f72be0386b0a823db7520e23b1eb424da9daf64a364dffa983738d0b07035"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"base": {
|
|
16
|
-
"index": "sha256:
|
|
16
|
+
"index": "sha256:8894f7f3deb8e3980cf138fea3552e3375d6250d08aba5b3828909bf55d796fc",
|
|
17
17
|
"platforms": {
|
|
18
|
-
"linux/
|
|
19
|
-
"linux/
|
|
18
|
+
"linux/arm64": "sha256:9e8c9c6495d4c0e743c2a9b471b3e76fe8117507b44e7eb469009cd241473262",
|
|
19
|
+
"linux/amd64": "sha256:4a8305251eb48835bc13c9640303f6e9dfdb14be8ccb28d57ed808175d4dc8e9"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"web": {
|
|
23
|
-
"index": "sha256:
|
|
23
|
+
"index": "sha256:5497576cba5a945249f9ff76f8cc40b43b0a26c1d0ce9dd7874ed842550b96c2",
|
|
24
24
|
"platforms": {
|
|
25
|
-
"linux/arm64": "sha256:
|
|
26
|
-
"linux/amd64": "sha256:
|
|
25
|
+
"linux/arm64": "sha256:a661e57cd9a748b8053a3b315474b5e42f39eb691ef2834f9b57dc2f0cb3db17",
|
|
26
|
+
"linux/amd64": "sha256:82d2163287aba2d77af0db0517ee4a96e69e34e2d54d45b4ef6f39151a466494"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"android": {
|
|
30
|
-
"index": "sha256:
|
|
30
|
+
"index": "sha256:f54939cf8d648db75178d9a8cb1456453459b03d46fca7f2fa483b979677aa00",
|
|
31
31
|
"platforms": {
|
|
32
|
-
"linux/amd64": "sha256:
|
|
32
|
+
"linux/amd64": "sha256:1030809494407742bd71675d1c8d2967b08a42285ccb47ddf81fa52e8d41c2c6"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"android-app": {
|
|
36
|
-
"index": "sha256:
|
|
36
|
+
"index": "sha256:9be66dedaaf5f36e5795d080eaa43a523b79246c014719783c15e522d6d6f6c4",
|
|
37
37
|
"platforms": {
|
|
38
|
-
"linux/amd64": "sha256:
|
|
38
|
+
"linux/amd64": "sha256:1f7963b928513a1892c39e706515a0929abd4b6f72946c2943d476be2741725e"
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"linux": {
|
|
42
|
-
"index": "sha256:
|
|
42
|
+
"index": "sha256:4cdbe97aef06a84f82bf1f6e2f5badc354638c4f15364033ff1059247caa28ed",
|
|
43
43
|
"platforms": {
|
|
44
|
-
"linux/arm64": "sha256:
|
|
45
|
-
"linux/amd64": "sha256:
|
|
44
|
+
"linux/arm64": "sha256:b074c80b96455cfa8cfc1349bc1b77ce3286bff36d095831416c4e1111c69cf0",
|
|
45
|
+
"linux/amd64": "sha256:ca2194dd604dadfdb73f0bbe3b2e5a65ab6330bae6f9b8079b1c8ca9c9e2a11a"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"windows": {
|
|
49
|
-
"index": "sha256:
|
|
49
|
+
"index": "sha256:89dea8be3a1415c87854c2bccfb2cee95ec1d2f775b1b4db6260882d01d02a46",
|
|
50
50
|
"platforms": {
|
|
51
|
-
"linux/amd64": "sha256:
|
|
52
|
-
"linux/arm64": "sha256:
|
|
51
|
+
"linux/amd64": "sha256:454b9c339628d033d43f64422a12be9445193f167183fbeca71b4ae8af128673",
|
|
52
|
+
"linux/arm64": "sha256:9d823e18683cedfcc94ff6fb3c637c74929b4ee5601f72b3b87a4cedc7cd2d9e"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
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
|
}
|