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.
@@ -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
- ...shared,
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
- const upstream = family?.manifest?.crossbind?.upstream?.license || null;
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. runtimeEnv 'node' adds the crossbind runtime an
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 node of nodes) {
519
- const key = keyOf(node);
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
+ }
@@ -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
- // '-e', replaceBasePathForDocker(`CCACHE_DIR=${state.config.paths.build}/ccache`),
479
+ ...sccache,
414
480
  imageOrContainer,
415
- replaceBasePathForDocker(dProgram),
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.20",
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:3f62871ccbebbdca53b798fa1b21173bee848ada7744f5108df66a290d3cca78",
9
+ "index": "sha256:de0e2969f74bb458d134f796d04f60ab2b2342ddc4f7d336a2ac945973c628a0",
10
10
  "platforms": {
11
- "linux/arm64": "sha256:ec7c2bba3add2385099b71520c5d68540e8dba691f57a42c0e9143ea5b8981df",
12
- "linux/amd64": "sha256:dfad6e04d3168f5e7375aba271d0e615cc6658793527503ce3070efbd4ac5f91"
11
+ "linux/amd64": "sha256:2b4b338f185a44dfffd28eece9a79eedf7cef27fdd613ac4428bded4b394e773",
12
+ "linux/arm64": "sha256:8f0f72be0386b0a823db7520e23b1eb424da9daf64a364dffa983738d0b07035"
13
13
  }
14
14
  },
15
15
  "base": {
16
- "index": "sha256:a5c7ea17f3e13c13d3c9f53307cce95d037bbb6b58b08034cfa2cb8112a63407",
16
+ "index": "sha256:8894f7f3deb8e3980cf138fea3552e3375d6250d08aba5b3828909bf55d796fc",
17
17
  "platforms": {
18
- "linux/amd64": "sha256:a0934c868ccd5a09de14d193ae33b091ecfddf3c13548595e918c0b053e41dd6",
19
- "linux/arm64": "sha256:14d4bcea7537972052cd6cee314a5c08f1cd68eebd3a7461ff7ca594994c49eb"
18
+ "linux/arm64": "sha256:9e8c9c6495d4c0e743c2a9b471b3e76fe8117507b44e7eb469009cd241473262",
19
+ "linux/amd64": "sha256:4a8305251eb48835bc13c9640303f6e9dfdb14be8ccb28d57ed808175d4dc8e9"
20
20
  }
21
21
  },
22
22
  "web": {
23
- "index": "sha256:00a19f61fa46315175be11f20cea051311c218ab6e99bd1bca70236415a69af9",
23
+ "index": "sha256:5497576cba5a945249f9ff76f8cc40b43b0a26c1d0ce9dd7874ed842550b96c2",
24
24
  "platforms": {
25
- "linux/arm64": "sha256:6e75223e8c696d93f5ab57e286425b4b94c4b1dcb3cf291e63c2820636dfa2a6",
26
- "linux/amd64": "sha256:fe4e84f435838a35f2d60774be0050c39f2ecf1b8ed6cca23530b8c32f5c6211"
25
+ "linux/arm64": "sha256:a661e57cd9a748b8053a3b315474b5e42f39eb691ef2834f9b57dc2f0cb3db17",
26
+ "linux/amd64": "sha256:82d2163287aba2d77af0db0517ee4a96e69e34e2d54d45b4ef6f39151a466494"
27
27
  }
28
28
  },
29
29
  "android": {
30
- "index": "sha256:df5bf439ab81afbe28a166dcd3c0ec281aea11d4265b1fc24225cc686f643ce1",
30
+ "index": "sha256:f54939cf8d648db75178d9a8cb1456453459b03d46fca7f2fa483b979677aa00",
31
31
  "platforms": {
32
- "linux/amd64": "sha256:d2f5460a998aa1695b04d0d1bc231af4d77dd78931ca0f2b02adec51a61f0c02"
32
+ "linux/amd64": "sha256:1030809494407742bd71675d1c8d2967b08a42285ccb47ddf81fa52e8d41c2c6"
33
33
  }
34
34
  },
35
35
  "android-app": {
36
- "index": "sha256:d72766f41a7d0343fdf36a425a9c5e0ea7dda8ac6f3b2697fe49d79a21e5018b",
36
+ "index": "sha256:9be66dedaaf5f36e5795d080eaa43a523b79246c014719783c15e522d6d6f6c4",
37
37
  "platforms": {
38
- "linux/amd64": "sha256:8e62eea8685cd28276dfdd28b34aa75b3a0a677b40a93ea3f28f7f3ef1565425"
38
+ "linux/amd64": "sha256:1f7963b928513a1892c39e706515a0929abd4b6f72946c2943d476be2741725e"
39
39
  }
40
40
  },
41
41
  "linux": {
42
- "index": "sha256:a4828cf2821cc2313f5ddf5bc761073c90616fbef09daa8b8d93e7fa3050497a",
42
+ "index": "sha256:4cdbe97aef06a84f82bf1f6e2f5badc354638c4f15364033ff1059247caa28ed",
43
43
  "platforms": {
44
- "linux/arm64": "sha256:501a878b581295d3946945dfa9bc98a0aa3de5d4c6bc006c7224756cccbf1e35",
45
- "linux/amd64": "sha256:ee5a4f7ee23fdda7e50d4130d783a76d7c44d079ae34b4a326565510eea761dc"
44
+ "linux/arm64": "sha256:b074c80b96455cfa8cfc1349bc1b77ce3286bff36d095831416c4e1111c69cf0",
45
+ "linux/amd64": "sha256:ca2194dd604dadfdb73f0bbe3b2e5a65ab6330bae6f9b8079b1c8ca9c9e2a11a"
46
46
  }
47
47
  },
48
48
  "windows": {
49
- "index": "sha256:13fc02ab67d1465cbe75715d7de6a87c5319477eba2c0459c914bb5b698df766",
49
+ "index": "sha256:89dea8be3a1415c87854c2bccfb2cee95ec1d2f775b1b4db6260882d01d02a46",
50
50
  "platforms": {
51
- "linux/amd64": "sha256:78883c59167af69c5b9433ef6cdfa35aab3833501bb85d830d95f24079a6c8d0",
52
- "linux/arm64": "sha256:f27f09f8e661261c770976580d9861d4ce290eb1ac3030e6198f3c0a4987bd8a"
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 expression = writeNodePackageLicense(state.config.paths.project, { ...nodePackageMeta(state.config), rows });
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
+ }
@@ -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
  }