crossbind 2.0.0-beta.72 → 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/addExtension.js +2 -3
- package/src/actions/buildApp.js +28 -4
- package/src/actions/createLib.js +4 -1
- package/src/actions/extensionCommands.js +2 -1
- package/src/actions/getDependLibs.js +5 -4
- package/src/actions/licenses.js +94 -22
- package/src/actions/pageLicenses.js +89 -0
- package/src/app/index.js +8 -7
- package/src/bin.js +37 -18
- package/src/index.js +4 -2
- package/src/licenses/index.d.ts +16 -0
- package/src/licenses/index.js +15 -0
- package/src/state/index.js +8 -6
- package/src/state/loadConfig.js +5 -0
- package/src/utils/appApiHeader.js +5 -5
- package/src/utils/appBuild.js +8 -0
- package/src/utils/appProject.js +24 -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/src/utils/resolveAppHost.js +10 -0
- package/src/utils/xcframeworkSlice.js +6 -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",
|
|
@@ -5,6 +5,7 @@ import { configFileOf, writeExtension } from '../utils/configWriter.js';
|
|
|
5
5
|
import { isUnderBase } from '../utils/replaceBasePathForDocker.js';
|
|
6
6
|
import { ownPackages } from '../utils/appExtensions.js';
|
|
7
7
|
import { packagesOfCrossbind } from '../utils/packageSources.js';
|
|
8
|
+
import { assertAppProject } from '../utils/appProject.js';
|
|
8
9
|
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
9
10
|
|
|
10
11
|
// The lockfiles by which a project names the package manager it is installed with.
|
|
@@ -144,9 +145,7 @@ function installedName(spec, before, after, manifest) {
|
|
|
144
145
|
export default function addExtension(spec, {
|
|
145
146
|
config, env = process.env, platform = process.platform, spawn = spawnSync,
|
|
146
147
|
}) {
|
|
147
|
-
|
|
148
|
-
throw new Error("crossbind: an extension builds into an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
|
|
149
|
-
}
|
|
148
|
+
assertAppProject(config, 'an extension builds into an app');
|
|
150
149
|
assertSpec(spec, platform);
|
|
151
150
|
assertNotOwn(spec);
|
|
152
151
|
const { project, base } = config.paths;
|
package/src/actions/buildApp.js
CHANGED
|
@@ -393,6 +393,33 @@ export function clearAppOutputs() {
|
|
|
393
393
|
else fs.rmSync(appOutputsFile(), { force: true });
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// The app target an entry of the folder the bundler builds into was written for, by its name: the target's app, or what
|
|
397
|
+
// packages it.
|
|
398
|
+
function outputTarget(entry) {
|
|
399
|
+
const { name } = state.config.general;
|
|
400
|
+
return state.targets.filter((t) => t.runtimeEnv === 'app').find((t) => [appOutputName(t), `${name}-${t.platform}-${t.arch}`]
|
|
401
|
+
.some((app) => entry === app || entry.startsWith(`${app}.`) || entry.startsWith(`${app}-`)));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// The paths a recorded output names: each desktop app with its installers' files, or a mobile app's project with its APK,
|
|
405
|
+
// bundle and debug symbols.
|
|
406
|
+
const outputPaths = (output) => (Array.isArray(output)
|
|
407
|
+
? output.flatMap(({ app, installers = [] }) => [app, ...installers.flatMap(({ files }) => files)])
|
|
408
|
+
: Object.values(output)).filter(Boolean);
|
|
409
|
+
|
|
410
|
+
// Empties the folder the bundler builds an app into, as the bundler would, but for git's own folder and what builds for the
|
|
411
|
+
// app's other platforms wrote there: their apps, named after their targets, and the files their installers recorded.
|
|
412
|
+
export function emptyAppOutDir(dir, platform = appPlatform()) {
|
|
413
|
+
if (!fs.existsSync(dir)) return;
|
|
414
|
+
const others = new Set(Object.entries(readAppOutputs()).filter(([name]) => name !== platform)
|
|
415
|
+
.flatMap(([, output]) => outputPaths(output)).map((file) => path.resolve(file)));
|
|
416
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
417
|
+
const target = outputTarget(entry);
|
|
418
|
+
const isOthers = target ? (DESKTOP_PLATFORMS[target.platform] ?? target.platform) !== platform : others.has(path.resolve(dir, entry));
|
|
419
|
+
if (entry !== '.git' && !isOthers) fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
396
423
|
// What a publisher's upload gets of the app the build wrote for a platform: a desktop platform's apps, one for each arch.
|
|
397
424
|
export function appOutputOf(platform) {
|
|
398
425
|
const output = readAppOutputs()[platform];
|
|
@@ -475,11 +502,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
|
|
|
475
502
|
// The app may land inside the page's own directory, which it then leaves out, with every app built there and what
|
|
476
503
|
// packages it: what is named after a target of the app, and every installer's files as a build recorded them, which
|
|
477
504
|
// a folder the bundler left as it was holds still.
|
|
478
|
-
const apps = state.targets.filter((t) => t.runtimeEnv === 'app')
|
|
479
|
-
.flatMap((t) => [appOutputName(t), `${state.config.general.name}-${t.platform}-${t.arch}`]).filter((name) => !name.endsWith('.app'));
|
|
480
505
|
const shipped = shippedFiles();
|
|
481
|
-
const isApp = (entry) => entry === path.basename(to) || entry.endsWith('.app')
|
|
482
|
-
|| apps.some((app) => entry === app || entry.startsWith(`${app}.`) || entry.startsWith(`${app}-`))
|
|
506
|
+
const isApp = (entry) => entry === path.basename(to) || entry.endsWith('.app') || Boolean(outputTarget(entry))
|
|
483
507
|
|| shipped.has(path.resolve(page, entry));
|
|
484
508
|
fs.readdirSync(page).filter((entry) => !isApp(entry)).forEach((entry) => {
|
|
485
509
|
fs.cpSync(`${page}/${entry}`, `${to}/${appLayout(target).resources}/page/${entry}`, { recursive: true });
|
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
|
|
|
@@ -3,6 +3,7 @@ import extensionApi from './extensionApi.js';
|
|
|
3
3
|
import state from '../state/index.js';
|
|
4
4
|
import { hasTerminal } from '../utils/ask.js';
|
|
5
5
|
import { extensionStep, ownExtensions, withoutNamed } from '../utils/appExtensions.js';
|
|
6
|
+
import { isAppProject } from '../utils/appProject.js';
|
|
6
7
|
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
7
8
|
|
|
8
9
|
const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
@@ -23,7 +24,7 @@ function ownOf(listed, app) {
|
|
|
23
24
|
// Adds to crossbind app the commands of an app's extensions, crossbind's own and those crossbind.config.js lists: a name
|
|
24
25
|
// another has stops the run, naming both.
|
|
25
26
|
export default function addExtensionCommands(parent, config = state.config) {
|
|
26
|
-
if (config?.target
|
|
27
|
+
if (!config?.target || !isAppProject(config)) return;
|
|
27
28
|
const listed = config.extensions ?? [];
|
|
28
29
|
const owners = new Map(parent.commands.map((command) => [command.name(), 'crossbind']));
|
|
29
30
|
for (const { name: extension, commands = {} } of [...ownOf(listed, config.app), ...listed]) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import findFiles from '../utils/findFiles.js';
|
|
2
|
+
import xcframeworkSliceOf from '../utils/xcframeworkSlice.js';
|
|
2
3
|
import state from '../state/index.js';
|
|
3
4
|
import { getFilteredTargetSpec } from './target.js';
|
|
4
5
|
|
|
@@ -21,10 +22,10 @@ export default function getDependLibs(target) {
|
|
|
21
22
|
if (libPaths.length === 0) {
|
|
22
23
|
libPaths = libsIn(target.releasePath);
|
|
23
24
|
}
|
|
24
|
-
// A published port can leave its iOS archive to its xcframework
|
|
25
|
-
|
|
26
|
-
if (libPaths.length === 0 &&
|
|
27
|
-
libPaths = findFiles(
|
|
25
|
+
// A published port can leave its iOS archive to its xcframework: the one of this package, as a project
|
|
26
|
+
// can list the port's packages of other platforms, whose libraries share the name.
|
|
27
|
+
if (libPaths.length === 0 && target.platform === 'ios') {
|
|
28
|
+
libPaths = findFiles(`${xcframeworkSliceOf(d.paths.project, fileName, target)}/lib${fileName}.a`, { cwd: d.paths.project });
|
|
28
29
|
}
|
|
29
30
|
dependLibs.push(...libPaths);
|
|
30
31
|
});
|
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/app/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// What an app's host does for its page: the app's folders, quitting, its windows, the events of the app's native code and
|
|
2
|
-
// the functions of its extensions, by their names. Each call goes to the host of an app
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// the functions of its extensions, by their names. Each call goes to the host of an app, which crossbind app builds; in a
|
|
3
|
+
// browser it rejects, or events.on throws. A page imports these from crossbind/app, which an app's bundler plugin takes
|
|
4
|
+
// from the app host, with what crossbind's own extensions give the page, as crossbind/app/core alone holds these.
|
|
5
5
|
|
|
6
|
-
// The app's crossbind.js, which initNative() loads, hands the calls to the host.
|
|
6
|
+
// The app's crossbind.js, which initNative() loads, hands the calls to the host. A browser has crossbind's glue as well,
|
|
7
|
+
// but no token of a host, and none of its functions.
|
|
7
8
|
function hostOf(member, name) {
|
|
8
9
|
const host = globalThis.Crossbind?.[member];
|
|
9
10
|
if (host) return host;
|
|
11
|
+
if (!globalThis.__crossbind_token && !globalThis.Crossbind?.app) throw new Error(`crossbind: ${name} works in an app alone, which crossbind app builds`);
|
|
10
12
|
if (globalThis.Crossbind) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to call ${name}`);
|
|
11
|
-
|
|
12
|
-
throw new Error(`crossbind: ${name} works in an app alone, whose crossbind.config.js sets target.runtimeEnv: 'app'`);
|
|
13
|
+
throw new Error(`crossbind: ${name} works once the page has called initNative()`);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const call = (name, options) => hostOf('app', name)(name, options);
|
|
@@ -106,7 +107,7 @@ function deviceOf(name) {
|
|
|
106
107
|
const device = globalThis.__crossbind_device;
|
|
107
108
|
if (device) return device[name];
|
|
108
109
|
if (globalThis.__crossbind_token) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to read device.${name}`);
|
|
109
|
-
throw new Error(`crossbind: device.${name} works in an app alone,
|
|
110
|
+
throw new Error(`crossbind: device.${name} works in an app alone, which crossbind app builds`);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// Where the app runs, the same through its run, which the page reads at once, as it starts: the platform and the arch
|
package/src/bin.js
CHANGED
|
@@ -15,6 +15,7 @@ import buildWasiCommand from './actions/buildWasiCommand.js';
|
|
|
15
15
|
import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
|
|
16
16
|
import buildNativeCommand from './actions/buildNativeCommand.js';
|
|
17
17
|
import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
|
|
18
|
+
import { assertAppProject } from './utils/appProject.js';
|
|
18
19
|
import {
|
|
19
20
|
makeUploadKey, releaseSigningEnv, showUploadKeyPassword, useUploadKey,
|
|
20
21
|
} from './actions/uploadKey.js';
|
|
@@ -47,9 +48,9 @@ import { cargoHome } from './utils/runCargo.js';
|
|
|
47
48
|
import { containerRootOf, sourceRoot } from './utils/runPackageSource.js';
|
|
48
49
|
import { importSchemesOf, sourceNamesOf } from './utils/packageSources.js';
|
|
49
50
|
import { cleanDepsCache } from './utils/dependencyRebuild.js';
|
|
50
|
-
import collectLicenseRows from './actions/licenses.js';
|
|
51
|
+
import collectLicenseRows, { vendoringLibraries } from './actions/licenses.js';
|
|
51
52
|
import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
|
|
52
|
-
import { SBOM_FILE, nodePackageMeta, writeNodePackageLicense } from './utils/packageLicense.js';
|
|
53
|
+
import { SBOM_FILE, nodePackageMeta, shippedRows, writeNodePackageLicense } from './utils/packageLicense.js';
|
|
53
54
|
import findFiles from './utils/findFiles.js';
|
|
54
55
|
|
|
55
56
|
const platforms = [...new Set(state.targets.map(target => target.platform).filter(t => t))];
|
|
@@ -113,19 +114,34 @@ function declaredSbomPath(projectDir) {
|
|
|
113
114
|
// An app's page is the project's Vite build, which @crossbind/plugin-vite makes into the app of the platform named.
|
|
114
115
|
const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
|
|
115
116
|
|
|
117
|
+
// Vite's CLI beside the entry of the package vite resolves to, where Vite keeps it and vite-plus-core too, whose vp runs
|
|
118
|
+
// it from there. An entry outside the package is another one's, which a lookup past it found.
|
|
119
|
+
function viteCliBesideEntry(require, packageDir) {
|
|
120
|
+
try {
|
|
121
|
+
const entry = require.resolve('vite');
|
|
122
|
+
const cli = path.join(path.dirname(entry), 'cli.js');
|
|
123
|
+
return entry.startsWith(`${packageDir}${path.sep}`) && fs.existsSync(cli) ? cli : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
function projectVite() {
|
|
130
|
+
const require = createRequire(`${state.config.paths.project}/package.json`);
|
|
117
131
|
let manifestPath;
|
|
118
132
|
try {
|
|
119
|
-
manifestPath =
|
|
133
|
+
manifestPath = require.resolve('vite/package.json');
|
|
120
134
|
} catch {
|
|
121
135
|
throw new Error("crossbind: crossbind app builds the app's page with the project's Vite, which the project does not have. Add vite and @crossbind/plugin-vite to it, as the desktop-app template has them.");
|
|
122
136
|
}
|
|
123
|
-
// A workspace can map vite to a package of Vite's API alone.
|
|
137
|
+
// A workspace can map vite to a package of Vite's API alone, as vite-plus maps it to vite-plus-core.
|
|
124
138
|
const { name, version, bin } = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
125
|
-
if (
|
|
126
|
-
|
|
139
|
+
if (bin?.vite) return path.join(path.dirname(manifestPath), bin.vite);
|
|
140
|
+
const cli = viteCliBesideEntry(require, path.dirname(manifestPath));
|
|
141
|
+
if (!cli) {
|
|
142
|
+
throw new Error(`crossbind: crossbind app runs the project's Vite, and its vite resolves to ${name} ${version}, which has neither a vite command nor Vite's cli.js beside its entry. Have vite resolve to Vite itself for the project, with an override where its workspace maps vite to another package.`);
|
|
127
143
|
}
|
|
128
|
-
return
|
|
144
|
+
return cli;
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
// The project's Vite, as `crossbind app dev`, `build` and `publish` run it, and its exit code once it ends. A signal sent
|
|
@@ -143,11 +159,7 @@ function runVite(args, env) {
|
|
|
143
159
|
});
|
|
144
160
|
}
|
|
145
161
|
|
|
146
|
-
|
|
147
|
-
if (state.config.target.runtimeEnv !== 'app') {
|
|
148
|
-
throw new Error("crossbind: crossbind app builds an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
|
|
149
|
-
}
|
|
150
|
-
}
|
|
162
|
+
const assertAppCommand = () => assertAppProject(state.config, 'crossbind app builds an app');
|
|
151
163
|
|
|
152
164
|
const commandApp = program.command('app').description('build the project as an app, or open it on the dev server');
|
|
153
165
|
[
|
|
@@ -164,12 +176,12 @@ const commandApp = program.command('app').description('build the project as an a
|
|
|
164
176
|
.allowUnknownOption()
|
|
165
177
|
.allowExcessArguments()
|
|
166
178
|
.action(async ({ platform, device }, command) => {
|
|
167
|
-
|
|
179
|
+
assertAppCommand();
|
|
168
180
|
// An Android build checks its upload key before the native build, not after it.
|
|
169
181
|
const isAndroidBuild = name === 'build' && (platform ?? process.env.CROSSBIND_APP_PLATFORM) === 'android';
|
|
170
182
|
const { env: signing } = isAndroidBuild ? await releaseSigningEnv() : { env: {} };
|
|
171
183
|
const onDevice = device === undefined ? {} : { CROSSBIND_APP_DEVICE: iosDeviceOption(device, platform ?? process.env.CROSSBIND_APP_PLATFORM) };
|
|
172
|
-
const env = { ...process.env, ...signing, ...(platform ? { CROSSBIND_APP_PLATFORM: platform } : {}), ...onDevice };
|
|
184
|
+
const env = { ...process.env, ...signing, CROSSBIND_APP_BUILD: '1', ...(platform ? { CROSSBIND_APP_PLATFORM: platform } : {}), ...onDevice };
|
|
173
185
|
process.exit(await runVite([...(name === 'build' ? ['build'] : []), ...command.args], env));
|
|
174
186
|
});
|
|
175
187
|
});
|
|
@@ -195,7 +207,7 @@ commandApp.command('publish')
|
|
|
195
207
|
.allowUnknownOption()
|
|
196
208
|
.allowExcessArguments()
|
|
197
209
|
.action(async ({ platform }, command) => {
|
|
198
|
-
|
|
210
|
+
assertAppCommand();
|
|
199
211
|
await publish(command.args, { platform, envPlatform: process.env.CROSSBIND_APP_PLATFORM, runVite });
|
|
200
212
|
});
|
|
201
213
|
|
|
@@ -228,11 +240,17 @@ program.command('licenses')
|
|
|
228
240
|
});
|
|
229
241
|
const copyleftRows = rows.filter((row) => row.isCopyleft);
|
|
230
242
|
if (copyleftRows.length > 0) {
|
|
231
|
-
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
|
+
}
|
|
232
250
|
}
|
|
233
251
|
if (options.notices) {
|
|
234
252
|
const file = typeof options.notices === 'string' ? options.notices : `${state.config.paths.project}/THIRD-PARTY-NOTICES.md`;
|
|
235
|
-
fs.writeFileSync(file, formatNoticesMarkdown(rows));
|
|
253
|
+
fs.writeFileSync(file, formatNoticesMarkdown(rows, nodePackageMeta(state.config)));
|
|
236
254
|
logger.info(`crossbind: wrote ${file}`);
|
|
237
255
|
}
|
|
238
256
|
if (options.sbom) {
|
|
@@ -253,7 +271,8 @@ program.command('licenses')
|
|
|
253
271
|
logger.info(`crossbind: wrote ${file}`);
|
|
254
272
|
}
|
|
255
273
|
if (options.package) {
|
|
256
|
-
const
|
|
274
|
+
const meta = nodePackageMeta(state.config);
|
|
275
|
+
const expression = writeNodePackageLicense(state.config.paths.project, { ...meta, rows: shippedRows(rows, meta) });
|
|
257
276
|
logger.info(`crossbind: wrote LICENSE and ${SBOM_FILE} (${expression})`);
|
|
258
277
|
}
|
|
259
278
|
if (options.check) {
|
package/src/index.js
CHANGED
|
@@ -4,10 +4,12 @@ export { default as createLib } from './actions/createLib.js';
|
|
|
4
4
|
export { default as createBridgeFile } from './actions/createInterface.js';
|
|
5
5
|
export { default as buildWasm } from './actions/buildWasm.js';
|
|
6
6
|
export {
|
|
7
|
-
default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appPlatform, appTargetParams,
|
|
7
|
+
default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appPlatform, appTargetParams, emptyAppOutDir,
|
|
8
8
|
} from './actions/buildApp.js';
|
|
9
9
|
export { checkInstaller, shipApp } from './actions/appInstaller.js';
|
|
10
|
-
export { resolveAppPageApi } from './utils/resolveAppHost.js';
|
|
10
|
+
export { findAppPageApi, resolveAppPageApi } from './utils/resolveAppHost.js';
|
|
11
|
+
export { default as isAppBuild } from './utils/appBuild.js';
|
|
12
|
+
export { default as pageLicensesAsset } from './actions/pageLicenses.js';
|
|
11
13
|
export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
|
|
12
14
|
export {
|
|
13
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/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
|
|
|
10
10
|
import loadConfig, { assertBinarySelection } from './loadConfig.js';
|
|
11
11
|
import { configChangesOf } from '../utils/configChanges.js';
|
|
12
12
|
import refreshPackageSources from './refreshPackageSources.js';
|
|
13
|
+
import xcframeworkSliceOf from '../utils/xcframeworkSlice.js';
|
|
13
14
|
|
|
14
15
|
const cacheDir = `${process.cwd()}/.crossbind`;
|
|
15
16
|
|
|
@@ -97,6 +98,7 @@ export function setAllDependecyPaths() {
|
|
|
97
98
|
state.config.allDependencyPaths = {};
|
|
98
99
|
state.targets.forEach((target) => {
|
|
99
100
|
state.config.allDependencyPaths[target.path] = { cmake: {} };
|
|
101
|
+
const owners = new Map();
|
|
100
102
|
state.config.allDependencies.forEach((d) => {
|
|
101
103
|
state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
|
|
102
104
|
// A package source's recipe can name a library on another platform only (libpng is png16 on Windows).
|
|
@@ -105,6 +107,11 @@ export function setAllDependecyPaths() {
|
|
|
105
107
|
.some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
|
|
106
108
|
: d.export.libName;
|
|
107
109
|
libNames.forEach((name) => {
|
|
110
|
+
// A project can list a port's packages for several platforms, which share the library's name: the one
|
|
111
|
+
// that serves the target keeps the entry.
|
|
112
|
+
const owner = owners.get(name);
|
|
113
|
+
if (owner && owner !== d && d.functions?.isEnabled?.(target) === false) return;
|
|
114
|
+
owners.set(name, d);
|
|
108
115
|
state.config.allDependencyPaths[target.path][name] = {
|
|
109
116
|
root: `${d.paths.output}/prebuilt/${target.path}`,
|
|
110
117
|
};
|
|
@@ -118,12 +125,7 @@ export function setAllDependecyPaths() {
|
|
|
118
125
|
const isDynamicLib = target.platform === 'android' && platformConfig.libType !== 'static';
|
|
119
126
|
const dep = state.config.allDependencyPaths[target.path][name];
|
|
120
127
|
if (target.platform === 'ios') {
|
|
121
|
-
|
|
122
|
-
if (target.arch === 'iphoneos') {
|
|
123
|
-
xcRoot = `${d.paths.project}/${name}.xcframework/ios-arm64`;
|
|
124
|
-
} else if (target.arch === 'iphonesimulator') {
|
|
125
|
-
xcRoot = `${d.paths.project}/${name}.xcframework/ios-arm64-simulator`;
|
|
126
|
-
}
|
|
128
|
+
const xcRoot = xcframeworkSliceOf(d.paths.project, name, target);
|
|
127
129
|
dep.header = `${xcRoot}/Headers`;
|
|
128
130
|
dep.libPath = xcRoot;
|
|
129
131
|
dep.lib = `${dep.libPath}/lib${name}.a`;
|
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
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// <crossbind/app.h>, which an app's own code includes for the host's functions (docs/api/app.md).
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
-
import
|
|
4
|
+
import isAppBuild from './appBuild.js';
|
|
5
5
|
import resolveAppHostRoot from './resolveAppHost.js';
|
|
6
6
|
import { runtimeEnvsOf } from './targets.js';
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
// on, and in every flavour of that platform's archives, which share one build. A library and an
|
|
10
|
-
// neither.
|
|
8
|
+
// In a run that builds an app its project's native code compiles against the header, with CROSSBIND_APP defined, on every
|
|
9
|
+
// platform an app runs on, and in every flavour of that platform's archives, which share one build. A library and an
|
|
10
|
+
// app's web build get neither.
|
|
11
11
|
export function compilesAsApp(target) {
|
|
12
|
-
return
|
|
12
|
+
return isAppBuild() && runtimeEnvsOf(target.platform).includes('app');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// What an app's own code compiles with: CROSSBIND_APP, and on iOS its debug information, from which the archive's dSYM
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import state from '../state/index.js';
|
|
2
|
+
|
|
3
|
+
// Whether this run builds an app: crossbind app says so to the bundler it starts, CROSSBIND_APP_PLATFORM names an app's
|
|
4
|
+
// platform, or the project is an app alone by target.runtimeEnv. Without any of these a bundler plugin builds what a
|
|
5
|
+
// browser loads, so one project makes both by the command that builds it.
|
|
6
|
+
export default function isAppBuild(env = process.env) {
|
|
7
|
+
return env.CROSSBIND_APP_BUILD === '1' || Boolean(env.CROSSBIND_APP_PLATFORM) || state.config.target?.runtimeEnv === 'app';
|
|
8
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const APP_HOST = '@crossbind/core-app-host';
|
|
5
|
+
|
|
6
|
+
function listsAppHost(project) {
|
|
7
|
+
const manifest = path.join(project, 'package.json');
|
|
8
|
+
if (!fs.existsSync(manifest)) return false;
|
|
9
|
+
const { dependencies, devDependencies } = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
10
|
+
return Boolean(dependencies?.[APP_HOST] ?? devDependencies?.[APP_HOST]);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A project an app builds from: one that says it is an app, or that has the app host and leaves its environment to the
|
|
14
|
+
// command that builds it.
|
|
15
|
+
export const isAppProject = ({ target, paths }) => target?.runtimeEnv === 'app' || (!target?.runtimeEnv && listsAppHost(paths.project));
|
|
16
|
+
|
|
17
|
+
// Stops what only an app does, in a project that is none, saying what would make it one.
|
|
18
|
+
export function assertAppProject(config, doing) {
|
|
19
|
+
if (isAppProject(config)) return;
|
|
20
|
+
const { runtimeEnv } = config.target;
|
|
21
|
+
throw new Error(runtimeEnv
|
|
22
|
+
? `crossbind: ${doing}, and this project's target.runtimeEnv says ${runtimeEnv}: leave it out, or set it to 'app'.`
|
|
23
|
+
: `crossbind: ${doing}, which this project is not: add ${APP_HOST} to its devDependencies, as the desktop-app template does.`);
|
|
24
|
+
}
|
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
|
+
}
|
|
@@ -26,3 +26,13 @@ export function resolveAppPageApi() {
|
|
|
26
26
|
if (!fs.existsSync(file)) throw new Error(`crossbind: the app host at ${resolveAppHostRoot()} has no crossbind/app for an app's page: update ${PKG} to match crossbind.`);
|
|
27
27
|
return fs.realpathSync(file);
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
// The same for a page a bundler builds for the browser, where a project has the app host: code an app shares with its
|
|
31
|
+
// web build still bundles, and what it calls rejects as outside an app. A project without the host has none.
|
|
32
|
+
export function findAppPageApi() {
|
|
33
|
+
try {
|
|
34
|
+
return resolveAppPageApi();
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The slice of a package's <name>.xcframework that an iOS target links: crossbind's packages carry arm64 alone.
|
|
2
|
+
const SLICES = { iphoneos: 'ios-arm64', iphonesimulator: 'ios-arm64-simulator' };
|
|
3
|
+
|
|
4
|
+
export default function xcframeworkSliceOf(projectPath, name, target) {
|
|
5
|
+
return `${projectPath}/${name}.xcframework/${SLICES[target.arch]}`;
|
|
6
|
+
}
|