crossbind 2.0.0-beta.68 → 2.0.0-beta.72
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/README.md +1 -1
- package/package.json +70 -59
- package/src/actions/addExtension.js +196 -0
- package/src/actions/androidGradle.js +26 -17
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/buildApp.js +177 -74
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +40 -29
- package/src/actions/createLib.js +3 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +5 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +207 -16
- package/src/actions/iosDevice.js +2 -2
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +113 -58
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/publish.js +220 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +60 -29
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +144 -132
- package/src/app/index.js +147 -185
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +37 -37
- package/src/index.js +4 -3
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/program.js +32 -3
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +8 -6
- package/src/state/loadConfig.js +26 -9
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +777 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +142 -112
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +71 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +172 -0
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/openUrl.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/plist.js +5 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/systemConfig.js +16 -3
- package/src/utils/targetToolchain.js +77 -0
- package/vitest.config.js +6 -1
- package/src/actions/appSetup.js +0 -116
- package/src/actions/appStoreConnect.js +0 -188
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
- package/src/actions/googlePlay.js +0 -213
- package/src/actions/googleSignIn.js +0 -78
- package/src/actions/playSetup.js +0 -19
- package/src/actions/publishAndroid.js +0 -102
- package/src/actions/publishIos.js +0 -146
- 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
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import upath from 'upath';
|
|
4
4
|
import state, { saveCache } from '../state/index.js';
|
|
5
|
-
import
|
|
5
|
+
import refreshPackageSources from '../state/refreshPackageSources.js';
|
|
6
6
|
import refreshBuiltDependencies from '../state/refreshBuiltDependencies.js';
|
|
7
|
-
import {
|
|
7
|
+
import { packageSourceInputsOf, sourceNamesOf } from '../utils/packageSources.js';
|
|
8
8
|
import { getContentHash, getFileHash } from '../utils/hash.js';
|
|
9
9
|
import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
|
|
10
10
|
import fixBridgeRuntime from '../utils/bridgeRuntimeFixes.js';
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
|
|
13
13
|
} from '../utils/bridgeExtras.js';
|
|
14
14
|
import resolveNativeImport from '../integration/resolveNativeImport.js';
|
|
15
|
-
import { writeHeaderDts,
|
|
15
|
+
import { writeHeaderDts, writeSourceImportDts } from '../utils/cppDts.js';
|
|
16
16
|
import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
|
|
17
17
|
import {
|
|
18
18
|
buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, findSwigInlineIncludes, findSwigPreamble,
|
|
@@ -21,9 +21,12 @@ import {
|
|
|
21
21
|
} from '../utils/swigInterface.js';
|
|
22
22
|
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
23
23
|
import { withDirLockSync } from '../utils/dirLock.js';
|
|
24
|
-
import {
|
|
24
|
+
import { sourceImportOfHeader } from '../utils/packageSourceImport.js';
|
|
25
25
|
import run, { cxxPreprocessorFor } from './run.js';
|
|
26
26
|
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
27
|
+
import {
|
|
28
|
+
loadBridgeTools, runPreprocessor, runSwig, usesBridgeTools,
|
|
29
|
+
} from '../utils/bridgeTools.js';
|
|
27
30
|
|
|
28
31
|
// Part of every interface hash, so interfaces cached by an older generator are rebuilt.
|
|
29
32
|
const INTERFACE_FORMAT = 'swig-macros-4';
|
|
@@ -50,10 +53,12 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, wholeHeaders = [] } = {}) {
|
|
53
|
-
// A header can include a
|
|
54
|
-
// dependency built since.
|
|
55
|
-
|
|
56
|
+
// A header can include the headers of a package source's package that a build staged after this process loaded,
|
|
57
|
+
// or belong to a dependency built since.
|
|
58
|
+
refreshPackageSources(state.config);
|
|
56
59
|
refreshBuiltDependencies(state.config);
|
|
60
|
+
// Without bridge-tools on a Mac the macro step would fail quietly, and its interface would be kept without macros.
|
|
61
|
+
if (usesBridgeTools(target)) loadBridgeTools();
|
|
57
62
|
const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
|
|
58
63
|
if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
|
|
59
64
|
fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
|
|
@@ -71,7 +76,7 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
|
|
|
71
76
|
const { headerPath } = includeLocation(interfaceFilePath, target);
|
|
72
77
|
const inlined = findSwigInlineIncludes(interfaceFilePath, [state.config, ...state.config.allDependencies], headerPath);
|
|
73
78
|
const swigView = inlined.length ? writeSwigView(interfaceFilePath, headerPath, inlined) : null;
|
|
74
|
-
const sourceHash = [getFileHash(interfaceFilePath), ...
|
|
79
|
+
const sourceHash = [getFileHash(interfaceFilePath), ...packageInputsHash()].join('\n');
|
|
75
80
|
// The names imported by name decide what the bridge adds to SWIG's output, so another one makes another bridge.
|
|
76
81
|
const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
|
|
77
82
|
const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
|
|
@@ -86,13 +91,14 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
|
|
|
86
91
|
exportsFile: `${bridgeFile}.exports.json`,
|
|
87
92
|
projectPath: state.config.paths.project,
|
|
88
93
|
cacheDir: state.config.paths.cache,
|
|
94
|
+
sources: sourceNamesOf(state.config),
|
|
89
95
|
dtsMode: state.config.dts,
|
|
90
96
|
};
|
|
91
97
|
writeHeaderDts(dtsOptions);
|
|
92
|
-
if (
|
|
93
|
-
dtsOptions.declarationsFile =
|
|
98
|
+
if (sourceImportOfHeader(interfaceFilePath, dtsOptions.cacheDir, dtsOptions.sources)) {
|
|
99
|
+
dtsOptions.declarationsFile = createSourceDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
|
|
94
100
|
}
|
|
95
|
-
|
|
101
|
+
writeSourceImportDts(dtsOptions);
|
|
96
102
|
}
|
|
97
103
|
if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
|
|
98
104
|
// A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
|
|
@@ -110,11 +116,11 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
|
|
|
110
116
|
return bridgeFile;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
|
-
// The
|
|
114
|
-
// staged after a header may already be bound, and restaged in place for another version.
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
return
|
|
119
|
+
// The packages of package sources a header's includes may reach, for the interface and bridge caches: they
|
|
120
|
+
// are staged after a header may already be bound, and restaged in place for another version.
|
|
121
|
+
function packageInputsHash() {
|
|
122
|
+
const inputs = packageSourceInputsOf(state.config);
|
|
123
|
+
return inputs ? [`packages:${getContentHash(JSON.stringify(inputs))}`] : [];
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
// A header binds the constants the app imports from it, and so the free functions of a dependency's header: the project's
|
|
@@ -154,17 +160,18 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
|
|
|
154
160
|
const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
|
|
155
161
|
const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
|
|
156
162
|
// A prelude or ignored-declaration change in the owning package, a completed class moving to another header, an
|
|
157
|
-
// interface the package ships beside the header changing or going away, another imported name,
|
|
158
|
-
// its includes may reach being staged or moving to another version
|
|
163
|
+
// interface the package ships beside the header changing or going away, another imported name, packages of a
|
|
164
|
+
// package source its includes may reach being staged or moving to another version, or the header included by
|
|
165
|
+
// another path, as another target finds it, must regenerate the interface as well.
|
|
159
166
|
const fileHash = [
|
|
160
|
-
INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
|
|
167
|
+
INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...(headerPath ? [`include:${headerPath}`] : []), ...prelude,
|
|
161
168
|
...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
|
|
162
169
|
...(preamble.length ? [`preamble:${preamble.join('\n')}`] : []),
|
|
163
170
|
...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
|
|
164
171
|
...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
|
|
165
172
|
...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
|
|
166
173
|
...(named.length ? [`named:${named.join(',')}`] : []),
|
|
167
|
-
...
|
|
174
|
+
...packageInputsHash(),
|
|
168
175
|
].join('\n');
|
|
169
176
|
const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
|
|
170
177
|
if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
|
|
@@ -353,21 +360,25 @@ const RELEASE_DEFINE = '-DNDEBUG';
|
|
|
353
360
|
|
|
354
361
|
// One bridge serves every desktop target, so SWIG and the macro table it reads come from the web image's em++,
|
|
355
362
|
// which defines no operating system; an android bridge is read with the NDK it compiles with. The headers
|
|
356
|
-
// still come from the target's own packages.
|
|
363
|
+
// still come from the target's own packages. On a Mac, bridge-tools makes the same bridge, in the build directory the
|
|
364
|
+
// image's tools run in.
|
|
357
365
|
const toolTargetFor = (target) => (target?.platform === 'android' ? target : { ...target, platform: 'wasm' });
|
|
358
366
|
|
|
359
367
|
function dumpMacros(outputFile, includes, includePath, target, defines = []) {
|
|
360
|
-
const
|
|
361
|
-
run(cxxPreprocessorFor(tool), [
|
|
368
|
+
const args = [
|
|
362
369
|
'-x', 'c++', '-std=c++17', '-dM', '-E', ...defines,
|
|
363
370
|
...includePath,
|
|
364
371
|
...includes.flatMap((header) => ['-include', header]),
|
|
365
372
|
'-o', outputFile,
|
|
366
373
|
'/dev/null',
|
|
367
|
-
]
|
|
374
|
+
];
|
|
375
|
+
if (usesBridgeTools(target)) runPreprocessor(args, state.config.paths.build);
|
|
376
|
+
else run(cxxPreprocessorFor(toolTargetFor(target)), args, null, toolTargetFor(target));
|
|
368
377
|
return parseMacroDump(fs.readFileSync(outputFile, 'utf8'));
|
|
369
378
|
}
|
|
370
379
|
|
|
380
|
+
const runSwigFor = (args, target) => (usesBridgeTools(target) ? runSwig(args, state.config.paths.build) : run('swig', args, null, toolTargetFor(target)));
|
|
381
|
+
|
|
371
382
|
function swigIncludePath(target, sourceDir) {
|
|
372
383
|
const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
|
|
373
384
|
const includePath = [
|
|
@@ -468,7 +479,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
|
|
|
468
479
|
const bridgeFilePath = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
|
|
469
480
|
fs.writeFileSync(keyOf(bridgeFilePath), '');
|
|
470
481
|
// #error lines guard branches SWIG evaluates without the compiler's predefined macros, so they only warn.
|
|
471
|
-
const swig = () =>
|
|
482
|
+
const swig = () => runSwigFor([
|
|
472
483
|
'-c++',
|
|
473
484
|
'-embind',
|
|
474
485
|
'-cpperraswarn',
|
|
@@ -476,7 +487,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
|
|
|
476
487
|
...(swigView ? [`-I${swigView.dir}`] : []),
|
|
477
488
|
...swigIncludePath(target, sourceDir),
|
|
478
489
|
interfaceFilePath,
|
|
479
|
-
],
|
|
490
|
+
], target);
|
|
480
491
|
try {
|
|
481
492
|
swig();
|
|
482
493
|
} catch (e) {
|
|
@@ -508,7 +519,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
|
|
|
508
519
|
}
|
|
509
520
|
|
|
510
521
|
// Generate a full export catalog for the editor, without compiling or linking these extra functions.
|
|
511
|
-
function
|
|
522
|
+
function createSourceDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
|
|
512
523
|
const dir = `${state.config.paths.build}/declarations`;
|
|
513
524
|
fs.mkdirSync(dir, { recursive: true });
|
|
514
525
|
const file = `${dir}/${upath.basename(interfaceFile)}`;
|
|
@@ -517,8 +528,8 @@ function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, s
|
|
|
517
528
|
const hash = getContentHash([BRIDGE_FORMAT, getFileHash(file), sourceHash, swigView?.hash ?? ''].join('\n'));
|
|
518
529
|
if (!fs.existsSync(`${bridge}.exports.json`) || !fs.existsSync(keyOf(bridge)) || fs.readFileSync(keyOf(bridge), 'utf8') !== hash) {
|
|
519
530
|
fs.writeFileSync(keyOf(bridge), '');
|
|
520
|
-
|
|
521
|
-
...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file],
|
|
531
|
+
runSwigFor(['-c++', '-embind', '-cpperraswarn', '-o', bridge,
|
|
532
|
+
...(swigView ? [`-I${swigView.dir}`] : []), ...swigIncludePath(target, sourceDir), file], target);
|
|
522
533
|
fs.writeFileSync(keyOf(bridge), hash);
|
|
523
534
|
}
|
|
524
535
|
return `${bridge}.exports.json`;
|
package/src/actions/createLib.js
CHANGED
|
@@ -6,7 +6,6 @@ import run from './run.js';
|
|
|
6
6
|
import buildCargo from './buildCargo.js';
|
|
7
7
|
import { cargoTripleFor } from '../utils/cargoTarget.js';
|
|
8
8
|
import getCmakeParameters from './getCmakeParameters.js';
|
|
9
|
-
import triggerExtensions from './extensions.js';
|
|
10
9
|
import state from '../state/index.js';
|
|
11
10
|
import logger from '../utils/logger.js';
|
|
12
11
|
import { WASI_EMULATION_LIBS, WASI_LINK_LIBS, wasiCFlags, wasiCxxFlags } from '../utils/wasiToolchain.js';
|
|
@@ -17,7 +16,7 @@ import {
|
|
|
17
16
|
import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
|
|
18
17
|
import { withDependencyBridges } from '../utils/dependencyBridges.js';
|
|
19
18
|
import { targetArchiveFlags } from '../utils/archiveFlags.js';
|
|
20
|
-
import {
|
|
19
|
+
import { packageSourceInputsOf } from '../utils/packageSources.js';
|
|
21
20
|
import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
|
|
22
21
|
import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
|
|
23
22
|
|
|
@@ -58,10 +57,10 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
58
57
|
const configEmccFlags = getData('binary', target)?.emccFlags || [];
|
|
59
58
|
const compileOptions = getData('cmake', target)?.compileOptions || [];
|
|
60
59
|
const flagsFingerprintFile = `${libdir}/crossbind-emccflags.fingerprint`;
|
|
61
|
-
const
|
|
60
|
+
const packageInputs = packageSourceInputsOf(state.config);
|
|
62
61
|
const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
|
|
63
62
|
const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
|
|
64
|
-
const keyInputs =
|
|
63
|
+
const keyInputs = packageInputs ? [flagsInputs, packageInputs] : flagsInputs;
|
|
65
64
|
// An app's wasi source compiles against <crossbind/http.h>, so a new one misses this cache too.
|
|
66
65
|
const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
|
|
67
66
|
? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
|
|
@@ -132,7 +131,6 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
132
131
|
|
|
133
132
|
const extraLibs = getExtraLibs ? getExtraLibs(target) : [];
|
|
134
133
|
|
|
135
|
-
triggerExtensions('createLib', 'setFlagWithBuildConfig', [buildEnv, cFlags, ldFlags]);
|
|
136
134
|
const archiveFlags = targetArchiveFlags(target);
|
|
137
135
|
cFlags.push(...archiveFlags);
|
|
138
136
|
ldFlags.push(...archiveFlags);
|
|
@@ -165,8 +163,6 @@ export default function createLib(target, fileType, options = {}) {
|
|
|
165
163
|
];
|
|
166
164
|
buildParams = getCmakeParameters(target, { ...options, nativeGlob, headerDirs });
|
|
167
165
|
|
|
168
|
-
triggerExtensions('createLib', 'setFlagWithoutBuildConfig', [buildEnv]);
|
|
169
|
-
|
|
170
166
|
const archiveFlags = targetArchiveFlags(target).join(' ');
|
|
171
167
|
if (archiveFlags) {
|
|
172
168
|
buildEnv.params.push('-e', `CFLAGS=${archiveFlags}`);
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import state from '../state/index.js';
|
|
5
|
+
import { exportOf } from '../state/loadConfig.js';
|
|
6
|
+
import loadSystemConfig, { extensionSettingsOf, updateSystemConfig } from '../utils/systemConfig.js';
|
|
7
|
+
import systemKeys from '../utils/systemKeys.js';
|
|
8
|
+
import { askLine, askYesNo, hasTerminal } from '../utils/ask.js';
|
|
9
|
+
import openUrl from '../utils/openUrl.js';
|
|
10
|
+
import { readSecret, storeSecret } from '../utils/keychain.js';
|
|
11
|
+
import { configChangesOf } from '../utils/configChanges.js';
|
|
12
|
+
import { entry, writeOption } from '../utils/configWriter.js';
|
|
13
|
+
import { readAppIcon, windowsIcon } from '../utils/appIcon.js';
|
|
14
|
+
import {
|
|
15
|
+
PRIVACY_CATEGORIES, androidAppId, appCategory, appDisplayName, appEncryption, appId, appPrivacy, appUrlSchemes,
|
|
16
|
+
} from '../utils/appOptions.js';
|
|
17
|
+
import { isJsonValue, linuxDepends } from '../utils/appExtensions.js';
|
|
18
|
+
import { fileTypeOwners } from '../utils/appLaunches.js';
|
|
19
|
+
import { unreasonedImports } from '../utils/requiredReasonApis.js';
|
|
20
|
+
import { resolveHermesDsym } from '../utils/hermes.js';
|
|
21
|
+
import { imageRoleFor } from '../utils/pullDockerImage.js';
|
|
22
|
+
import { isUnderBase } from '../utils/replaceBasePathForDocker.js';
|
|
23
|
+
import {
|
|
24
|
+
androidVersion, appBuildNumber, appTeam, appVersion,
|
|
25
|
+
} from './buildApp.js';
|
|
26
|
+
import runTool from './run.js';
|
|
27
|
+
import { makeUploadKey, releaseSigningEnv } from './uploadKey.js';
|
|
28
|
+
import { appStoreKey } from './appleSigning.js';
|
|
29
|
+
import { inWindowsImage, jsignScript, windowsSigning } from './windowsSigning.js';
|
|
30
|
+
import { xcodebuild, xcodeTeams } from './xcodebuild.js';
|
|
31
|
+
|
|
32
|
+
// The api an extension's build-side code gets in place of crossbind's own modules, which it never imports: what the run
|
|
33
|
+
// is, the project and its app as the build reads them, the terminal, this machine's settings and keychain, and the
|
|
34
|
+
// signing and tools crossbind builds and packages each platform's apps with.
|
|
35
|
+
|
|
36
|
+
const PATH_PART = /^[A-Za-z_$][\w$]*$/;
|
|
37
|
+
const SETTING = /^[A-Z][A-Z0-9_]*$/;
|
|
38
|
+
const ACCOUNT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
39
|
+
|
|
40
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
|
|
42
|
+
function refuse() {
|
|
43
|
+
throw new TypeError('crossbind: api.config is read-only: api.config.write writes an option into crossbind.config.js.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const views = new WeakMap();
|
|
47
|
+
const READ_ONLY = {
|
|
48
|
+
get(target, key, receiver) {
|
|
49
|
+
const value = Reflect.get(target, key, receiver);
|
|
50
|
+
const own = Reflect.getOwnPropertyDescriptor(target, key);
|
|
51
|
+
// A proxy gives a property that never changes as it is.
|
|
52
|
+
if (own && !own.configurable && !own.writable) return value;
|
|
53
|
+
return readOnly(value);
|
|
54
|
+
},
|
|
55
|
+
// A configurable property's value is a view too; one that never changes is given as it is, as a proxy must.
|
|
56
|
+
getOwnPropertyDescriptor(target, key) {
|
|
57
|
+
const own = Reflect.getOwnPropertyDescriptor(target, key);
|
|
58
|
+
return own?.configurable && 'value' in own ? { ...own, value: readOnly(own.value) } : own;
|
|
59
|
+
},
|
|
60
|
+
set: refuse,
|
|
61
|
+
defineProperty: refuse,
|
|
62
|
+
deleteProperty: refuse,
|
|
63
|
+
setPrototypeOf: refuse,
|
|
64
|
+
preventExtensions: refuse,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function readOnly(value) {
|
|
68
|
+
if (value === null || typeof value !== 'object') return value;
|
|
69
|
+
if (!views.has(value)) views.set(value, new Proxy(value, READ_ONLY));
|
|
70
|
+
return views.get(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// crossbind.config.js's export as the run's config read it, with the app's options the api wrote since, which the run's
|
|
74
|
+
// config holds: each read sees them, however long ago the extension took the view.
|
|
75
|
+
function configView(write) {
|
|
76
|
+
const current = () => ({ ...exportOf(state.config), app: state.config.app });
|
|
77
|
+
return new Proxy({}, {
|
|
78
|
+
get: (target, key) => (key === 'write' ? write : readOnly(current()[key])),
|
|
79
|
+
has: (target, key) => key in current(),
|
|
80
|
+
ownKeys: () => Reflect.ownKeys(current()),
|
|
81
|
+
getOwnPropertyDescriptor(target, key) {
|
|
82
|
+
const own = Reflect.getOwnPropertyDescriptor(current(), key);
|
|
83
|
+
return own && { ...own, value: readOnly(own.value) };
|
|
84
|
+
},
|
|
85
|
+
set: refuse,
|
|
86
|
+
defineProperty: refuse,
|
|
87
|
+
deleteProperty: refuse,
|
|
88
|
+
setPrototypeOf: refuse,
|
|
89
|
+
preventExtensions: refuse,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const deepFrozen = (value) => {
|
|
94
|
+
if (value !== null && typeof value === 'object') Object.values(value).forEach(deepFrozen);
|
|
95
|
+
return Object.freeze(value);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const valueAt = (object, keys) => keys.reduce((value, key) => (isObject(value) ? value[key] : undefined), object);
|
|
99
|
+
const withValue = (object, [key, ...rest], value) => ({ ...object, [key]: rest.length === 0 ? value : withValue(isObject(object?.[key]) ? object[key] : {}, rest, value) });
|
|
100
|
+
|
|
101
|
+
// What crossbind checks of an option written into crossbind.config.js, as its builds read it.
|
|
102
|
+
const OPTION_CHECKS = {
|
|
103
|
+
'app.id': (config, platform) => (platform === 'android' ? androidAppId(config) : appId(config)),
|
|
104
|
+
'app.name': appDisplayName,
|
|
105
|
+
'app.icon': readAppIcon,
|
|
106
|
+
'app.encryption': appEncryption,
|
|
107
|
+
'app.privacy': appPrivacy,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Writes an option into crossbind.config.js once the user says yes on a terminal, and into this run's config: one of
|
|
111
|
+
// those crossbind checks as the builds would, alone. One the config sets already is the user's to change, and one the
|
|
112
|
+
// builds would refuse fails as invalid, for the caller to ask again.
|
|
113
|
+
async function writeConfig(name, platform, key, value) {
|
|
114
|
+
const keys = typeof key === 'string' ? key.split('.') : [];
|
|
115
|
+
if (keys.length === 0 || !keys.every((part) => PATH_PART.test(part))) throw new Error(`crossbind: ${name} names ${JSON.stringify(key)}, which is no option of crossbind.config.js, as 'app.id' is.`);
|
|
116
|
+
if (!Object.hasOwn(OPTION_CHECKS, key)) {
|
|
117
|
+
const writable = Object.keys(OPTION_CHECKS);
|
|
118
|
+
throw new Error(`crossbind: ${name} writes ${key}, which api.config.write leaves to the app's developer: it writes ${writable.slice(0, -1).join(', ')} and ${writable.at(-1)} alone.`);
|
|
119
|
+
}
|
|
120
|
+
if (!isJsonValue(value)) throw new Error(`crossbind: ${name} gives ${key} a value that is no JSON value, which is all crossbind writes into crossbind.config.js.`);
|
|
121
|
+
// A copy of its own, as the value may hold the api's views of the config.
|
|
122
|
+
const plain = JSON.parse(JSON.stringify(value));
|
|
123
|
+
const text = `${keys.slice(0, -1).map((part) => `${part}.`).join('')}${entry(keys.at(-1), plain)}`;
|
|
124
|
+
if (valueAt(state.config, keys) !== undefined) throw new Error(`crossbind: set ${text} in crossbind.config.js in place of the ${key} there.`);
|
|
125
|
+
const changed = withValue(state.config, keys, plain);
|
|
126
|
+
try {
|
|
127
|
+
OPTION_CHECKS[key]?.(changed, platform);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw Object.assign(error, { isInvalid: true });
|
|
130
|
+
}
|
|
131
|
+
if (!hasTerminal()) throw new Error(`crossbind: ${name} would write ${text} into crossbind.config.js, which takes a yes on a terminal: set it there yourself.`);
|
|
132
|
+
if (!await askYesNo(`Write ${text} into crossbind.config.js? [Y/n] `)) throw new Error(`crossbind: set ${text} in crossbind.config.js.`);
|
|
133
|
+
if (!writeOption(state.config.paths.project, keys, plain)) {
|
|
134
|
+
throw new Error(`crossbind: crossbind.config.js is no config crossbind can write ${key} into safely: set ${text} in it yourself.`);
|
|
135
|
+
}
|
|
136
|
+
state.config[keys[0]] = changed[keys[0]];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// This machine's settings: crossbind's own by their names, as `crossbind config set` takes them, and by any other name
|
|
140
|
+
// the extension's own, which ~/.crossbind.json keeps apart for each extension.
|
|
141
|
+
function settingsOf(name) {
|
|
142
|
+
const isCrossbinds = (key) => Object.hasOwn(systemKeys, key);
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
get: (key) => (isCrossbinds(key) ? state.config.system[key] : extensionSettingsOf(state.config.system, name)[key]),
|
|
145
|
+
set(values) {
|
|
146
|
+
if (!isObject(values) || !Object.entries(values).every(([key, value]) => SETTING.test(key) && typeof value === 'string')) {
|
|
147
|
+
throw new Error(`crossbind: ${name} sets settings that are no object of this machine's settings, each an uppercase name with a string.`);
|
|
148
|
+
}
|
|
149
|
+
const entries = Object.entries(values);
|
|
150
|
+
entries.filter(([key]) => systemKeys[key]?.options).forEach(([key, value]) => {
|
|
151
|
+
if (!systemKeys[key].options.includes(value)) {
|
|
152
|
+
throw new Error(`crossbind: ${name} sets ${key} to ${JSON.stringify(value)}, which takes ${systemKeys[key].options.join(', ')} alone.`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const own = entries.filter(([key]) => !isCrossbinds(key));
|
|
156
|
+
updateSystemConfig(Object.fromEntries(entries.filter(([key]) => isCrossbinds(key))), own.length > 0 ? { name, values: Object.fromEntries(own) } : null);
|
|
157
|
+
state.config.system = loadSystemConfig();
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The keychain keeps an extension's secrets under its own service, each by a path the account names, as Windows keeps
|
|
163
|
+
// one in a file beside that path.
|
|
164
|
+
function secretsOf(name) {
|
|
165
|
+
const service = `crossbind ${name}`;
|
|
166
|
+
const keyOf = (account) => {
|
|
167
|
+
if (typeof account !== 'string' || !ACCOUNT.test(account)) throw new Error(`crossbind: ${name} names the secret ${JSON.stringify(account)}, which is no name of letters, digits, dots, dashes and underscores.`);
|
|
168
|
+
return path.join(os.homedir(), '.crossbind', 'secrets', name, account);
|
|
169
|
+
};
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
get: (account) => readSecret(keyOf(account), { service }),
|
|
172
|
+
set(account, secret) {
|
|
173
|
+
const key = keyOf(account);
|
|
174
|
+
if (typeof secret !== 'string' || !secret) throw new Error(`crossbind: ${name} keeps no secret of ${account}, which is no text.`);
|
|
175
|
+
fs.mkdirSync(path.dirname(key), { recursive: true, mode: 0o700 });
|
|
176
|
+
return storeSecret(key, secret, { service });
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function terminalOf(name) {
|
|
182
|
+
const assertTerminal = (question) => {
|
|
183
|
+
if (!hasTerminal()) throw new Error(`crossbind: ${name} asks ${JSON.stringify(question.trim())}, with no terminal to answer it.`);
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
ask(question) {
|
|
187
|
+
assertTerminal(question);
|
|
188
|
+
return askLine(question);
|
|
189
|
+
},
|
|
190
|
+
askYesNo(question) {
|
|
191
|
+
assertTerminal(question);
|
|
192
|
+
return askYesNo(question);
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Runs a tool to its end where crossbind packages an app of the target's platform: on this Mac, with Apple's tools on
|
|
198
|
+
// the PATH, for a macOS app, and for a Linux or Windows app in crossbind's image of its platform, which sees the
|
|
199
|
+
// project's base alone, the windows one with the key that signs the app for a tool that signs. An installer's api alone
|
|
200
|
+
// has the target of the app it makes.
|
|
201
|
+
function runOf(name, target) {
|
|
202
|
+
return (command, options) => {
|
|
203
|
+
if (!target) throw new Error(`crossbind: ${name} runs a tool with api.run, which an installer's api alone has.`);
|
|
204
|
+
if (!Array.isArray(command) || command.length === 0 || !command.every((part) => typeof part === 'string')) {
|
|
205
|
+
throw new Error(`crossbind: ${name} runs no command with api.run, which takes a list of strings: the program and its arguments.`);
|
|
206
|
+
}
|
|
207
|
+
const { cwd = state.config.paths.project, isSigning = false } = options ?? {};
|
|
208
|
+
if (typeof cwd !== 'string') throw new Error(`crossbind: ${name} runs ${command[0]} in ${JSON.stringify(cwd)}, which is no path of a folder.`);
|
|
209
|
+
const dir = path.resolve(state.config.paths.project, cwd);
|
|
210
|
+
if (!fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`crossbind: ${name} runs ${command[0]} in ${dir}, which is no folder.`);
|
|
211
|
+
if (target.platform !== 'darwin' && !isUnderBase(dir, state.config.paths.base)) {
|
|
212
|
+
throw new Error(`crossbind: ${name} runs ${command[0]} in ${dir}, outside ${state.config.paths.base}, the project's base and all the ${imageRoleFor(target)} container sees. Build the app into a folder under it.`);
|
|
213
|
+
}
|
|
214
|
+
if (target.platform === 'win32') inWindowsImage(command, dir, isSigning ? windowsSigning() : null);
|
|
215
|
+
else runTool(null, command, null, target, { workdir: dir });
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The api of an extension's run on a platform: the options the command line gave it, the run's build number once the
|
|
220
|
+
// publish picked it, which the app's facts then show, and an installer's target, the app it makes.
|
|
221
|
+
export default function extensionApi(name, {
|
|
222
|
+
platform, options = {}, isGuided, run = {}, target = null,
|
|
223
|
+
}) {
|
|
224
|
+
const buildEnv = () => (run.buildNumber === undefined || run.buildNumber === null
|
|
225
|
+
? process.env : { ...process.env, CROSSBIND_APP_BUILD_NUMBER: String(run.buildNumber) });
|
|
226
|
+
const config = configView((key, value) => writeConfig(name, platform, key, value));
|
|
227
|
+
return Object.freeze({
|
|
228
|
+
name,
|
|
229
|
+
platform,
|
|
230
|
+
isGuided,
|
|
231
|
+
options: Object.freeze({ ...options }),
|
|
232
|
+
project: Object.freeze({
|
|
233
|
+
get root() { return state.config.paths.project; },
|
|
234
|
+
get name() { return state.config.general.name; },
|
|
235
|
+
get package() { return readOnly(state.config.package); },
|
|
236
|
+
}),
|
|
237
|
+
get workDir() { return path.join(state.config.paths.build, name); },
|
|
238
|
+
config,
|
|
239
|
+
get publisherChanges() { return readOnly(configChangesOf()); },
|
|
240
|
+
app: Object.freeze({
|
|
241
|
+
get id() { return platform === 'android' ? androidAppId(state.config) : appId(state.config); },
|
|
242
|
+
get name() { return appDisplayName(state.config); },
|
|
243
|
+
get version() { return state.config.package?.version ?? '1.0.0'; },
|
|
244
|
+
get appleVersion() { return appVersion(); },
|
|
245
|
+
get versionCode() { return androidVersion(buildEnv()).code; },
|
|
246
|
+
get buildNumber() { return appBuildNumber(buildEnv()); },
|
|
247
|
+
get team() { return appTeam(); },
|
|
248
|
+
get encryption() { return appEncryption(state.config); },
|
|
249
|
+
get urlSchemes() { return Object.freeze(appUrlSchemes(state.config)); },
|
|
250
|
+
get fileTypes() { return Object.freeze(fileTypeOwners(state.config).map(({ extension, ...type }) => Object.freeze(type))); },
|
|
251
|
+
// The Debian packages a Linux app needs beside WebKitGTK and GTK, as its extensions' Linux parts list them.
|
|
252
|
+
get depends() { return Object.freeze(linuxDepends(state.config.extensions ?? [])); },
|
|
253
|
+
get category() {
|
|
254
|
+
const category = appCategory(state.config);
|
|
255
|
+
return category && Object.freeze({ macos: category.darwin, linux: category.linux });
|
|
256
|
+
},
|
|
257
|
+
}),
|
|
258
|
+
log: (text) => console.log(text),
|
|
259
|
+
warn: (text) => console.warn(text),
|
|
260
|
+
...terminalOf(name),
|
|
261
|
+
openUrl(url) {
|
|
262
|
+
let address = null;
|
|
263
|
+
try {
|
|
264
|
+
address = new URL(url);
|
|
265
|
+
} catch {
|
|
266
|
+
// What is no URL is refused below.
|
|
267
|
+
}
|
|
268
|
+
if (!['http:', 'https:'].includes(address?.protocol)) throw new Error(`crossbind: ${name} opens ${JSON.stringify(url)}, which is no http or https address.`);
|
|
269
|
+
openUrl(address.href);
|
|
270
|
+
},
|
|
271
|
+
run: runOf(name, target),
|
|
272
|
+
settings: settingsOf(name),
|
|
273
|
+
secrets: secretsOf(name),
|
|
274
|
+
android: Object.freeze({
|
|
275
|
+
signing: () => releaseSigningEnv(),
|
|
276
|
+
async makeUploadKey() {
|
|
277
|
+
await makeUploadKey();
|
|
278
|
+
state.config.system = loadSystemConfig();
|
|
279
|
+
},
|
|
280
|
+
}),
|
|
281
|
+
apple: Object.freeze({
|
|
282
|
+
key: () => appStoreKey(),
|
|
283
|
+
teams: () => xcodeTeams(),
|
|
284
|
+
xcodebuild: (what, args, xcodebuildOptions) => xcodebuild(what, args, xcodebuildOptions),
|
|
285
|
+
unreasonedApis: (binary, privacy) => unreasonedImports(binary, privacy),
|
|
286
|
+
privacyCategories: deepFrozen(structuredClone(PRIVACY_CATEGORIES)),
|
|
287
|
+
dsyms: async () => [await resolveHermesDsym()],
|
|
288
|
+
}),
|
|
289
|
+
windows: Object.freeze({
|
|
290
|
+
icon() {
|
|
291
|
+
const icon = readAppIcon(state.config);
|
|
292
|
+
return icon && windowsIcon(icon);
|
|
293
|
+
},
|
|
294
|
+
// The script that signs a file in the windows image with the key, which a tool that signs as it builds runs.
|
|
295
|
+
get signer() { return windowsSigning() ? jsignScript() : null; },
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Option } from 'commander';
|
|
2
|
+
import extensionApi from './extensionApi.js';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import { hasTerminal } from '../utils/ask.js';
|
|
5
|
+
import { extensionStep, ownExtensions, withoutNamed } from '../utils/appExtensions.js';
|
|
6
|
+
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
7
|
+
|
|
8
|
+
const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
9
|
+
|
|
10
|
+
// crossbind's own extensions a project has, none where its host is not installed, which each command that needs it says:
|
|
11
|
+
// the commands of those app.without leaves out, and of those whose role or name another takes, go with them.
|
|
12
|
+
function ownOf(listed, app) {
|
|
13
|
+
let root;
|
|
14
|
+
try {
|
|
15
|
+
root = resolveAppHostRoot();
|
|
16
|
+
} catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const taken = new Set(listed.flatMap(({ name, replaces }) => [name, ...(replaces ?? [])]));
|
|
20
|
+
return withoutNamed(ownExtensions(root), app).filter(({ name }) => !taken.has(name));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Adds to crossbind app the commands of an app's extensions, crossbind's own and those crossbind.config.js lists: a name
|
|
24
|
+
// another has stops the run, naming both.
|
|
25
|
+
export default function addExtensionCommands(parent, config = state.config) {
|
|
26
|
+
if (config?.target?.runtimeEnv !== 'app') return;
|
|
27
|
+
const listed = config.extensions ?? [];
|
|
28
|
+
const owners = new Map(parent.commands.map((command) => [command.name(), 'crossbind']));
|
|
29
|
+
for (const { name: extension, commands = {} } of [...ownOf(listed, config.app), ...listed]) {
|
|
30
|
+
for (const [name, command] of Object.entries(commands)) {
|
|
31
|
+
if (owners.has(name)) throw new Error(`crossbind: ${extension} adds the command crossbind app ${name}, which ${owners.get(name)} has.`);
|
|
32
|
+
owners.set(name, extension);
|
|
33
|
+
const added = parent.command(name).description(command.help);
|
|
34
|
+
Object.entries(command.options).forEach(([option, { help, value, default: fallback }]) => {
|
|
35
|
+
const flag = new Option(`--${kebab(option)}${value ? ` ${value}` : ''}`, help);
|
|
36
|
+
added.addOption(fallback === null ? flag : flag.default(fallback));
|
|
37
|
+
});
|
|
38
|
+
added.action((options) => extensionStep(extension, name, () => command.run(extensionApi(extension, { platform: null, options, isGuided: hasTerminal() }))));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import extensionApi from './extensionApi.js';
|
|
2
|
+
import state from '../state/index.js';
|
|
3
|
+
import {
|
|
4
|
+
appExtensionsOf, ownExtensions, withFromApp, withRoles, withoutNamed,
|
|
5
|
+
} from '../utils/appExtensions.js';
|
|
6
|
+
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
7
|
+
|
|
8
|
+
// The platforms as an extension's api names them.
|
|
9
|
+
const API_PLATFORMS = { darwin: 'macos', win32: 'windows' };
|
|
10
|
+
|
|
11
|
+
// The extensions an app of this platform has: crossbind's own but those app.without leaves out, then those
|
|
12
|
+
// crossbind.config.js lists, each with the roles it holds, which a listed one's replaces takes from the host's, and the
|
|
13
|
+
// options it takes from the app's config, where it does.
|
|
14
|
+
export default function extensionsInApp(platform, warn) {
|
|
15
|
+
const own = ownExtensions(resolveAppHostRoot());
|
|
16
|
+
const { app, extensions = [] } = state.config;
|
|
17
|
+
const held = withRoles(appExtensionsOf(withoutNamed(own, app)), appExtensionsOf(extensions), warn, appExtensionsOf(own));
|
|
18
|
+
return withFromApp(held, (name) => extensionApi(name, { platform: API_PLATFORMS[platform] ?? platform, isGuided: false }));
|
|
19
|
+
}
|
|
@@ -2,19 +2,34 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import findFiles from '../utils/findFiles.js';
|
|
4
4
|
import state from '../state/index.js';
|
|
5
|
+
import { declarationOf, dependenciesOf, sourceImportOfHeader } from '../utils/packageSourceImport.js';
|
|
6
|
+
|
|
7
|
+
// Why a bridge's header can no longer be included, or null. A package source's stage outlives the declaration, and
|
|
8
|
+
// the source itself, so the header of a package the project no longer declares is still there while its include
|
|
9
|
+
// directory is off the compile. The stage is named by the path, as the project may have dropped the source too.
|
|
10
|
+
function staleReasonOf(header) {
|
|
11
|
+
if (!fs.existsSync(header)) return 'its header is gone';
|
|
12
|
+
const { config } = state;
|
|
13
|
+
const stages = `${config.paths.cache}/`;
|
|
14
|
+
const [source] = header.startsWith(stages) ? header.slice(stages.length).split('/') : [];
|
|
15
|
+
const staged = source ? sourceImportOfHeader(header, config.paths.cache, [source]) : null;
|
|
16
|
+
if (!staged || Object.hasOwn(dependenciesOf(config, source), staged.name)) return null;
|
|
17
|
+
return `${declarationOf(source)} no longer declares ${staged.name}`;
|
|
18
|
+
}
|
|
5
19
|
|
|
6
20
|
export default function getAllBridges(log = console.log) {
|
|
7
21
|
return findFiles(`${state.config.paths.build}/bridge/*.i.cpp`).filter((bridge) => {
|
|
8
22
|
// The .source sidecar records the header this bridge was generated from. When that
|
|
9
|
-
// header
|
|
10
|
-
// build - the same failure a deleted .rs used to cause. Bridges without
|
|
11
|
-
// predate it and are left alone.
|
|
23
|
+
// header can no longer be included, the stale .i.cpp would #include a missing file and
|
|
24
|
+
// break the native build - the same failure a deleted .rs used to cause. Bridges without
|
|
25
|
+
// a sidecar predate it and are left alone.
|
|
12
26
|
const sidecar = `${bridge}.source`;
|
|
13
27
|
if (!fs.existsSync(sidecar)) return true;
|
|
14
28
|
const source = fs.readFileSync(sidecar, 'utf8').trim();
|
|
15
|
-
|
|
29
|
+
const reason = source ? staleReasonOf(source) : null;
|
|
30
|
+
if (!reason) return true;
|
|
16
31
|
[bridge, `${bridge}.exports.json`, `${bridge}.deps`, `${bridge}.warnings`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
|
|
17
|
-
log(`crossbind: pruned stale bridge '${path.basename(bridge)}' -
|
|
32
|
+
log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - ${reason}`);
|
|
18
33
|
return false;
|
|
19
34
|
});
|
|
20
35
|
}
|
|
@@ -21,6 +21,11 @@ export default function getDependLibs(target) {
|
|
|
21
21
|
if (libPaths.length === 0) {
|
|
22
22
|
libPaths = libsIn(target.releasePath);
|
|
23
23
|
}
|
|
24
|
+
// A published port can leave its iOS archive to its xcframework, where the package's paths lead.
|
|
25
|
+
const xcframeworkLib = target.platform === 'ios' && state.config.allDependencyPaths?.[target.path]?.[fileName]?.lib;
|
|
26
|
+
if (libPaths.length === 0 && xcframeworkLib) {
|
|
27
|
+
libPaths = findFiles(xcframeworkLib, { cwd: d.paths.project });
|
|
28
|
+
}
|
|
24
29
|
dependLibs.push(...libPaths);
|
|
25
30
|
});
|
|
26
31
|
}
|