crossbind 2.0.0-beta.69 → 2.0.0-beta.73
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 +3 -2
- package/src/actions/addExtension.js +8 -4
- package/src/actions/buildApp.js +38 -9
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +23 -21
- package/src/actions/createLib.js +3 -3
- package/src/actions/extensionCommands.js +2 -1
- package/src/actions/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +6 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +13 -2
- package/src/actions/licenses.js +30 -24
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +12 -6
- package/src/app/index.d.ts +19 -0
- package/src/app/index.js +32 -7
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/bin.js +37 -17
- package/src/index.js +4 -3
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +14 -11
- package/src/state/loadConfig.js +10 -4
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appApiHeader.js +5 -5
- package/src/utils/appBuild.js +8 -0
- package/src/utils/appExtensions.js +44 -28
- package/src/utils/appOptions.js +31 -10
- package/src/utils/appProject.js +24 -0
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/bridgeTools.js +6 -1
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +38 -1
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/packageSourceApi.js +87 -0
- package/src/utils/packageSourceImport.js +69 -0
- package/src/utils/packageSourceInstall.js +162 -0
- package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
- package/src/utils/packageSources.js +108 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/resolveAppHost.js +10 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/targetToolchain.js +77 -0
- package/src/utils/xcframeworkSlice.js +6 -0
- package/src/state/refreshConanDependencies.js +0 -17
- package/src/utils/conanDependencies.js +0 -66
- package/src/utils/conanImport.js +0 -58
- package/src/utils/conanInstall.js +0 -154
- package/src/utils/conanProfile.js +0 -203
- package/src/utils/runConan.js +0 -257
package/src/state/loadConfig.js
CHANGED
|
@@ -12,7 +12,7 @@ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
|
|
|
12
12
|
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
13
13
|
import logger from '../utils/logger.js';
|
|
14
14
|
import { RUNTIME_ENVS } from '../utils/targets.js';
|
|
15
|
-
import {
|
|
15
|
+
import { checkedPackages, packageSourcesOf } from '../utils/packageSources.js';
|
|
16
16
|
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
17
17
|
import { listedExtension } from '../utils/appExtensions.js';
|
|
18
18
|
import { mergePatch } from '../utils/configChanges.js';
|
|
@@ -44,7 +44,9 @@ export default async function loadConfig(configDir = process.cwd(), configName =
|
|
|
44
44
|
const overrides = await loadJs(configDir, 'crossbind.overrides');
|
|
45
45
|
const exclude = resolveExcludedNames(overrides);
|
|
46
46
|
const seen = new Set();
|
|
47
|
-
const output = getFilledConfig(config, {
|
|
47
|
+
const output = getFilledConfig(config, {
|
|
48
|
+
isDepend: false, exclude, seen, replaces: overrides, file: `${configDir}/${configName}`,
|
|
49
|
+
});
|
|
48
50
|
output.excludedDependencies = exclude;
|
|
49
51
|
|
|
50
52
|
const build = await loadJs(configDir, 'crossbind.build');
|
|
@@ -126,8 +128,6 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
126
128
|
const newConfig = {
|
|
127
129
|
general: config.general || {},
|
|
128
130
|
dependencies,
|
|
129
|
-
cargoDependencies: config.cargoDependencies || {},
|
|
130
|
-
conanDependencies: normalizeConanDependencies(config.conanDependencies),
|
|
131
131
|
// 'sync' types the direct surface; 'promise' wraps every generated method return
|
|
132
132
|
// for useWorker-style runtimes (constructors stay sync-typed: await new X()).
|
|
133
133
|
dts: config.dts || 'sync',
|
|
@@ -214,6 +214,12 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
214
214
|
const twice = newConfig.extensions.find(({ name }, i) => newConfig.extensions.findIndex((other) => other.name === name) !== i);
|
|
215
215
|
if (twice) throw new Error(`crossbind: "extensions" lists two extensions named ${twice.name}.`);
|
|
216
216
|
|
|
217
|
+
// What the project declares under packages: cargo's crates, and each package source's packages, as packages.conan,
|
|
218
|
+
// as the source's own check takes them. A package installs nothing for the project that depends on it, so a
|
|
219
|
+
// dependency's own declarations are not read.
|
|
220
|
+
newConfig.packageSources = packageSourcesOf(newConfig.extensions);
|
|
221
|
+
newConfig.packages = checkedPackages(newConfig.packageSources, options.isDepend ? {} : config, options.file);
|
|
222
|
+
|
|
217
223
|
if (newConfig.target.runtime !== 'mt' && newConfig.allDependencies.some((d) => d?.target?.runtime === 'mt')) {
|
|
218
224
|
newConfig.target.runtime = 'mt';
|
|
219
225
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import attachPackageSources from './attachPackageSources.js';
|
|
2
|
+
import { sourceStageDir } from '../utils/packageSourceImport.js';
|
|
3
|
+
import { declaredSources } from '../utils/packageSources.js';
|
|
4
|
+
import { sourceManifestsStamp } from '../utils/packageSourceStage.js';
|
|
5
|
+
|
|
6
|
+
const attachedFrom = new WeakMap();
|
|
7
|
+
|
|
8
|
+
// Attaches what a build staged since this config last attached: a process that loaded state before
|
|
9
|
+
// the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
|
|
10
|
+
// it attached again.
|
|
11
|
+
export default function refreshPackageSources(config) {
|
|
12
|
+
const declared = declaredSources(config);
|
|
13
|
+
if (declared.length === 0) return false;
|
|
14
|
+
const stamp = declared.map(({ name }) => `${name}:${sourceManifestsStamp(sourceStageDir(config.paths.cache, name))}`).join('\n');
|
|
15
|
+
if (attachedFrom.get(config) === stamp) return false;
|
|
16
|
+
attachedFrom.set(config, stamp);
|
|
17
|
+
attachPackageSources(config);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -3,12 +3,13 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { carriedPath, isOutside } from './appFiles.js';
|
|
5
5
|
import {
|
|
6
|
-
NAME, checkedCommands, checkedInstaller, checkedPublisher,
|
|
6
|
+
NAME, checkedCommands, checkedInstaller, checkedPackageSource, checkedPublisher,
|
|
7
7
|
} from './extensionSteps.js';
|
|
8
8
|
import { checkedActivityTypes, checkedFileTypes, checkedLinks, checkedShortcutItems } from './appLaunches.js';
|
|
9
9
|
import { checkedPrivacy, checkedUrlSchemes } from './appOptions.js';
|
|
10
10
|
import { checkedTargetKind, checkedTargetOptions, isHeader } from './appTargets.js';
|
|
11
11
|
import { isUnderBase } from './replaceBasePathForDocker.js';
|
|
12
|
+
import { printable } from './printableText.js';
|
|
12
13
|
|
|
13
14
|
// What an app extension compiles with on every platform, and on each: its sources, the folders of its own their
|
|
14
15
|
// includes are found in, and the macros they are compiled with.
|
|
@@ -41,7 +42,7 @@ const LINK_NAME = /^[A-Za-z0-9][A-Za-z0-9_.+-]*$/;
|
|
|
41
42
|
// A class in a package, which the activity's package can name.
|
|
42
43
|
const JAVA_CLASS = /^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*)+$/;
|
|
43
44
|
|
|
44
|
-
export { PUBLISH_PLATFORMS, checkedInstaller, checkedPublisher } from './extensionSteps.js';
|
|
45
|
+
export { PUBLISH_PLATFORMS, checkedInstaller, checkedPackageSource, checkedPublisher } from './extensionSteps.js';
|
|
45
46
|
|
|
46
47
|
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
47
48
|
const isEntitlement = (value) => typeof value === 'boolean' || typeof value === 'string'
|
|
@@ -338,6 +339,9 @@ export function checkedExtension(extension, root, where) {
|
|
|
338
339
|
},
|
|
339
340
|
}),
|
|
340
341
|
...(extension.commands === undefined ? {} : { commands: checkedCommands(extension.commands, `${where}'s commands`) }),
|
|
342
|
+
...(extension.packageSource === undefined ? {} : {
|
|
343
|
+
packageSource: checkedPackageSource(extension.packageSource, `${where}'s packageSource`, extension.name),
|
|
344
|
+
}),
|
|
341
345
|
...(extension.fromApp === undefined ? {} : { fromApp: extension.fromApp.bind(extension) }),
|
|
342
346
|
...checkedOwnKeys(extension, where),
|
|
343
347
|
};
|
|
@@ -352,11 +356,14 @@ export function checkedExtension(extension, root, where) {
|
|
|
352
356
|
// extension written for any of them builds. A minor version that adds to the contract raises the newest, and a major
|
|
353
357
|
// version that breaks it the oldest.
|
|
354
358
|
export const OLDEST_EXTENSION_CONTRACT = 1;
|
|
355
|
-
export const EXTENSION_CONTRACT =
|
|
359
|
+
export const EXTENSION_CONTRACT = 2;
|
|
360
|
+
// What a later version added to an extension, by the version that did.
|
|
361
|
+
const ADDED_IN = { packageSource: 2 };
|
|
356
362
|
|
|
357
363
|
// The contract an extension is written for, or the several it is written for alike, is one this crossbind takes: one
|
|
358
|
-
// written for others alone stops the build, naming what to update.
|
|
359
|
-
function checkedContract(
|
|
364
|
+
// written for others alone stops the build, naming what to update, as one that gives what its contract has not.
|
|
365
|
+
function checkedContract(extension, where, update) {
|
|
366
|
+
const { contract } = extension;
|
|
360
367
|
if (contract === undefined) {
|
|
361
368
|
throw new Error(`crossbind: ${where} names no contract, the version of crossbind's extension contract it is written for, as contract: ${EXTENSION_CONTRACT} is for this crossbind: ${update}.`);
|
|
362
369
|
}
|
|
@@ -364,8 +371,12 @@ function checkedContract({ contract }, where, update) {
|
|
|
364
371
|
if (versions.length === 0 || versions.some((version) => !Number.isInteger(version) || version < 1) || new Set(versions).size !== versions.length) {
|
|
365
372
|
throw new Error(`crossbind: ${where}'s contract ${JSON.stringify(contract)} is no version of crossbind's extension contract, as 1 or [1, 2] is.`);
|
|
366
373
|
}
|
|
367
|
-
if (versions.some((version) => version >= OLDEST_EXTENSION_CONTRACT && version <= EXTENSION_CONTRACT)) return;
|
|
368
374
|
const named = versions.join(' and ');
|
|
375
|
+
const early = Object.entries(ADDED_IN).find(([key, since]) => extension[key] !== undefined && versions.some((version) => version < since));
|
|
376
|
+
if (early) {
|
|
377
|
+
throw new Error(`crossbind: ${where} gives a ${early[0]}, which crossbind's extension contract has from its version ${early[1]} on, and is written for ${named}: name contract: ${early[1]}.`);
|
|
378
|
+
}
|
|
379
|
+
if (versions.some((version) => version >= OLDEST_EXTENSION_CONTRACT && version <= EXTENSION_CONTRACT)) return;
|
|
369
380
|
if (versions.every((version) => version > EXTENSION_CONTRACT)) {
|
|
370
381
|
throw new Error(`crossbind: ${where} is written for crossbind's extension contract ${named}, newer than this crossbind's ${EXTENSION_CONTRACT}: update crossbind.`);
|
|
371
382
|
}
|
|
@@ -407,7 +418,7 @@ export function extensionPackage(root, base) {
|
|
|
407
418
|
|
|
408
419
|
// What an extension of crossbind's build once hooked into, which no build runs.
|
|
409
420
|
const OLD_HOOKS = ['loadConfig', 'buildWasm', 'createLib'];
|
|
410
|
-
const LISTED_KEYS = ['name', 'contract', 'root', 'app', 'options', 'fromApp', 'publisher', 'installer', 'commands', 'replaces'];
|
|
421
|
+
const LISTED_KEYS = ['name', 'contract', 'root', 'app', 'options', 'fromApp', 'publisher', 'installer', 'commands', 'packageSource', 'replaces'];
|
|
411
422
|
|
|
412
423
|
// An extension crossbind.config.js lists, as its package's crossbind.extension.js gives it: its name, the folder its
|
|
413
424
|
// files are in, which a build sees under the project's base alone, and what it adds to an app.
|
|
@@ -433,10 +444,27 @@ export function listedExtension(extension, where, base) {
|
|
|
433
444
|
|
|
434
445
|
const OWN_PACKAGE = /^@crossbind\/extension-(.+)$/;
|
|
435
446
|
|
|
436
|
-
// The names of the extension packages among the host's
|
|
447
|
+
// The names of the extension packages among a package's dependencies: the host's, or crossbind's own.
|
|
437
448
|
export const ownPackages = (appHostRoot) => Object.keys(JSON.parse(fs.readFileSync(path.join(appHostRoot, 'package.json'), 'utf8')).dependencies ?? {})
|
|
438
449
|
.filter((name) => OWN_PACKAGE.test(name)).sort();
|
|
439
450
|
|
|
451
|
+
// One of those packages, as its crossbind.extension.js gives it, named as its package. `fix` is what brings it in step
|
|
452
|
+
// with crossbind.
|
|
453
|
+
export function broughtExtension(load, name, fix) {
|
|
454
|
+
const where = `${name}'s crossbind.extension.js`;
|
|
455
|
+
let extension;
|
|
456
|
+
try {
|
|
457
|
+
extension = load(`${name}/crossbind.extension.js`).default();
|
|
458
|
+
} catch (error) {
|
|
459
|
+
throw new Error(`crossbind: cannot read ${where}: ${error.message}: install the project's packages, or ${fix}.`, { cause: error });
|
|
460
|
+
}
|
|
461
|
+
checkedContract(extension ?? {}, where, fix);
|
|
462
|
+
const checked = checkedExtension(extension, extension?.root, where);
|
|
463
|
+
const expected = OWN_PACKAGE.exec(name)[1];
|
|
464
|
+
if (checked.name !== expected) throw new Error(`crossbind: ${where} names the extension ${checked.name}, not ${expected} as its package.`);
|
|
465
|
+
return checked;
|
|
466
|
+
}
|
|
467
|
+
|
|
440
468
|
// crossbind's own extensions: the extension packages among the host's dependencies, which an app has by having the host,
|
|
441
469
|
// each named as its package. crossbind finds them as it finds the embind runtime, through the host, as pnpm shows them to
|
|
442
470
|
// it alone.
|
|
@@ -444,20 +472,7 @@ export function ownExtensions(appHostRoot) {
|
|
|
444
472
|
const load = createRequire(path.join(appHostRoot, 'package.json'));
|
|
445
473
|
const packages = ownPackages(appHostRoot);
|
|
446
474
|
if (packages.length === 0) throw new Error(`crossbind: the app host at ${appHostRoot} brings no extensions: update @crossbind/core-app-host to match crossbind.`);
|
|
447
|
-
return packages.map((name) =>
|
|
448
|
-
const where = `${name}'s crossbind.extension.js`;
|
|
449
|
-
let extension;
|
|
450
|
-
try {
|
|
451
|
-
extension = load(`${name}/crossbind.extension.js`).default();
|
|
452
|
-
} catch (error) {
|
|
453
|
-
throw new Error(`crossbind: cannot read ${where}: ${error.message}: install the project's packages, or update @crossbind/core-app-host to match crossbind.`, { cause: error });
|
|
454
|
-
}
|
|
455
|
-
checkedContract(extension ?? {}, where, 'update @crossbind/core-app-host to match crossbind');
|
|
456
|
-
const checked = checkedExtension(extension, extension?.root, where);
|
|
457
|
-
const expected = OWN_PACKAGE.exec(name)[1];
|
|
458
|
-
if (checked.name !== expected) throw new Error(`crossbind: ${where} names the extension ${checked.name}, not ${expected} as its package.`);
|
|
459
|
-
return checked;
|
|
460
|
-
});
|
|
475
|
+
return packages.map((name) => broughtExtension(load, name, 'update @crossbind/core-app-host to match crossbind'));
|
|
461
476
|
}
|
|
462
477
|
|
|
463
478
|
// Whether an option of the app's config asks for anything, as false, null and an empty list or object do not.
|
|
@@ -482,9 +497,10 @@ export function withoutNamed(own, app = {}) {
|
|
|
482
497
|
return own.filter((extension) => !left.includes(extension));
|
|
483
498
|
}
|
|
484
499
|
|
|
485
|
-
// A failure of an extension's step: crossbind's own message as it is, else one that names the extension and the step
|
|
486
|
-
|
|
487
|
-
|
|
500
|
+
// A failure of an extension's step: crossbind's own message as it is, else one that names the extension and the step,
|
|
501
|
+
// without the control characters a file or a tool's output it quotes may hold.
|
|
502
|
+
export const stepFailure = (name, which, error) => (error instanceof Error && error.message.startsWith('crossbind:') ? error
|
|
503
|
+
: new Error(`crossbind: ${name}'s ${which} failed: ${printable(error instanceof Error ? error.message : String(error))}`, { cause: error }));
|
|
488
504
|
|
|
489
505
|
// A step of an extension's publisher or installer, whose failure stops the run.
|
|
490
506
|
export async function extensionStep(name, which, run) {
|
|
@@ -515,10 +531,10 @@ export const withFromApp = (extensions, apiOf) => extensions.flatMap((extension)
|
|
|
515
531
|
return options === null ? [] : [{ ...extension, options }];
|
|
516
532
|
});
|
|
517
533
|
|
|
518
|
-
// The extensions crossbind.config.js lists that add to an app: all but those that give a publisher
|
|
519
|
-
// alone, which leave the host's own of their name as it is.
|
|
534
|
+
// The extensions crossbind.config.js lists that add to an app: all but those that give a publisher, an installer or a
|
|
535
|
+
// package source alone, which leave the host's own of their name as it is.
|
|
520
536
|
export const appExtensionsOf = (listed) => listed
|
|
521
|
-
.filter((extension) => (!extension.publisher && !extension.installer) || Object.keys(extension.app).length > 0);
|
|
537
|
+
.filter((extension) => (!extension.publisher && !extension.installer && !extension.packageSource) || Object.keys(extension.app).length > 0);
|
|
522
538
|
|
|
523
539
|
// The host's own extensions an app has, then those crossbind.config.js lists, each with the roles it holds, its own name
|
|
524
540
|
// where it says nothing else: a role a listed one replaces leaves the host's that held it, which the app leaves out once
|
package/src/utils/appOptions.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// its page opens files.
|
|
3
3
|
import { carriedPath } from './appFiles.js';
|
|
4
4
|
|
|
5
|
-
const WINDOW_DEFAULTS = { width: 1024, height: 768, minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, resizable: true, fullscreen: false, backgroundColor: null };
|
|
5
|
+
const WINDOW_DEFAULTS = { width: 1024, height: 768, minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, resizable: true, fullscreen: false, backgroundColor: null, titleBar: 'visible' };
|
|
6
|
+
const TITLE_BARS = ['visible', 'hidden'];
|
|
6
7
|
// Larger than any display, and far within the int a host takes a size as.
|
|
7
8
|
const MAX_WINDOW_SIZE = 16384;
|
|
8
9
|
// Windows keeps each of a file version's four numbers in 16 bits.
|
|
@@ -82,8 +83,9 @@ export function appCategory(config) {
|
|
|
82
83
|
return { darwin: `public.app-category.${category}`, linux: APP_CATEGORIES[category] };
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// The desktop window: the size of its page, the least it shrinks to and the most it grows to, whether it resizes
|
|
86
|
-
// whether it opens full screen. Its backgroundColor, which a phone
|
|
86
|
+
// The desktop window: the size of its page, the least it shrinks to and the most it grows to, whether it resizes,
|
|
87
|
+
// whether it opens full screen and whether a macOS page lies under its title bar. Its backgroundColor, which a phone
|
|
88
|
+
// takes as well, is appBackgroundColor's.
|
|
87
89
|
export function appWindow(config) {
|
|
88
90
|
const window = config.app.window ?? {};
|
|
89
91
|
const keys = Object.keys(WINDOW_DEFAULTS).join(', ');
|
|
@@ -102,6 +104,9 @@ export function appWindow(config) {
|
|
|
102
104
|
['resizable', 'fullscreen'].forEach((key) => {
|
|
103
105
|
if (typeof options[key] !== 'boolean') throw new Error(`crossbind: app.window.${key} ${JSON.stringify(options[key])} is no boolean: set it to true or false.`);
|
|
104
106
|
});
|
|
107
|
+
if (!TITLE_BARS.includes(options.titleBar)) {
|
|
108
|
+
throw new Error(`crossbind: app.window.titleBar ${JSON.stringify(options.titleBar)} is neither 'visible' nor 'hidden': 'hidden' lays a macOS app's page under its title bar, whose buttons stay.`);
|
|
109
|
+
}
|
|
105
110
|
if (!options.resizable && (options.minWidth || options.minHeight || options.maxWidth || options.maxHeight)) {
|
|
106
111
|
throw new Error('crossbind: app.window.minWidth, minHeight, maxWidth and maxHeight limit a window that resizes: drop them, or set app.window.resizable to true.');
|
|
107
112
|
}
|
|
@@ -117,16 +122,29 @@ export function appWindow(config) {
|
|
|
117
122
|
return options;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const COLOR = /^#[0-9A-Fa-f]{6}$/;
|
|
126
|
+
|
|
127
|
+
// The colors an app shows until its page paints, and behind a page without a background of its own, on iOS and Android as
|
|
128
|
+
// well: app.window.backgroundColor, one color, or { light, dark }, the one while the system is light and the one while
|
|
129
|
+
// it is dark, which a phone follows; else null, which leaves the system's.
|
|
130
|
+
function backgroundColors(config) {
|
|
123
131
|
const color = config.app.window?.backgroundColor ?? null;
|
|
124
|
-
if (color
|
|
125
|
-
|
|
132
|
+
if (color === null) return null;
|
|
133
|
+
if (typeof color === 'string' && COLOR.test(color)) return { light: color, dark: null };
|
|
134
|
+
const isPair = typeof color === 'object' && !Array.isArray(color) && Object.keys(color).sort().join() === 'dark,light'
|
|
135
|
+
&& [color.light, color.dark].every((one) => typeof one === 'string' && COLOR.test(one));
|
|
136
|
+
if (!isPair) {
|
|
137
|
+
throw new Error(`crossbind: app.window.backgroundColor ${JSON.stringify(color)} is no color of the form #rrggbb, as #1e1e1e is, nor { light, dark } of two, as { light: '#f2f2f2', dark: '#1e1e1e' } is.`);
|
|
126
138
|
}
|
|
127
139
|
return color;
|
|
128
140
|
}
|
|
129
141
|
|
|
142
|
+
// The color, or the light one of the two, which a desktop shows whatever its system's appearance.
|
|
143
|
+
export const appBackgroundColor = (config) => backgroundColors(config)?.light ?? null;
|
|
144
|
+
|
|
145
|
+
// The color an iOS or Android app shows while its system is dark, where app.window.backgroundColor gives one.
|
|
146
|
+
export const appDarkBackgroundColor = (config) => backgroundColors(config)?.dark ?? null;
|
|
147
|
+
|
|
130
148
|
const PERMISSIONS = ['camera', 'microphone', 'location'];
|
|
131
149
|
|
|
132
150
|
// The devices the page may use, each with the reason for it, which macOS and iOS read from the Info.plist:
|
|
@@ -313,8 +331,8 @@ export function appInstallers(config) {
|
|
|
313
331
|
return installers;
|
|
314
332
|
}
|
|
315
333
|
|
|
316
|
-
// A desktop host's window: its page's size and limits, whether it resizes
|
|
317
|
-
// one side alone leaves the other as large as any display.
|
|
334
|
+
// A desktop host's window: its page's size and limits, whether it resizes, whether it opens full screen and whether its
|
|
335
|
+
// page lies under its title bar. A maximum of one side alone leaves the other as large as any display.
|
|
318
336
|
function windowDefines(window) {
|
|
319
337
|
const most = window.maxWidth || window.maxHeight ? [
|
|
320
338
|
`#define CROSSBIND_APP_WINDOW_MAX_WIDTH ${window.maxWidth || MAX_WINDOW_SIZE}`,
|
|
@@ -328,6 +346,7 @@ function windowDefines(window) {
|
|
|
328
346
|
`#define CROSSBIND_APP_WINDOW_RESIZABLE ${window.resizable ? 1 : 0}`,
|
|
329
347
|
...most,
|
|
330
348
|
...(window.fullscreen ? ['#define CROSSBIND_APP_WINDOW_FULLSCREEN 1'] : []),
|
|
349
|
+
...(window.titleBar === 'hidden' ? ['#define CROSSBIND_APP_WINDOW_TITLE_BAR_HIDDEN 1'] : []),
|
|
331
350
|
];
|
|
332
351
|
}
|
|
333
352
|
|
|
@@ -339,10 +358,12 @@ export function appHeader(config, platform, appGroup = null) {
|
|
|
339
358
|
const id = `#define CROSSBIND_APP_ID "${platform === 'android' ? androidAppId(config) : appId(config)}"`;
|
|
340
359
|
const name = `#define CROSSBIND_APP_NAME "${appDisplayName(config)}"`;
|
|
341
360
|
const color = appBackgroundColor(config);
|
|
361
|
+
const dark = platform === 'ios' ? appDarkBackgroundColor(config) : null;
|
|
342
362
|
const permissions = appPermissions(config);
|
|
343
363
|
// An Android app's activity takes the color and the permissions from its Gradle project.
|
|
344
364
|
const page = platform === 'android' ? [] : [
|
|
345
365
|
...(color ? [`#define CROSSBIND_APP_BACKGROUND_COLOR 0x${color.slice(1).toLowerCase()}`] : []),
|
|
366
|
+
...(dark ? [`#define CROSSBIND_APP_BACKGROUND_COLOR_DARK 0x${dark.slice(1).toLowerCase()}`] : []),
|
|
346
367
|
...PERMISSIONS.map((name) => `#define CROSSBIND_APP_PERMISSION_${name.toUpperCase()} ${name in permissions ? 1 : 0}`),
|
|
347
368
|
];
|
|
348
369
|
const windows = platform === 'ios' && iosMultipleWindows(config) ? ['#define CROSSBIND_APP_IOS_MULTIPLE_WINDOWS 1'] : [];
|
|
@@ -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
|
+
}
|
|
@@ -23,9 +23,9 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
|
|
|
23
23
|
.map((e) => e.name);
|
|
24
24
|
|
|
25
25
|
// Prune bridges whose source of truth is gone. An app-local surface whose .rs was deleted
|
|
26
|
-
// breaks cargo on the missing #[path] target; a crate_ bridge whose
|
|
26
|
+
// breaks cargo on the missing #[path] target; a crate_ bridge whose packages.cargo entry
|
|
27
27
|
// was removed silently keeps dead registrations in every later binary. crate_ pruning only
|
|
28
|
-
// runs when the caller passes the
|
|
28
|
+
// runs when the caller passes the crates declared now - null means "don't judge".
|
|
29
29
|
crates = crates.filter((name) => {
|
|
30
30
|
const dir = `${root}/${name}`;
|
|
31
31
|
const libRs = `${dir}/src/lib.rs`;
|
package/src/utils/bridgeTools.js
CHANGED
|
@@ -15,8 +15,13 @@ const require = createRequire(import.meta.url);
|
|
|
15
15
|
|
|
16
16
|
export const usesBridgeTools = (target, host = process.platform) => host === 'darwin' && target?.platform !== 'android';
|
|
17
17
|
|
|
18
|
-
export function missingToolsMessage(error, version = require('../../package.json').version) {
|
|
18
|
+
export function missingToolsMessage(error, version = require('../../package.json').version, node = process.versions.node) {
|
|
19
19
|
if (error?.code === 'MODULE_NOT_FOUND') {
|
|
20
|
+
// A package manager leaves out an optional dependency whose engines the Node it runs on does not meet.
|
|
21
|
+
const needed = Number(require('../../package.json').engines.node.match(/\d+/)[0]);
|
|
22
|
+
if (Number(node.split('.')[0]) < needed) {
|
|
23
|
+
return `crossbind: bridges on a Mac need ${PACKAGE}, which needs Node ${needed} or later, as crossbind does: under Node ${node} the package manager leaves it out. Install again with Node ${needed} or later.`;
|
|
24
|
+
}
|
|
20
25
|
return `crossbind: bridges on a Mac need ${PACKAGE}, which installs with crossbind there unless optional dependencies are left out: npm install --save-dev ${PACKAGE}@${version}`;
|
|
21
26
|
}
|
|
22
27
|
if (error?.code === 'ENOENT') {
|
package/src/utils/cppDts.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { stripComments } from './swigInterface.js';
|
|
4
4
|
import writeIfChanged from './writeIfChanged.js';
|
|
5
|
-
import {
|
|
5
|
+
import { sourceImportOfHeader, sourceStageDir } from './packageSourceImport.js';
|
|
6
6
|
|
|
7
7
|
// Best-effort .d.ts for `.h` imports, the C++ analog of the Rust emitDts: parse the
|
|
8
8
|
// binding-rules surface (classes, public methods, primitives/string/shared_ptr), skip
|
|
@@ -256,20 +256,20 @@ function declarationsOf(headerFile, exportNames, dtsMode, log) {
|
|
|
256
256
|
|
|
257
257
|
// Mirror the declaration under <cache>/types/<project-relative>.d.ts - never next to the
|
|
258
258
|
// user's header. Package headers are skipped: their types ship with the package itself, and a
|
|
259
|
-
// header staged from
|
|
260
|
-
export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, dtsMode = 'sync', log = () => {} }) {
|
|
259
|
+
// header staged from a package source is typed by its module instead, as conan:zlib/zlib.h is.
|
|
260
|
+
export function writeHeaderDts({ headerFile, exportsFile, projectPath, cacheDir, sources = [], dtsMode = 'sync', log = () => {} }) {
|
|
261
261
|
const relative = path.relative(projectPath, headerFile);
|
|
262
|
-
if (relative.startsWith('..') || headerFile.startsWith(`${
|
|
262
|
+
if (relative.startsWith('..') || sources.some((source) => headerFile.startsWith(`${sourceStageDir(cacheDir, source)}/`))) return;
|
|
263
263
|
const exportNames = readExportNames(exportsFile, log);
|
|
264
264
|
if (!exportNames) return;
|
|
265
265
|
writeIfChanged(`${cacheDir}/types/${relative}.d.ts`, declarationsOf(headerFile, exportNames, dtsMode, log));
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
// A conan:
|
|
269
|
-
// @crossbind/typescript-config includes. `declare` is an error inside one (TS1038).
|
|
270
|
-
export function
|
|
271
|
-
const
|
|
272
|
-
const declared =
|
|
268
|
+
// A specifier such as conan:zlib/zlib.h names no file the editor could map, so it is typed by an ambient
|
|
269
|
+
// module, which @crossbind/typescript-config includes for conan. `declare` is an error inside one (TS1038).
|
|
270
|
+
export function writeSourceImportDts({ headerFile, exportsFile, declarationsFile = exportsFile, cacheDir, sources = [], dtsMode = 'sync', log = () => {} }) {
|
|
271
|
+
const sourceImport = sourceImportOfHeader(headerFile, cacheDir, sources);
|
|
272
|
+
const declared = sourceImport && readExportNames(declarationsFile, log);
|
|
273
273
|
if (!declared) return;
|
|
274
274
|
// The bridge also binds macros and variadic functions the app imports, which the whole header's SWIG run skips.
|
|
275
275
|
const exportNames = [...new Set([...declared, ...(readExportNames(exportsFile, log) ?? [])])];
|
|
@@ -279,7 +279,7 @@ export function writeConanImportDts({ headerFile, exportsFile, declarationsFile
|
|
|
279
279
|
.map((line) => (line ? ` ${line}` : line))
|
|
280
280
|
.join('\n');
|
|
281
281
|
writeIfChanged(
|
|
282
|
-
`${
|
|
283
|
-
`declare module '
|
|
282
|
+
`${sourceStageDir(cacheDir, sourceImport.source)}/types/${sourceImport.name}/${sourceImport.header}.d.ts`,
|
|
283
|
+
`declare module '${sourceImport.source}:${sourceImport.name}/${sourceImport.header}' {\n${body}\n}\n`,
|
|
284
284
|
);
|
|
285
285
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What a publisher, an installer and a command of an extension's give, as crossbind takes them.
|
|
1
|
+
// What a publisher, an installer, a package source and a command of an extension's give, as crossbind takes them.
|
|
2
2
|
|
|
3
3
|
// A name prefixes the extension's functions and names its C++ entry, crossbind_extension_<name>.
|
|
4
4
|
export const NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
@@ -110,6 +110,43 @@ export function checkedInstaller(installer, where) {
|
|
|
110
110
|
return { platforms, ...(installer.isLast ? { isLast: true } : {}), ...Object.fromEntries(steps) };
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
const PACKAGE_SOURCE_STEPS = ['check', 'inputs', 'install'];
|
|
114
|
+
const PACKAGE_SOURCE_KEYS = ['storeUnits', ...PACKAGE_SOURCE_STEPS];
|
|
115
|
+
// A package source's name starts its imports, its key in crossbind.config.js and its folders beside crossbind's own:
|
|
116
|
+
// one word, and none that means something there already: an import scheme of a bundler or a runtime, a folder of
|
|
117
|
+
// crossbind's, or a device's name, which Windows gives no folder.
|
|
118
|
+
const SOURCE_NAME = /^[a-z][a-z0-9]*$/;
|
|
119
|
+
const KEPT_SOURCE_NAMES = [
|
|
120
|
+
'cargo', 'node', 'file', 'data', 'http', 'https', 'npm', 'virtual', 'blob', 'bun', 'deno', 'jsr', 'cloudflare', 'workerd',
|
|
121
|
+
'all', 'android', 'build', 'config', 'deps', 'hermes', 'keys', 'secrets', 'sysroots', 'types',
|
|
122
|
+
'con', 'prn', 'aux', 'nul',
|
|
123
|
+
];
|
|
124
|
+
const STORE_SEGMENT = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/;
|
|
125
|
+
// Folders of a source's store that never change once written, as p/*/p names one in each folder of p.
|
|
126
|
+
const isStoreUnit = (unit) => typeof unit === 'string'
|
|
127
|
+
&& unit.split('/').every((segment) => segment === '*' || STORE_SEGMENT.test(segment))
|
|
128
|
+
&& unit.split('/').includes('*');
|
|
129
|
+
|
|
130
|
+
// A package source as crossbind runs it, under its extension's name: the folders of its store a remote runner sends
|
|
131
|
+
// back, and its steps, which keep the object as their this.
|
|
132
|
+
export function checkedPackageSource(packageSource, where, name) {
|
|
133
|
+
if (!isObject(packageSource)) throw new Error(`crossbind: ${where} is no object.`);
|
|
134
|
+
const unknown = Object.keys(packageSource).find((key) => !PACKAGE_SOURCE_KEYS.includes(key));
|
|
135
|
+
if (unknown) throw new Error(`crossbind: ${where} takes ${PACKAGE_SOURCE_KEYS.join(', ')}; ${unknown} is none of them.`);
|
|
136
|
+
if (!SOURCE_NAME.test(name) || KEPT_SOURCE_NAMES.includes(name)) {
|
|
137
|
+
throw new Error(`crossbind: ${where} makes ${name} the name of a package source, which starts its imports, as ${name}:<package>/<header>: one word of lowercase letters and digits, and none of ${KEPT_SOURCE_NAMES.join(', ')}.`);
|
|
138
|
+
}
|
|
139
|
+
const storeUnits = packageSource.storeUnits ?? [];
|
|
140
|
+
if (!Array.isArray(storeUnits) || !storeUnits.every(isStoreUnit)) {
|
|
141
|
+
throw new Error(`crossbind: ${where}.storeUnits is no list of the folders of its store that never change once written, each a path with a * for a folder's name, as 'p/*/p'.`);
|
|
142
|
+
}
|
|
143
|
+
assertSteps(where, packageSource, PACKAGE_SOURCE_STEPS);
|
|
144
|
+
if (!packageSource.check) throw new Error(`crossbind: ${where} has no check, the step that takes the project's packages.${name}.`);
|
|
145
|
+
if (!packageSource.install) throw new Error(`crossbind: ${where} has no install, the step that installs a target's packages.`);
|
|
146
|
+
const steps = PACKAGE_SOURCE_STEPS.filter((step) => packageSource[step]).map((step) => [step, packageSource[step].bind(packageSource)]);
|
|
147
|
+
return { storeUnits: [...storeUnits], ...Object.fromEntries(steps) };
|
|
148
|
+
}
|
|
149
|
+
|
|
113
150
|
const COMMAND_KEYS = ['help', 'options', 'run'];
|
|
114
151
|
|
|
115
152
|
// The commands an extension adds under crossbind app, by their names: each its line of help, the options it takes, as a
|
|
@@ -11,9 +11,16 @@ export function findImportSpecifiers(text) {
|
|
|
11
11
|
|
|
12
12
|
export const nativeExtensions = ({ header, module }) => [...header, ...module, 'rs'];
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Whether a specifier opens with one of the schemes an import names a store by, as cargo: and conan: are.
|
|
15
|
+
export const schemeTest = (schemes) => {
|
|
16
|
+
const scheme = new RegExp(`^(${schemes.join('|')}):`);
|
|
17
|
+
return (specifier) => scheme.test(specifier);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const nativeSpecifierTest = (ext, schemes) => {
|
|
15
21
|
const extension = new RegExp(`\\.(${nativeExtensions(ext).join('|')})$`);
|
|
16
|
-
|
|
22
|
+
const hasScheme = schemeTest(schemes);
|
|
23
|
+
return (specifier) => hasScheme(specifier) || extension.test(specifier);
|
|
17
24
|
};
|
|
18
25
|
|
|
19
26
|
export function findNativeImportsIn(projectDir, isNative) {
|
|
@@ -6,6 +6,7 @@ import loadJson from './loadJson.js';
|
|
|
6
6
|
import { computeInputStamp } from './inputStamp.js';
|
|
7
7
|
import { findHeaderImportsIn } from './headerImports.js';
|
|
8
8
|
import { findNativeImportsIn, nativeSpecifierTest } from './nativeImports.js';
|
|
9
|
+
import { importSchemesOf } from './packageSources.js';
|
|
9
10
|
|
|
10
11
|
// Set on the build a dev start runs: that build is a Node process too, which NODE_OPTIONS may hand the dev hooks again.
|
|
11
12
|
export const DEV_BUILD_ENV = 'CROSSBIND_DEV_BUILD';
|
|
@@ -49,7 +50,7 @@ export function devFormatOf(project) {
|
|
|
49
50
|
export function nativeInputsFingerprint(config) {
|
|
50
51
|
const { project, native, header, module: modules } = config.paths;
|
|
51
52
|
const relativeTo = (importer) => upath.relative(project, importer);
|
|
52
|
-
const imports = findNativeImportsIn(project, nativeSpecifierTest(config.ext))
|
|
53
|
+
const imports = findNativeImportsIn(project, nativeSpecifierTest(config.ext, importSchemesOf(config)))
|
|
53
54
|
.map(({ importer, specifier }) => ({ importer: relativeTo(importer), specifier }));
|
|
54
55
|
const names = findHeaderImportsIn(project, config.ext.header)
|
|
55
56
|
.map(({ importer, specifier, names: taken }) => JSON.stringify([relativeTo(importer), specifier, taken]));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import writeIfChanged from './writeIfChanged.js';
|
|
4
|
+
import logger from './logger.js';
|
|
5
|
+
import runPackageSource, { runProbe } from './runPackageSource.js';
|
|
6
|
+
import { printable } from './printableText.js';
|
|
7
|
+
|
|
8
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
// Enough of a failed build's log to reach the error the tool ends it with.
|
|
10
|
+
const LOG_TAIL_LINES = 60;
|
|
11
|
+
|
|
12
|
+
const isText = (value) => typeof value === 'string' && !value.includes('\0');
|
|
13
|
+
|
|
14
|
+
// A file the tool left in the work directory, as text, or null where there is none. Recipes write there too, so
|
|
15
|
+
// a link, which could name any file the machine's user reads, is refused, as is a path that leaves the directory.
|
|
16
|
+
function readOwn(name, dir, segments) {
|
|
17
|
+
const root = fs.realpathSync(dir);
|
|
18
|
+
const relative = segments.every(isText) ? path.relative(root, path.join(root, ...segments)) : '';
|
|
19
|
+
const file = path.join(root, relative);
|
|
20
|
+
const isInside = relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
21
|
+
if (isInside && !fs.existsSync(file)) return null;
|
|
22
|
+
if (!isInside || fs.realpathSync(file) !== file || !fs.statSync(file).isFile()) {
|
|
23
|
+
throw new Error(`crossbind: ${name} reads ${printable(segments.join('/'))} from its work directory, which is no file of its own there: a link, a folder, or a path that leaves the directory.`);
|
|
24
|
+
}
|
|
25
|
+
return fs.readFileSync(file, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assertRun(name, command, env) {
|
|
29
|
+
if (!Array.isArray(command) || command.length === 0 || !command.every(isText)) {
|
|
30
|
+
throw new Error(`crossbind: ${name}'s run takes a program and its arguments as a list of strings.`);
|
|
31
|
+
}
|
|
32
|
+
const isEnv = env !== null && typeof env === 'object' && !Array.isArray(env)
|
|
33
|
+
&& Object.entries(env).every(([key, value]) => ENV_NAME.test(key) && isText(value));
|
|
34
|
+
if (!isEnv) throw new Error(`crossbind: ${name}'s run takes env as variable names with their text.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runFailure(name, step, target, program, result, logFile) {
|
|
38
|
+
if (result.error && result.error.code !== 'ENOBUFS') {
|
|
39
|
+
return new Error(`crossbind: could not run ${program} for ${target.path} (${result.error.message}).`);
|
|
40
|
+
}
|
|
41
|
+
let ending = `failed (exit code ${result.status})`;
|
|
42
|
+
if (result.error) ending = 'was stopped: it printed more than crossbind reads';
|
|
43
|
+
else if (result.signal) ending = `was stopped by ${result.signal}`;
|
|
44
|
+
// A tool that says why on stdout alone would end the message with nothing.
|
|
45
|
+
const printed = `${result.stderr ?? ''}`.trim() ? result.stderr : result.stdout;
|
|
46
|
+
const tail = printable(`${printed ?? ''}`).trim().split('\n').slice(-LOG_TAIL_LINES).join('\n');
|
|
47
|
+
return new Error(`crossbind: ${name} ${step} for ${target.path} ${ending}.${logFile ? ` Its whole log is in ${logFile}.` : ''}\n${tail}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// What a package source's steps get of crossbind. In install, with a log file, `run` runs the source's tool
|
|
51
|
+
// where the target's packages build (its image, a remote runner, or the host where isLocal says so), in the
|
|
52
|
+
// run's work directory, and what the tool writes to stderr is the install's log. In inputs the work directory
|
|
53
|
+
// is the source's probe directory and only a tool of the host can be asked.
|
|
54
|
+
export default function packageSourceApi({
|
|
55
|
+
source, config, target, work, logFile = null,
|
|
56
|
+
}) {
|
|
57
|
+
const { name } = source;
|
|
58
|
+
const isLocal = work.runner === 'LOCAL';
|
|
59
|
+
const step = logFile ? 'install' : 'inputs';
|
|
60
|
+
const log = [];
|
|
61
|
+
|
|
62
|
+
function run(command, { env = {} } = {}) {
|
|
63
|
+
assertRun(name, command, env);
|
|
64
|
+
if (!logFile && !isLocal) {
|
|
65
|
+
throw new Error(`crossbind: ${name}'s inputs runs ${command[0]}, which it can where api.isLocal is true alone: elsewhere the image says what runs.`);
|
|
66
|
+
}
|
|
67
|
+
const result = logFile ? runPackageSource(command, {
|
|
68
|
+
source, config, target, work, env,
|
|
69
|
+
}) : runProbe(command, { work, env });
|
|
70
|
+
if (logFile) {
|
|
71
|
+
log.push(`${result.stderr ?? ''}`);
|
|
72
|
+
writeIfChanged(logFile, log.join(''));
|
|
73
|
+
}
|
|
74
|
+
if (result.error || result.status !== 0) throw runFailure(name, step, target, command[0], result, logFile);
|
|
75
|
+
return Object.freeze({ stdout: `${result.stdout ?? ''}`, stderr: `${result.stderr ?? ''}` });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
name,
|
|
80
|
+
isLocal,
|
|
81
|
+
logFile,
|
|
82
|
+
work: Object.freeze({ dir: work.dir, path: work.toolPath, read: (...segments) => readOwn(name, work.dir, segments) }),
|
|
83
|
+
store: Object.freeze({ dir: work.store, path: work.storePath }),
|
|
84
|
+
log: (text) => logger.info(text),
|
|
85
|
+
run,
|
|
86
|
+
});
|
|
87
|
+
}
|