crossbind 2.0.0-beta.64 → 2.0.0-beta.66
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 +6 -1
- package/src/actions/androidDevice.js +77 -0
- package/src/actions/androidGradle.js +121 -0
- package/src/actions/appInstaller.js +174 -0
- package/src/actions/appUpdate.js +68 -0
- package/src/actions/appleSigning.js +96 -0
- package/src/actions/buildApp.js +509 -0
- package/src/actions/buildDependencies.js +4 -3
- package/src/actions/buildExternal.js +3 -3
- package/src/actions/buildLib.js +1 -1
- package/src/actions/buildNode.js +15 -10
- package/src/actions/createInterface.js +1 -2
- package/src/actions/createLib.js +13 -5
- package/src/actions/getCmakeParameters.js +6 -2
- package/src/actions/getDependLibs.js +8 -2
- package/src/actions/getLinkInputs.js +10 -7
- package/src/actions/gradleProject.js +171 -0
- package/src/actions/iosSimulator.js +63 -0
- package/src/actions/licenses.js +189 -14
- package/src/actions/nativeBridges.js +35 -0
- package/src/actions/run.js +3 -3
- package/src/actions/target.js +2 -1
- package/src/actions/windowsSigning.js +96 -0
- package/src/actions/xcodeProject.js +186 -0
- package/src/app/index.d.ts +129 -0
- package/src/app/index.js +184 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +8 -1
- package/src/assets/toolchain-digests.json +24 -18
- package/src/bin.js +84 -32
- package/src/index.js +7 -0
- package/src/integration/getCrossbindScript.js +15 -1
- package/src/state/index.js +5 -1
- package/src/state/loadConfig.js +2 -0
- package/src/utils/appApiHeader.js +29 -0
- package/src/utils/appIcon.js +174 -0
- package/src/utils/appOptions.js +284 -0
- package/src/utils/appRustCrates.js +3 -1
- package/src/utils/downloadAndExtractFile.js +42 -14
- package/src/utils/embindRsFingerprint.js +10 -3
- package/src/utils/hermes.js +40 -0
- package/src/utils/plist.js +42 -0
- package/src/utils/pullDockerImage.js +9 -3
- package/src/utils/replaceBasePathForDocker.js +8 -0
- package/src/utils/resolveAppHost.js +21 -0
- package/src/utils/selectRunner.js +1 -1
- package/src/utils/systemKeys.js +4 -0
- package/src/utils/targets.js +19 -3
- package/src/utils/writeIfChanged.js +1 -1
- package/vitest.config.js +19 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import createBridgeFile from './createInterface.js';
|
|
2
|
+
import { createImportedBridges, findAppNativeImports } from './createImportedBridges.js';
|
|
3
|
+
import getDependFilePath from '../integration/getDependFilePath.js';
|
|
4
|
+
import state from '../state/index.js';
|
|
5
|
+
import findFiles from '../utils/findFiles.js';
|
|
6
|
+
import {
|
|
7
|
+
boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders,
|
|
8
|
+
} from '../utils/boundHeaders.js';
|
|
9
|
+
|
|
10
|
+
// The project's own headers, each with its bridge.
|
|
11
|
+
export function createBridges(target) {
|
|
12
|
+
let headers = [];
|
|
13
|
+
state.config.paths.header.forEach((headerPath) => {
|
|
14
|
+
headers.push(findFiles('**/*.h', { cwd: headerPath }));
|
|
15
|
+
});
|
|
16
|
+
headers = headers.filter((path) => !!path.toString()).flat();
|
|
17
|
+
|
|
18
|
+
return headers.map((header) => ({ file: header, bridge: createBridgeFile(header, target) }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The dependency headers the config binds whole, each read where one of the targets' packages ships it.
|
|
22
|
+
export function createBoundBridges(targets) {
|
|
23
|
+
const headers = resolveBoundHeaders(boundHeaderSpecifiers(state.config), bridgeTargets(targets, state.targets), getDependFilePath);
|
|
24
|
+
const wholeHeaders = headers.map((header) => header.file);
|
|
25
|
+
return headers.map((header) => ({ ...header, bridge: createBridgeFile(header.file, header.target, { wholeHeaders }) }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// What a native binary binds, an addon or an app host: the project's headers, the dependency headers the config binds
|
|
29
|
+
// whole, and the ones the app's sources import.
|
|
30
|
+
export function createNativeBridges(targets) {
|
|
31
|
+
const boundHeaders = createBoundBridges(targets);
|
|
32
|
+
const ownHeaders = [...createBridges(nodeBridgeTarget(targets)), ...boundHeaders];
|
|
33
|
+
const imports = findAppNativeImports(bridgeTargets(targets, state.targets));
|
|
34
|
+
return { headers: [...ownHeaders, ...createImportedBridges(ownHeaders, imports)], imports };
|
|
35
|
+
}
|
package/src/actions/run.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from '../utils/darwinToolchain.js';
|
|
23
23
|
|
|
24
24
|
// Native builds can outrun Node's 1 MiB default pipe buffer; without a raised cap a successful build dies with ENOBUFS.
|
|
25
|
-
const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
|
|
25
|
+
export const EXEC_MAX_BUFFER = 512 * 1024 * 1024;
|
|
26
26
|
// Containers share the memory of Docker's VM, and one compile at full -j can take most of a desktop VM
|
|
27
27
|
// (GDAL's bindings peak at 7.5 of 7.7 GiB), so the exclusive steps of parallel builds take turns.
|
|
28
28
|
const DOCKER_COMPILE_LOCK = path.join(os.homedir(), '.crossbind', 'docker-compile.lock');
|
|
@@ -124,7 +124,7 @@ const iosParams = [
|
|
|
124
124
|
|
|
125
125
|
// The C++ compiler of the image that runs SWIG for the target, so its -dM macro table matches that parse.
|
|
126
126
|
export function cxxPreprocessorFor(target) {
|
|
127
|
-
if (
|
|
127
|
+
if (target?.platform !== 'android') return 'em++';
|
|
128
128
|
return `${t}/${target.arch === 'x86_64' ? CROSSCOMPILER_x86_64 : CROSSCOMPILER_ARM64}-clang++`;
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -196,7 +196,7 @@ export default function run(program, params = [], platformPrefix = null, target
|
|
|
196
196
|
case 'android':
|
|
197
197
|
[dProgram, ...dParams] = params;
|
|
198
198
|
platformParams = target.arch === 'x86_64' ? androidParamsX86_64 : androidParamsArm64;
|
|
199
|
-
if (dProgram === 'cmake') {
|
|
199
|
+
if (dProgram === 'cmake' && dParams[0] !== '--build' && dParams[0] !== '--install') {
|
|
200
200
|
dParams = [
|
|
201
201
|
...dParams,
|
|
202
202
|
`-DCMAKE_TOOLCHAIN_FILE=${ANDROID_NDK}/build/cmake/android.toolchain.cmake`,
|
package/src/actions/target.js
CHANGED
|
@@ -28,7 +28,8 @@ export function getTargetParams(givenTargetParams = {}, noParamCheck = false) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function getBuildTargets(targetParams) {
|
|
31
|
-
|
|
31
|
+
// A lookup that names no runtime environment finds the targets that have none: the archives' alone.
|
|
32
|
+
const { platform, arch, runtime, buildType, runtimeEnv = [] } = targetParams;
|
|
32
33
|
return state.targets.filter(t => (
|
|
33
34
|
(!t.platform || platform.includes(t.platform))
|
|
34
35
|
&& (!t.arch || arch.includes(t.arch))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import state from '../state/index.js';
|
|
5
|
+
import getOsUserAndGroupId from '../utils/getOsUserAndGroupId.js';
|
|
6
|
+
import pullDockerImage, { getDockerImage } from '../utils/pullDockerImage.js';
|
|
7
|
+
import replaceBasePathForDocker, { DOCKER_BASE } from '../utils/replaceBasePathForDocker.js';
|
|
8
|
+
import { DOCKER_RUN_SECURITY_ARGS } from '../utils/dockerSecurity.js';
|
|
9
|
+
import resolveAppHostRoot from '../utils/resolveAppHost.js';
|
|
10
|
+
|
|
11
|
+
// The key that signs a Windows app and its setup program, named by the environment alone so that no secret reaches a
|
|
12
|
+
// file or a command line: jsign's own options, which the windows image's jsign takes through the host's jsign.sh.
|
|
13
|
+
export const WINDOWS_SIGNING = {
|
|
14
|
+
storetype: 'CROSSBIND_WINDOWS_SIGNING_STORETYPE',
|
|
15
|
+
keystore: 'CROSSBIND_WINDOWS_SIGNING_KEYSTORE',
|
|
16
|
+
alias: 'CROSSBIND_WINDOWS_SIGNING_ALIAS',
|
|
17
|
+
storepass: 'CROSSBIND_WINDOWS_SIGNING_STOREPASS',
|
|
18
|
+
keypass: 'CROSSBIND_WINDOWS_SIGNING_KEYPASS',
|
|
19
|
+
certfile: 'CROSSBIND_WINDOWS_SIGNING_CERTFILE',
|
|
20
|
+
tsaurl: 'CROSSBIND_WINDOWS_SIGNING_TSAURL',
|
|
21
|
+
};
|
|
22
|
+
// The store types of the image's jsign 7.6: files, which the container reads mounted, cloud key services, and hardware
|
|
23
|
+
// tokens, which it does not reach.
|
|
24
|
+
const FILE_STORES = ['PKCS12', 'JKS', 'JCEKS'];
|
|
25
|
+
const CLOUDS = ['AWS', 'AZUREKEYVAULT', 'DIGICERTONE', 'ESIGNER', 'GARASIGN', 'GOOGLECLOUD', 'HASHICORPVAULT', 'ORACLECLOUD', 'SIGNPATH', 'SIGNSERVER', 'TRUSTEDSIGNING'];
|
|
26
|
+
const TOKENS = ['PKCS11', 'CRYPTOCERTUM', 'ETOKEN', 'NITROKEY', 'OPENPGP', 'OPENSC', 'PIV', 'YUBIKEY'];
|
|
27
|
+
// Where the container reads the files the variables name.
|
|
28
|
+
const FILES = '/run/crossbind/signing';
|
|
29
|
+
const OUTPUT_LIMIT = 64 * 1024 * 1024;
|
|
30
|
+
const FAILURE_LINES = 40;
|
|
31
|
+
|
|
32
|
+
const listed = (names) => `${names.join(' and ')} ${names.length > 1 ? 'are' : 'is'}`;
|
|
33
|
+
|
|
34
|
+
function existingFile(name, env) {
|
|
35
|
+
const file = path.resolve(env[name] ?? '');
|
|
36
|
+
if (!env[name] || !fs.statSync(file, { throwIfNoEntry: false })?.isFile()) {
|
|
37
|
+
throw new Error(`crossbind: ${name} names ${env[name] ? file : 'nothing'}, which is no file.`);
|
|
38
|
+
}
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The key a Windows app is signed with, or null without CROSSBIND_WINDOWS_SIGNING_STORETYPE: the app and its setup
|
|
43
|
+
// program are then not signed.
|
|
44
|
+
export function windowsSigning(env = process.env) {
|
|
45
|
+
const set = Object.values(WINDOWS_SIGNING).filter((name) => env[name]);
|
|
46
|
+
const storetype = env[WINDOWS_SIGNING.storetype]?.toUpperCase();
|
|
47
|
+
if (!storetype) {
|
|
48
|
+
if (set.length) throw new Error(`crossbind: ${listed(set)} set without ${WINDOWS_SIGNING.storetype}, the kind of key jsign signs a Windows app with.`);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (TOKENS.includes(storetype)) {
|
|
52
|
+
throw new Error(`crossbind: ${WINDOWS_SIGNING.storetype} ${storetype} is a hardware token, which crossbind's windows container does not reach. Sign with a cloud's key service, as Azure Trusted Signing (TRUSTEDSIGNING) or Azure Key Vault (AZUREKEYVAULT), or with a PKCS#12 file.`);
|
|
53
|
+
}
|
|
54
|
+
if (![...FILE_STORES, ...CLOUDS].includes(storetype)) {
|
|
55
|
+
throw new Error(`crossbind: ${WINDOWS_SIGNING.storetype} ${storetype} is no store type jsign takes: ${[...FILE_STORES, ...CLOUDS].join(', ')}.`);
|
|
56
|
+
}
|
|
57
|
+
const files = {};
|
|
58
|
+
if (FILE_STORES.includes(storetype)) files.keystore = existingFile(WINDOWS_SIGNING.keystore, env);
|
|
59
|
+
if (env[WINDOWS_SIGNING.certfile]) files.certfile = existingFile(WINDOWS_SIGNING.certfile, env);
|
|
60
|
+
return { names: set, files };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A command in crossbind's windows image, which sees the project's base, with the signing key's files mounted
|
|
64
|
+
// read-only under their own names and its other variables passed by name, so that their values stay off the command
|
|
65
|
+
// line. A failure names the tool it was, as the label.
|
|
66
|
+
export function inWindowsImage(command, workdir, signing = null, label = command[0]) {
|
|
67
|
+
const { base } = state.config.paths;
|
|
68
|
+
pullDockerImage('windows');
|
|
69
|
+
const files = Object.entries(signing?.files ?? {});
|
|
70
|
+
const mounted = files.map(([key]) => WINDOWS_SIGNING[key]);
|
|
71
|
+
const args = [
|
|
72
|
+
'run', '--rm', ...DOCKER_RUN_SECURITY_ARGS, '--user', getOsUserAndGroupId(), '-v', `${base}:${DOCKER_BASE}`,
|
|
73
|
+
...files.flatMap(([key, file]) => ['-v', `${file}:${FILES}/${key}:ro`, '-e', `${WINDOWS_SIGNING[key]}=${FILES}/${key}`]),
|
|
74
|
+
...(signing?.names ?? []).filter((name) => !mounted.includes(name)).flatMap((name) => ['-e', name]),
|
|
75
|
+
// makensis reads its arguments in the locale's encoding, which the image leaves unset.
|
|
76
|
+
'-e', 'LC_ALL=C.UTF-8',
|
|
77
|
+
'-w', replaceBasePathForDocker(workdir, base),
|
|
78
|
+
getDockerImage('windows'),
|
|
79
|
+
...replaceBasePathForDocker(command, base),
|
|
80
|
+
];
|
|
81
|
+
const result = spawnSync('docker', args, { encoding: 'utf8', maxBuffer: OUTPUT_LIMIT });
|
|
82
|
+
if (result.error) throw new Error(`crossbind: cannot run docker for ${label}: ${result.error.message}`, { cause: result.error });
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().split('\n').slice(-FAILURE_LINES).join('\n');
|
|
85
|
+
const end = result.signal ? `on ${result.signal}` : `with exit code ${result.status}`;
|
|
86
|
+
throw new Error(`crossbind: ${label} failed in the windows image ${end}:\n${output}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The script the windows image signs a file with, which a setup program's build runs too.
|
|
91
|
+
export const jsignScript = () => `${resolveAppHostRoot()}/windows/jsign.sh`;
|
|
92
|
+
|
|
93
|
+
// Signs Windows executables in place with the key the environment names.
|
|
94
|
+
export function signWindows(files, signing) {
|
|
95
|
+
inWindowsImage(['sh', jsignScript(), ...files], path.dirname(files[0]), signing, 'jsign');
|
|
96
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import state from '../state/index.js';
|
|
4
|
+
import { IOS_DEPLOYMENT_TARGET } from '../utils/iosToolchain.js';
|
|
5
|
+
import { textPlist, xmlPlist } from '../utils/plist.js';
|
|
6
|
+
|
|
7
|
+
// The SDKs an iOS app builds for, one target each, and the slice of Hermes' release each takes.
|
|
8
|
+
export const IOS_SDKS = ['iphoneos', 'iphonesimulator'];
|
|
9
|
+
export const HERMES_SLICES = { iphoneos: 'ios-arm64', iphonesimulator: 'ios-arm64_x86_64-simulator' };
|
|
10
|
+
// What the host's own code needs from the system: Xcode adds it only for the sources it compiles itself.
|
|
11
|
+
const SYSTEM_LIBS = ['-framework', 'UIKit', '-framework', 'WebKit', '-framework', 'CoreGraphics', '-lc++'];
|
|
12
|
+
|
|
13
|
+
const MAIN = `// crossbind writes this project anew on every build: the app comes prebuilt in lib/, and its files are in app/.
|
|
14
|
+
int crossbind_app_main(int argc, char *argv[]);
|
|
15
|
+
|
|
16
|
+
int main(int argc, char *argv[]) { return crossbind_app_main(argc, argv); }
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const OBJECT_NAMES = ['project', 'mainGroup', 'products', 'target', 'product', 'main', 'info', 'app', 'hermes', 'mainBuild',
|
|
20
|
+
'appBuild', 'hermesLink', 'hermesEmbed', 'sources', 'frameworks', 'resources', 'embed', 'projectDebug', 'projectRelease',
|
|
21
|
+
'targetDebug', 'targetRelease', 'projectConfigs', 'targetConfigs', 'assets', 'assetsBuild'];
|
|
22
|
+
// Xcode names its objects by 24 hex digits. Fixed ones leave the project unchanged while what it holds is.
|
|
23
|
+
const ID = Object.fromEntries(OBJECT_NAMES.map((name, index) => [name, `CB${(index + 1).toString(16).toUpperCase().padStart(22, '0')}`]));
|
|
24
|
+
|
|
25
|
+
const PROJECT_SETTINGS = {
|
|
26
|
+
SDKROOT: 'iphoneos',
|
|
27
|
+
SUPPORTED_PLATFORMS: 'iphoneos iphonesimulator',
|
|
28
|
+
IPHONEOS_DEPLOYMENT_TARGET: IOS_DEPLOYMENT_TARGET,
|
|
29
|
+
TARGETED_DEVICE_FAMILY: '1,2',
|
|
30
|
+
// crossbind's iOS archives are arm64 alone, for devices and the Simulator alike.
|
|
31
|
+
ARCHS: 'arm64',
|
|
32
|
+
ALWAYS_SEARCH_USER_PATHS: 'NO',
|
|
33
|
+
CLANG_ENABLE_OBJC_ARC: 'YES',
|
|
34
|
+
DEAD_CODE_STRIPPING: 'YES',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ORIENTATIONS = ['UIInterfaceOrientationPortrait', 'UIInterfaceOrientationLandscapeLeft', 'UIInterfaceOrientationLandscapeRight'];
|
|
38
|
+
|
|
39
|
+
// The color set of app.window.backgroundColor, which the launch screen shows.
|
|
40
|
+
const LAUNCH_COLOR = 'LaunchBackground';
|
|
41
|
+
|
|
42
|
+
const infoPlist = (name, urlSchemes, hasBackground, usage) => xmlPlist({
|
|
43
|
+
CFBundleDevelopmentRegion: 'en',
|
|
44
|
+
CFBundleDisplayName: name,
|
|
45
|
+
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
46
|
+
CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)',
|
|
47
|
+
CFBundleInfoDictionaryVersion: '6.0',
|
|
48
|
+
CFBundleName: '$(PRODUCT_NAME)',
|
|
49
|
+
CFBundlePackageType: 'APPL',
|
|
50
|
+
CFBundleShortVersionString: '$(MARKETING_VERSION)',
|
|
51
|
+
...(urlSchemes.length > 0 ? { CFBundleURLTypes: [{ CFBundleURLName: '$(PRODUCT_BUNDLE_IDENTIFIER)', CFBundleURLSchemes: urlSchemes }] } : {}),
|
|
52
|
+
CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
|
|
53
|
+
LSRequiresIPhoneOS: true,
|
|
54
|
+
UIApplicationSceneManifest: { UIApplicationSupportsMultipleScenes: false },
|
|
55
|
+
UILaunchScreen: hasBackground ? { UIColorName: LAUNCH_COLOR } : {},
|
|
56
|
+
UISupportedInterfaceOrientations: ORIENTATIONS,
|
|
57
|
+
'UISupportedInterfaceOrientations~ipad': [...ORIENTATIONS, 'UIInterfaceOrientationPortraitUpsideDown'],
|
|
58
|
+
// The reasons of app.permissions, without which iOS lets the app use none of those devices.
|
|
59
|
+
...usage,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function objects(name, targetSettings, hasAssets) {
|
|
63
|
+
const file = (path, type) => ({ isa: 'PBXFileReference', lastKnownFileType: type, path, sourceTree: '<group>' });
|
|
64
|
+
const phase = (isa, files, extra = {}) => ({ isa, buildActionMask: 2147483647, files, runOnlyForDeploymentPostprocessing: 0, ...extra });
|
|
65
|
+
const configuration = (configurationName, buildSettings) => ({ isa: 'XCBuildConfiguration', buildSettings, name: configurationName });
|
|
66
|
+
const configurations = (debug, release) => ({
|
|
67
|
+
isa: 'XCConfigurationList', buildConfigurations: [debug, release], defaultConfigurationIsVisible: 0, defaultConfigurationName: 'Release',
|
|
68
|
+
});
|
|
69
|
+
// The app's icon and launch screen color, in an asset catalog.
|
|
70
|
+
const assets = hasAssets ? {
|
|
71
|
+
[ID.assets]: file('Assets.xcassets', 'folder.assetcatalog'),
|
|
72
|
+
[ID.assetsBuild]: { isa: 'PBXBuildFile', fileRef: ID.assets },
|
|
73
|
+
} : {};
|
|
74
|
+
return {
|
|
75
|
+
...assets,
|
|
76
|
+
[ID.main]: file('main.m', 'sourcecode.c.objc'),
|
|
77
|
+
[ID.info]: file('Info.plist', 'text.plist.xml'),
|
|
78
|
+
[ID.app]: file('app', 'folder'),
|
|
79
|
+
[ID.hermes]: file('Frameworks/hermesvm.xcframework', 'wrapper.xcframework'),
|
|
80
|
+
[ID.product]: {
|
|
81
|
+
isa: 'PBXFileReference', explicitFileType: 'wrapper.application', includeInIndex: 0, path: `${name}.app`, sourceTree: 'BUILT_PRODUCTS_DIR',
|
|
82
|
+
},
|
|
83
|
+
[ID.mainBuild]: { isa: 'PBXBuildFile', fileRef: ID.main },
|
|
84
|
+
[ID.appBuild]: { isa: 'PBXBuildFile', fileRef: ID.app },
|
|
85
|
+
[ID.hermesLink]: { isa: 'PBXBuildFile', fileRef: ID.hermes },
|
|
86
|
+
[ID.hermesEmbed]: { isa: 'PBXBuildFile', fileRef: ID.hermes, settings: { ATTRIBUTES: ['CodeSignOnCopy', 'RemoveHeadersOnCopy'] } },
|
|
87
|
+
[ID.sources]: phase('PBXSourcesBuildPhase', [ID.mainBuild]),
|
|
88
|
+
[ID.frameworks]: phase('PBXFrameworksBuildPhase', [ID.hermesLink]),
|
|
89
|
+
[ID.resources]: phase('PBXResourcesBuildPhase', [ID.appBuild, ...(hasAssets ? [ID.assetsBuild] : [])]),
|
|
90
|
+
[ID.embed]: phase('PBXCopyFilesBuildPhase', [ID.hermesEmbed], { dstPath: '', dstSubfolderSpec: 10, name: 'Embed Frameworks' }),
|
|
91
|
+
[ID.mainGroup]: {
|
|
92
|
+
isa: 'PBXGroup', children: [ID.main, ID.info, ID.app, ...(hasAssets ? [ID.assets] : []), ID.hermes, ID.products], sourceTree: '<group>',
|
|
93
|
+
},
|
|
94
|
+
[ID.products]: { isa: 'PBXGroup', children: [ID.product], name: 'Products', sourceTree: '<group>' },
|
|
95
|
+
[ID.target]: {
|
|
96
|
+
isa: 'PBXNativeTarget',
|
|
97
|
+
buildConfigurationList: ID.targetConfigs,
|
|
98
|
+
buildPhases: [ID.sources, ID.frameworks, ID.resources, ID.embed],
|
|
99
|
+
buildRules: [],
|
|
100
|
+
dependencies: [],
|
|
101
|
+
name,
|
|
102
|
+
productName: name,
|
|
103
|
+
productReference: ID.product,
|
|
104
|
+
productType: 'com.apple.product-type.application',
|
|
105
|
+
},
|
|
106
|
+
[ID.project]: {
|
|
107
|
+
isa: 'PBXProject',
|
|
108
|
+
buildConfigurationList: ID.projectConfigs,
|
|
109
|
+
compatibilityVersion: 'Xcode 14.0',
|
|
110
|
+
developmentRegion: 'en',
|
|
111
|
+
hasScannedForEncodings: 0,
|
|
112
|
+
knownRegions: ['en', 'Base'],
|
|
113
|
+
mainGroup: ID.mainGroup,
|
|
114
|
+
productRefGroup: ID.products,
|
|
115
|
+
projectDirPath: '',
|
|
116
|
+
projectRoot: '',
|
|
117
|
+
targets: [ID.target],
|
|
118
|
+
},
|
|
119
|
+
[ID.projectDebug]: configuration('Debug', PROJECT_SETTINGS),
|
|
120
|
+
[ID.projectRelease]: configuration('Release', PROJECT_SETTINGS),
|
|
121
|
+
[ID.targetDebug]: configuration('Debug', targetSettings),
|
|
122
|
+
[ID.targetRelease]: configuration('Release', targetSettings),
|
|
123
|
+
[ID.projectConfigs]: configurations(ID.projectDebug, ID.projectRelease),
|
|
124
|
+
[ID.targetConfigs]: configurations(ID.targetDebug, ID.targetRelease),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color.
|
|
129
|
+
function writeAssets(catalog, icon, background) {
|
|
130
|
+
const info = { author: 'xcode', version: 1 };
|
|
131
|
+
const assetSet = (set, contents) => {
|
|
132
|
+
fs.mkdirSync(`${catalog}/${set}`, { recursive: true });
|
|
133
|
+
fs.writeFileSync(`${catalog}/Contents.json`, JSON.stringify({ info }, null, 2));
|
|
134
|
+
fs.writeFileSync(`${catalog}/${set}/Contents.json`, JSON.stringify({ ...contents, info }, null, 2));
|
|
135
|
+
};
|
|
136
|
+
if (icon) {
|
|
137
|
+
assetSet('AppIcon.appiconset', { images: [{ filename: 'icon.png', idiom: 'universal', platform: 'ios', size: '1024x1024' }] });
|
|
138
|
+
fs.writeFileSync(`${catalog}/AppIcon.appiconset/icon.png`, icon);
|
|
139
|
+
}
|
|
140
|
+
if (background) {
|
|
141
|
+
const [red, green, blue] = [1, 3, 5].map((at) => `0x${background.slice(at, at + 2).toUpperCase()}`);
|
|
142
|
+
assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } }, idiom: 'universal' }] });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// An iOS app as an Xcode project, written anew from the parts its SDK targets built: each brings its archives and
|
|
147
|
+
// their link line, and its slice of Hermes; the first also the app's files. Xcode signs the app and runs it, in the
|
|
148
|
+
// Simulator or on a device, and makes every size of its icon from the one PNG given.
|
|
149
|
+
export default function writeXcodeProject(to, { parts, identity, label, version, icon = null, urlSchemes = [], background = null, usage = {} }) {
|
|
150
|
+
const { name } = state.config.general;
|
|
151
|
+
if (parts.length === 0) throw new Error(`crossbind: no part of the iOS app ${name} is built, so there is no Xcode project to write.`);
|
|
152
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
153
|
+
fs.mkdirSync(`${to}/${name}.xcodeproj`, { recursive: true });
|
|
154
|
+
const linkFlags = {};
|
|
155
|
+
parts.forEach(({ sdk, dir }) => {
|
|
156
|
+
fs.cpSync(`${dir}/lib`, `${to}/lib/${sdk}`, { recursive: true });
|
|
157
|
+
// The quotes keep a project path with spaces in one argument.
|
|
158
|
+
const args = JSON.parse(fs.readFileSync(`${dir}/link.json`, 'utf8'))
|
|
159
|
+
.map((arg) => arg.replace(/\{lib\}\/(.+)$/, `"$(PROJECT_DIR)/lib/${sdk}/$1"`));
|
|
160
|
+
linkFlags[`OTHER_LDFLAGS[sdk=${sdk}*]`] = [...args, ...SYSTEM_LIBS];
|
|
161
|
+
});
|
|
162
|
+
fs.cpSync(`${parts[0].dir}/app`, `${to}/app`, { recursive: true });
|
|
163
|
+
const frameworks = parts.flatMap(({ dir }) => ['-framework', `${dir}/hermesvm.framework`]);
|
|
164
|
+
const result = spawnSync('xcodebuild', ['-create-xcframework', ...frameworks, '-output', `${to}/Frameworks/hermesvm.xcframework`], { encoding: 'utf8' });
|
|
165
|
+
if (result.status !== 0) throw new Error(`crossbind: cannot make Hermes' xcframework for ${to}: ${result.stderr || result.error?.message}`);
|
|
166
|
+
|
|
167
|
+
fs.writeFileSync(`${to}/main.m`, MAIN);
|
|
168
|
+
fs.writeFileSync(`${to}/Info.plist`, infoPlist(label, urlSchemes, Boolean(background), usage));
|
|
169
|
+
writeAssets(`${to}/Assets.xcassets`, icon, background);
|
|
170
|
+
const targetSettings = {
|
|
171
|
+
...(icon ? { ASSETCATALOG_COMPILER_APPICON_NAME: 'AppIcon' } : {}),
|
|
172
|
+
PRODUCT_NAME: name,
|
|
173
|
+
PRODUCT_BUNDLE_IDENTIFIER: identity.id,
|
|
174
|
+
DEVELOPMENT_TEAM: identity.team,
|
|
175
|
+
CODE_SIGN_STYLE: 'Automatic',
|
|
176
|
+
INFOPLIST_FILE: 'Info.plist',
|
|
177
|
+
MARKETING_VERSION: version,
|
|
178
|
+
CURRENT_PROJECT_VERSION: version,
|
|
179
|
+
LD_RUNPATH_SEARCH_PATHS: ['$(inherited)', '@executable_path/Frameworks'],
|
|
180
|
+
...linkFlags,
|
|
181
|
+
};
|
|
182
|
+
fs.writeFileSync(`${to}/${name}.xcodeproj/project.pbxproj`, textPlist({
|
|
183
|
+
archiveVersion: 1, classes: {}, objectVersion: 56, objects: objects(name, targetSettings, Boolean(icon || background)), rootObject: ID.project,
|
|
184
|
+
}));
|
|
185
|
+
return to;
|
|
186
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// What an app's host does for its page (target.runtimeEnv: 'app'); in any other build every call rejects.
|
|
2
|
+
|
|
3
|
+
/** A kind of file a dialog offers: its name, and the extensions, with or without the dot, that make it; "*" is any file. */
|
|
4
|
+
export interface FileFilter {
|
|
5
|
+
name: string;
|
|
6
|
+
extensions: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OpenOptions {
|
|
10
|
+
title?: string;
|
|
11
|
+
/** The folder the dialog shows first. */
|
|
12
|
+
defaultPath?: string;
|
|
13
|
+
filters?: FileFilter[];
|
|
14
|
+
/** Folders instead of files. */
|
|
15
|
+
directory?: boolean;
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SaveOptions {
|
|
20
|
+
title?: string;
|
|
21
|
+
/** The folder the dialog shows first. */
|
|
22
|
+
defaultPath?: string;
|
|
23
|
+
defaultName?: string;
|
|
24
|
+
filters?: FileFilter[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MessageOptions {
|
|
28
|
+
message: string;
|
|
29
|
+
/** Smaller text under the message. */
|
|
30
|
+
detail?: string;
|
|
31
|
+
/** info by default. Windows shows its icon for each, and macOS a caution sign for error; Linux, iOS and Android show none. */
|
|
32
|
+
kind?: 'info' | 'warning' | 'error';
|
|
33
|
+
/** One to three labels, the first the default; without them OK. */
|
|
34
|
+
buttons?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The app's own folders, made when first asked for: what it keeps, what it can lose, and its logs. */
|
|
38
|
+
export interface AppPaths {
|
|
39
|
+
data: string;
|
|
40
|
+
cache: string;
|
|
41
|
+
logs: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export declare const dialog: {
|
|
45
|
+
/** The paths of the files or folders the user picked, or null when they cancelled. */
|
|
46
|
+
open(options?: OpenOptions): Promise<string[] | null>;
|
|
47
|
+
/** The path the user chose to save to, or null when they cancelled. */
|
|
48
|
+
save(options?: SaveOptions): Promise<string | null>;
|
|
49
|
+
/** The index of the button the user clicked: Return gives the first, and Escape or closing the dialog the last. */
|
|
50
|
+
message(options: MessageOptions): Promise<number>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export declare const shell: {
|
|
54
|
+
/** An http, https or mailto address in the default browser or mail app. */
|
|
55
|
+
openExternal(url: string | URL): Promise<void>;
|
|
56
|
+
/** A file in the app the system opens its kind with, where app.openPath in crossbind.config.js lets the page: nowhere without it. */
|
|
57
|
+
openPath(path: string): Promise<void>;
|
|
58
|
+
/** A file, selected, in the system's file manager: one in the app's folders, or one a dialog picked during this run. */
|
|
59
|
+
revealInFolder(path: string): Promise<void>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export declare const app: {
|
|
63
|
+
paths(): Promise<AppPaths>;
|
|
64
|
+
/** Ends the app as closing its window does. */
|
|
65
|
+
quit(): Promise<void>;
|
|
66
|
+
/** The links of app.urlSchemes in crossbind.config.js that opened the app and have not reached the page yet, the one it was launched with among them. Once the page has asked, each later link comes as the event open-url while it listens, so listening first, then asking, misses none. */
|
|
67
|
+
launchUrls(): Promise<string[]>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** A key with CmdOrCtrl, Ctrl or Alt and maybe Shift, joined by +, as CmdOrCtrl+Shift+O is, or F1–F12, alone or with any of them. The key is A–Z, 0–9, F1–F12, Enter, Escape, Tab, Space, Backspace, Delete, Up, Down, Left, Right, Home, End, PageUp, PageDown, Plus, Minus, Comma or Period. */
|
|
71
|
+
export type Accelerator = string;
|
|
72
|
+
|
|
73
|
+
/** An item of a menu: one the page handles, by its id, a submenu, a separator, or a standard one by its role. */
|
|
74
|
+
export type MenuItem =
|
|
75
|
+
| { label: string; id?: string; accelerator?: Accelerator; enabled?: boolean; checked?: boolean }
|
|
76
|
+
| { label: string; items: MenuItem[] }
|
|
77
|
+
| { type: 'separator' }
|
|
78
|
+
| { role: 'undo' | 'redo' | 'cut' | 'copy' | 'paste' | 'selectAll'; label?: string; accelerator?: never }
|
|
79
|
+
| { role: 'minimize' | 'close' | 'quit'; label?: string; accelerator?: Accelerator }
|
|
80
|
+
| { role: 'editMenu' | 'windowMenu'; label?: string };
|
|
81
|
+
|
|
82
|
+
/** A menu of the bar, by its items, or a standard one by its role. */
|
|
83
|
+
export type Menu = { label: string; items: MenuItem[] } | { role: 'editMenu' | 'windowMenu'; label?: string };
|
|
84
|
+
|
|
85
|
+
/** What the event menu carries: the id of the item clicked, and for one with checked whether it now is. */
|
|
86
|
+
export interface MenuClick {
|
|
87
|
+
id: string;
|
|
88
|
+
checked?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export declare const menu: {
|
|
92
|
+
/** Sets the app's menus: on macOS after its app menu, which the app keeps, and on Windows and Linux in a bar of its window. A click of an item with an id is an event, menu, with a MenuClick. */
|
|
93
|
+
set(menus: Menu[]): Promise<void>;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** What the event second-instance carries: a later launch's arguments, the program's own left out, and the folder it was launched in. */
|
|
97
|
+
export interface SecondInstance {
|
|
98
|
+
args: string[];
|
|
99
|
+
cwd: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What the event open-url carries: a link of one of app.urlSchemes that the system opened the app with. */
|
|
103
|
+
export interface OpenUrl {
|
|
104
|
+
url: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export declare const events: {
|
|
108
|
+
/** Calls listener with each click of an item with an id in the menus menu.set sets, until the function it returns is called. */
|
|
109
|
+
on(name: 'menu', listener: (click: MenuClick) => void): () => void;
|
|
110
|
+
/** Calls listener with each link of app.urlSchemes the system opens the app with once the page has called app.launchUrls(), until the function it returns is called. */
|
|
111
|
+
on(name: 'open-url', listener: (open: OpenUrl) => void): () => void;
|
|
112
|
+
/** Calls listener with each later launch of a desktop app that app.singleInstance keeps to one instance, which brings its window to the front as far as the system lets it, until the function it returns is called. */
|
|
113
|
+
on(name: 'second-instance', listener: (launch: SecondInstance) => void): () => void;
|
|
114
|
+
/** Calls listener with the value of each event of this name the app's native code emits from now on, until the function it returns is called. */
|
|
115
|
+
on(name: string, listener: (value: unknown) => void): () => void;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** A newer version of the app, which app.updater.url in crossbind.config.js offers. */
|
|
119
|
+
export interface Update {
|
|
120
|
+
version: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One check or install runs at a time, and another rejects meanwhile. */
|
|
124
|
+
export declare const updater: {
|
|
125
|
+
/** The version app.updater.url offers when it is newer than the app's, or null. */
|
|
126
|
+
check(): Promise<Update | null>;
|
|
127
|
+
/** Downloads that version, checks that the key of app.updater.publicKey signed it and installs it in place of the app, which then quits and opens the new version. It rejects when that fails. */
|
|
128
|
+
install(): Promise<void>;
|
|
129
|
+
};
|