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.
Files changed (88) hide show
  1. package/README.md +1 -1
  2. package/package.json +70 -59
  3. package/src/actions/addExtension.js +196 -0
  4. package/src/actions/androidGradle.js +26 -17
  5. package/src/actions/appInstaller.js +69 -153
  6. package/src/actions/appSwift.js +66 -0
  7. package/src/actions/buildApp.js +177 -74
  8. package/src/actions/buildDependencies.js +3 -3
  9. package/src/actions/buildNode.js +6 -1
  10. package/src/actions/buildWasm.js +0 -9
  11. package/src/actions/createImportedBridges.js +10 -8
  12. package/src/actions/createInterface.js +40 -29
  13. package/src/actions/createLib.js +3 -7
  14. package/src/actions/extensionApi.js +298 -0
  15. package/src/actions/extensionCommands.js +41 -0
  16. package/src/actions/extensionsInApp.js +19 -0
  17. package/src/actions/getAllBridges.js +20 -5
  18. package/src/actions/getDependLibs.js +5 -0
  19. package/src/actions/getLinkInputs.js +2 -1
  20. package/src/actions/gradleProject.js +207 -16
  21. package/src/actions/iosDevice.js +2 -2
  22. package/src/actions/iosSimulator.js +3 -3
  23. package/src/actions/licenses.js +113 -58
  24. package/src/actions/linuxPackages.js +48 -0
  25. package/src/actions/mavenVerification.js +37 -0
  26. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  27. package/src/actions/publish.js +220 -0
  28. package/src/actions/windowsSigning.js +1 -1
  29. package/src/actions/writeRuntimeEntry.js +2 -2
  30. package/src/actions/xcodeProject.js +60 -29
  31. package/src/actions/xcodeTargets.js +122 -0
  32. package/src/actions/xcodebuild.js +65 -0
  33. package/src/app/index.d.ts +144 -132
  34. package/src/app/index.js +147 -185
  35. package/src/assets/configure/install.sh +0 -0
  36. package/src/assets/node-import-hooks.mjs +7 -6
  37. package/src/assets/toolchain-digests.json +20 -20
  38. package/src/bin.js +37 -37
  39. package/src/index.js +4 -3
  40. package/src/integration/getCrossbindScript.js +3 -2
  41. package/src/integration/getDependFilePath.js +10 -10
  42. package/src/program.js +32 -3
  43. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  44. package/src/state/index.js +8 -6
  45. package/src/state/loadConfig.js +26 -9
  46. package/src/state/refreshPackageSources.js +19 -0
  47. package/src/utils/appExtensions.js +777 -0
  48. package/src/utils/appFiles.js +58 -0
  49. package/src/utils/appLaunches.js +249 -0
  50. package/src/utils/appOptions.js +142 -112
  51. package/src/utils/appRustCrates.js +2 -2
  52. package/src/utils/appTargets.js +168 -0
  53. package/src/utils/bridgeTools.js +71 -0
  54. package/src/utils/configChanges.js +30 -0
  55. package/src/utils/configWriter.js +143 -26
  56. package/src/utils/cppDts.js +11 -11
  57. package/src/utils/extensionSteps.js +172 -0
  58. package/src/utils/nativeImports.js +9 -2
  59. package/src/utils/nodeDevBuild.js +2 -1
  60. package/src/utils/openUrl.js +2 -1
  61. package/src/utils/packageSourceApi.js +87 -0
  62. package/src/utils/packageSourceImport.js +69 -0
  63. package/src/utils/packageSourceInstall.js +162 -0
  64. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  65. package/src/utils/packageSources.js +108 -0
  66. package/src/utils/plist.js +5 -0
  67. package/src/utils/printableText.js +5 -0
  68. package/src/utils/resolveAppHost.js +7 -0
  69. package/src/utils/runPackageSource.js +251 -0
  70. package/src/utils/rustBridgeGen.js +3 -3
  71. package/src/utils/systemConfig.js +16 -3
  72. package/src/utils/targetToolchain.js +77 -0
  73. package/vitest.config.js +6 -1
  74. package/src/actions/appSetup.js +0 -116
  75. package/src/actions/appStoreConnect.js +0 -188
  76. package/src/actions/appUpdate.js +0 -68
  77. package/src/actions/extensions.js +0 -7
  78. package/src/actions/googlePlay.js +0 -213
  79. package/src/actions/googleSignIn.js +0 -78
  80. package/src/actions/playSetup.js +0 -19
  81. package/src/actions/publishAndroid.js +0 -102
  82. package/src/actions/publishIos.js +0 -146
  83. package/src/state/refreshConanDependencies.js +0 -17
  84. package/src/utils/conanDependencies.js +0 -66
  85. package/src/utils/conanImport.js +0 -58
  86. package/src/utils/conanInstall.js +0 -154
  87. package/src/utils/conanProfile.js +0 -203
  88. package/src/utils/runConan.js +0 -257
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import {
4
+ broughtExtension, isJsonValue, ownPackages, stepFailure, withListed,
5
+ } from './appExtensions.js';
6
+ import { getContentHash } from './hash.js';
7
+ import { PACKAGE_NAME, declarationOf, dependenciesOf } from './packageSourceImport.js';
8
+
9
+ // crossbind's own folder, whose dependencies name the extensions it brings.
10
+ const CORE_ROOT = path.join(import.meta.dirname, '..', '..');
11
+
12
+ // The extension packages crossbind itself brings, which a project neither installs nor lists.
13
+ export const packagesOfCrossbind = () => ownPackages(CORE_ROOT);
14
+
15
+ let own = null;
16
+
17
+ // crossbind's own package sources: those the extension packages among its dependencies give, which every project
18
+ // has by having crossbind. Loaded once, as a source may keep what it asked its tool.
19
+ function ownSources() {
20
+ if (!own) {
21
+ const load = createRequire(path.join(CORE_ROOT, 'package.json'));
22
+ own = packagesOfCrossbind()
23
+ .map((name) => broughtExtension(load, name, `update ${name} to match crossbind`))
24
+ .filter((extension) => extension.packageSource);
25
+ }
26
+ return own;
27
+ }
28
+
29
+ // A project's package sources: crossbind's own, then those crossbind.config.js lists. One listed under the name of
30
+ // one of crossbind's takes its place.
31
+ export const packageSourcesOf = (listed) => withListed(ownSources(), listed.filter((extension) => extension.packageSource));
32
+
33
+ export { declarationOf, dependenciesOf };
34
+
35
+ // The sources a project declares packages of.
36
+ export const declaredSources = (config) => (config.packageSources ?? []).filter(({ name }) => Object.keys(dependenciesOf(config, name)).length > 0);
37
+
38
+ export const sourceNamesOf = (config) => (config.packageSources ?? []).map(({ name }) => name);
39
+
40
+ // What an import names a store by, as the node: and npm: schemes do: cargo's crates, and each source's packages.
41
+ export const importSchemesOf = (config) => ['cargo', ...sourceNamesOf(config)];
42
+
43
+ // What a project declares for a source, as the source's check takes it: a map of package names to what each asks
44
+ // for, in plain JSON, as it is hashed, kept in manifests and handed back to the source.
45
+ export function checkedDependencies({ name, packageSource }, raw) {
46
+ let dependencies;
47
+ try {
48
+ dependencies = packageSource.check(raw);
49
+ } catch (error) {
50
+ throw stepFailure(name, 'check', error);
51
+ }
52
+ const isMap = dependencies !== null && typeof dependencies === 'object' && !Array.isArray(dependencies) && isJsonValue(dependencies);
53
+ if (!isMap || !Object.keys(dependencies).every((pkg) => PACKAGE_NAME.test(pkg))) {
54
+ throw new Error(`crossbind: ${name}'s check gives no ${declarationOf(name)} crossbind can use: a JSON map of package names, of lower-case letters, digits and _ + . -, to what each asks for.`);
55
+ }
56
+ return dependencies;
57
+ }
58
+
59
+ // cargo's crates, each as Cargo.toml takes it: a version, or the table that would follow `<crate> =`. Text, where a
60
+ // source's package may be an object: a number would lose a trailing zero, 0.10 becoming 0.1.
61
+ function checkedCrates(crates = {}) {
62
+ if (crates === null || typeof crates !== 'object' || Array.isArray(crates)) {
63
+ throw new Error(`crossbind: ${declarationOf('cargo')} must be an object of crates, such as { uuid: '1' }.`);
64
+ }
65
+ const [crate] = Object.entries(crates).find(([, spec]) => typeof spec !== 'string') ?? [];
66
+ if (crate !== undefined) {
67
+ throw new Error(`crossbind: ${declarationOf('cargo')}.${crate} must be text as Cargo.toml takes it: a version such as '1', or a table such as '{ version = "1", features = ["v4"] }'.`);
68
+ }
69
+ return { ...crates };
70
+ }
71
+
72
+ // What a project declares under packages, by the name its imports open with: cargo's crates, which are the core's
73
+ // own, and each source's packages. Another name stops the config: a misspelt one would be left unread. So does the
74
+ // key cargo or a source once had of its own, with where its packages are declared now and the config that holds it:
75
+ // a package crossbind builds from source loads as a project too, and its key is not the app's developer's to find.
76
+ export function checkedPackages(sources, config, file = 'crossbind.config') {
77
+ const names = sources.map(({ name }) => name);
78
+ const moved = ['cargo', ...names].find((name) => config[`${name}Dependencies`] !== undefined);
79
+ if (moved) throw new Error(`crossbind: ${moved}Dependencies is now ${declarationOf(moved)}: write packages: { ${moved}: { … } } in ${file}.`);
80
+ const { packages = {} } = config;
81
+ if (packages === null || typeof packages !== 'object' || Array.isArray(packages)) {
82
+ throw new Error("crossbind: \"packages\" must be an object of each package source's packages, such as { conan: { zlib: '1.3.2' } }.");
83
+ }
84
+ const unknown = Object.keys(packages).find((name) => name !== 'cargo' && !names.includes(name));
85
+ if (unknown !== undefined) {
86
+ throw new Error(`crossbind: ${declarationOf(unknown)} names neither cargo nor a package source of this project, which has ${names.join(', ')}. List its extension in extensions, or fix the name.`);
87
+ }
88
+ const written = (name) => (Object.hasOwn(packages, name) ? packages[name] : undefined);
89
+ return {
90
+ cargo: checkedCrates(written('cargo')),
91
+ ...Object.fromEntries(sources.map((source) => [source.name, checkedDependencies(source, written(source.name))])),
92
+ };
93
+ }
94
+
95
+ // What staged packages were installed for; a change to the dependencies makes them stale.
96
+ export const dependenciesKey = (dependencies) => getContentHash(JSON.stringify(dependencies));
97
+
98
+ // What an app's headers and sources see of its sources' packages: the packages are staged after a header may
99
+ // already be bound, and restaged in place when one moves to another version. Null without any.
100
+ export function packageSourceInputsOf(config) {
101
+ const declared = declaredSources(config);
102
+ if (declared.length === 0) return null;
103
+ return declared.map(({ name }) => ({
104
+ source: name,
105
+ dependencies: dependenciesOf(config, name),
106
+ refs: config.allDependencies.filter(({ general }) => general.packageSource?.name === name).map(({ general }) => general.packageSource.package.ref),
107
+ }));
108
+ }
@@ -5,6 +5,11 @@ const escapeXml = (text) => String(text).replace(/&/g, '&amp;').replace(/</g, '&
5
5
 
6
6
  function xmlLines(value, indent) {
7
7
  if (typeof value === 'boolean') return [`${indent}<${value}/>`];
8
+ if (typeof value === 'number') {
9
+ // An integer an integer element reads back the same, and any other number a real.
10
+ const type = Number.isSafeInteger(value) ? 'integer' : 'real';
11
+ return [`${indent}<${type}>${value}</${type}>`];
12
+ }
8
13
  const inner = indent + XML_INDENT;
9
14
  if (Array.isArray(value)) return [`${indent}<array>`, ...value.flatMap((item) => xmlLines(item, inner)), `${indent}</array>`];
10
15
  if (typeof value === 'object') {
@@ -0,0 +1,5 @@
1
+ // C0 and C1 controls, which carry terminal escape sequences.
2
+ export const isControlCharacter = (char) => char < ' ' || (char >= '\u007f' && char <= '\u009f');
3
+
4
+ // Text a recipe or a tool wrote, for a terminal or a message: its lines and tabs, without any other control character.
5
+ export const printable = (text) => [...text].filter((char) => char === '\n' || char === '\t' || !isControlCharacter(char)).join('');
@@ -19,3 +19,10 @@ export function resolveAppEmbindJsiRoot() {
19
19
  const req = createRequire(path.join(resolveAppHostRoot(), 'package.json'));
20
20
  return path.dirname(fs.realpathSync(req.resolve('@crossbind/core-embind-jsi/package.json')));
21
21
  }
22
+
23
+ // crossbind/app as an app's page imports it, which the app host assembles with what crossbind's own extensions give the page.
24
+ export function resolveAppPageApi() {
25
+ const file = path.join(resolveAppHostRoot(), 'js', 'app.js');
26
+ if (!fs.existsSync(file)) throw new Error(`crossbind: the app host at ${resolveAppHostRoot()} has no crossbind/app for an app's page: update ${PKG} to match crossbind.`);
27
+ return fs.realpathSync(file);
28
+ }
@@ -0,0 +1,251 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import getOsUserAndGroupId from './getOsUserAndGroupId.js';
6
+ import makeTreeWritable from './makeTreeWritable.js';
7
+ import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
8
+ import { DOCKER_RUN_SECURITY_ARGS } from './dockerSecurity.js';
9
+ import assertExecContainer from './execContainer.js';
10
+ import { IOS_DEVELOPER_DIR, XCODE_TOOLCHAIN_BIN } from './iosToolchain.js';
11
+ import { DARWIN_CC, DARWIN_TOOLS_BIN } from './darwinToolchain.js';
12
+ import { HOST_BUILT_PLATFORMS } from './targets.js';
13
+ import { remoteExecParams } from './remoteRunner.js';
14
+ import { runnerFor } from './selectRunner.js';
15
+
16
+ // Every run of a package source's tool goes through here. The config is passed in instead of read from
17
+ // state, because state attaches the staged packages while it is still being built.
18
+ //
19
+ // The tool runs recipes, which are code, and can write wherever the tool keeps its settings and plugins.
20
+ // So every run gets a fresh work directory, deleted with the run, for whatever of the tool must not
21
+ // outlive it; only the package store does. On the host the environment is rebuilt from an allowlist, so
22
+ // the tool's own variables, a stray CC or a remote's credentials never reach it.
23
+ const ALLOWED_ENV = [
24
+ /^PATH$/,
25
+ /^HOME$/,
26
+ /^USER$/,
27
+ /^LOGNAME$/,
28
+ /^SHELL$/,
29
+ /^TERM$/,
30
+ /^TMPDIR$/,
31
+ /^TEMP$/,
32
+ /^TMP$/,
33
+ /^LANG$/,
34
+ /^LC_[A-Z_]+$/,
35
+ /^(HTTP|HTTPS|ALL|NO)_PROXY$/i,
36
+ /^SSL_CERT_(FILE|DIR)$/,
37
+ /^REQUESTS_CA_BUNDLE$/,
38
+ // A host Emscripten under RUNNER=LOCAL.
39
+ /^EMSDK(_NODE|_PYTHON)?$/,
40
+ /^EM_(CONFIG|CACHE)$/,
41
+ /^SYSTEMROOT$/i,
42
+ /^WINDIR$/i,
43
+ /^USERPROFILE$/i,
44
+ /^PROGRAMDATA$/i,
45
+ ];
46
+
47
+ // What a tool can build on the host: wasm with a host Emscripten, iOS and macOS with Xcode.
48
+ const HOST_PLATFORMS = ['wasm', 'ios', 'darwin'];
49
+ const APPLE_PLATFORM_NAMES = { ios: 'iOS', darwin: 'macOS' };
50
+ // Where a container sees sourceRoot(): an exec container mounts all of it here, a run container only
51
+ // the store and its own work directory, at the same places.
52
+ export const containerRootOf = (name) => `/var/cache/crossbind/${name}`;
53
+ // A recipe's build log is the whole compiler output of a library.
54
+ const MAX_BUFFER = 256 * 1024 * 1024;
55
+ const SPAWN_OPTIONS = { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', maxBuffer: MAX_BUFFER };
56
+
57
+ export function sourceRunner(config, target) {
58
+ const { runner } = runnerFor(imageRoleFor(target), config.system);
59
+ // Xcode runs on the Mac alone, so iOS and macOS packages build there whatever runner the rest use.
60
+ return HOST_BUILT_PLATFORMS.includes(target?.platform) ? 'LOCAL' : runner;
61
+ }
62
+
63
+ // Machine-wide like the cargo cache: built packages are reused by every project, and `pnpm run clear`
64
+ // in one project must not throw them away. Containers write to the store and a tool runs the recipes
65
+ // it keeps there, so a tool on the host never shares a store with them.
66
+ export function sourceRoot(name, runner = 'DOCKER_RUN') {
67
+ return path.join(os.homedir(), '.crossbind', runner === 'LOCAL' ? `${name}-local` : name);
68
+ }
69
+
70
+ // A directory under a source's root, with the store beside it. toolPath and storePath name a file of
71
+ // each as the tool sees it.
72
+ function workAt(name, runner, relative) {
73
+ const root = sourceRoot(name, runner);
74
+ const viewOf = (base) => (...segments) => (runner === 'LOCAL'
75
+ ? path.join(root, base, ...segments)
76
+ : [containerRootOf(name), base, ...segments].join('/'));
77
+ const toolPath = viewOf(relative);
78
+ return {
79
+ name, runner, root, store: path.join(root, 'store'), dir: path.join(root, relative), toolDir: toolPath(), toolPath, storePath: viewOf('store'),
80
+ };
81
+ }
82
+
83
+ // The directory one run reads its inputs from and writes its outputs to.
84
+ export function createSourceWork(name, runner) {
85
+ const root = sourceRoot(name, runner);
86
+ fs.mkdirSync(path.join(root, 'store'), { recursive: true });
87
+ fs.mkdirSync(path.join(root, 'work'), { recursive: true });
88
+ return workAt(name, runner, `work/${path.basename(fs.mkdtempSync(path.join(root, 'work', 'run-')))}`);
89
+ }
90
+
91
+ // Where a source asks the tool of the host what it is, outside any run. Only named where an image says what runs:
92
+ // nothing is asked there, and a build that finds every target staged touches nothing of the home.
93
+ export function createSourceProbe(name, runner) {
94
+ if (runner === 'LOCAL') fs.mkdirSync(path.join(sourceRoot(name, runner), 'probe'), { recursive: true });
95
+ return workAt(name, runner, 'probe');
96
+ }
97
+
98
+ function removeTree(dir) {
99
+ if (!fs.existsSync(dir)) return;
100
+ makeTreeWritable(dir);
101
+ fs.rmSync(dir, { recursive: true, force: true });
102
+ }
103
+
104
+ export const removeSourceWork = (work) => removeTree(work.dir);
105
+
106
+ // What runs that were killed left behind. Only while holding the install lock: no run is going then.
107
+ export const clearSourceWork = (name, runner) => removeTree(path.join(sourceRoot(name, runner), 'work'));
108
+
109
+ // A tool reports paths as it sees them. In a container that is through the mounts, which all sit under
110
+ // the container's root, so mapping the prefix back is exact; a path anywhere else is none crossbind gave it.
111
+ export function toHostPath(reported, work) {
112
+ if (work.runner === 'LOCAL') return path.resolve(reported);
113
+ const containerRoot = containerRootOf(work.name);
114
+ const clean = path.posix.normalize(reported);
115
+ // A backslash is a separator again once the path is joined on a Windows host.
116
+ if (reported.includes('\\') || !clean.startsWith(`${containerRoot}/`)) {
117
+ throw new Error(`crossbind: ${work.name} reported ${JSON.stringify(reported)}, outside the directories crossbind mounts for it.`);
118
+ }
119
+ return path.join(work.root, clean.slice(containerRoot.length + 1));
120
+ }
121
+
122
+ function allowedEnv() {
123
+ return Object.fromEntries(Object.entries(process.env).filter(([key]) => ALLOWED_ENV.some((allowed) => allowed.test(key))));
124
+ }
125
+
126
+ // The Xcode every crossbind iOS archive builds with, whichever one xcode-select points at.
127
+ const iosEnv = () => ({ ...allowedEnv(), DEVELOPER_DIR: IOS_DEVELOPER_DIR });
128
+
129
+ // A GNU ar or nm ahead of Apple's on a Mac's PATH (Homebrew's binutils) writes archives Apple's linker
130
+ // cannot read, and recipes, the build tools they build and Meson all take these tools by name. So the
131
+ // run gets links to Xcode's first on its PATH; for macOS to the ones xcode-select picks, as the macOS
132
+ // ports are built with.
133
+ const APPLE_TOOLS = {
134
+ ar: 'ar', as: 'as', nm: 'llvm-nm', ranlib: 'ranlib', strip: 'strip',
135
+ };
136
+ const appleToolOf = (target, name) => (target.platform === 'darwin'
137
+ ? path.join(DARWIN_TOOLS_BIN, name)
138
+ : path.join(XCODE_TOOLCHAIN_BIN, APPLE_TOOLS[name]));
139
+
140
+ function withAppleTools(work, env, target) {
141
+ if (process.platform !== 'darwin') return env;
142
+ const dir = path.join(work.dir, 'apple-tools');
143
+ if (!fs.existsSync(dir)) {
144
+ fs.mkdirSync(dir);
145
+ Object.keys(APPLE_TOOLS).forEach((name) => fs.symlinkSync(appleToolOf(target, name), path.join(dir, name)));
146
+ }
147
+ return { ...env, PATH: [dir, env.PATH].filter(Boolean).join(path.delimiter) };
148
+ }
149
+
150
+ const toolVersions = new Map();
151
+ // The first line a tool prints for --version, asked once.
152
+ function toolVersion(command, env = allowedEnv()) {
153
+ const key = command.join(' ');
154
+ if (!toolVersions.has(key)) {
155
+ const [program, ...args] = command;
156
+ const result = spawnSync(program, [...args, '--version'], { encoding: 'utf8', env });
157
+ toolVersions.set(key, `${result.stdout ?? ''}`.split('\n')[0].trim());
158
+ }
159
+ return toolVersions.get(key);
160
+ }
161
+
162
+ // The compiler a tool on the host builds with; for a container the image digest says the same.
163
+ export function localCompilerIdentity(target) {
164
+ if (target?.platform === 'ios') return toolVersion(['xcrun', 'clang'], iosEnv());
165
+ if (target?.platform === 'darwin') return toolVersion([DARWIN_CC]);
166
+ return toolVersion(['emcc']);
167
+ }
168
+
169
+ // What a tool on the host cannot build: the targets an image's toolchain builds, and Xcode's off a Mac.
170
+ export function assertRunsOnHost(name, target) {
171
+ if (!HOST_PLATFORMS.includes(target.platform)) {
172
+ throw new Error(`crossbind: ${name} packages for ${target.platform} build in the ${imageRoleFor(target)} image, which RUNNER=LOCAL does not use.`);
173
+ }
174
+ if (HOST_BUILT_PLATFORMS.includes(target.platform) && process.platform !== 'darwin') {
175
+ throw new Error(`crossbind: ${name} packages for ${APPLE_PLATFORM_NAMES[target.platform]} build with Xcode, on a Mac.`);
176
+ }
177
+ }
178
+
179
+ // Of a store, a remote runner sends back the folders its source says never change once written, and nothing else.
180
+ export function assertRunsRemotely({ name, packageSource }) {
181
+ if (packageSource.storeUnits.length > 0) return;
182
+ throw new Error(`crossbind: ${name} names no storeUnits, so a remote runner would send none of its packages back: name the folders of its store that never change once written, as 'p/*/p', or build with another RUNNER.`);
183
+ }
184
+
185
+ // A tool of the host asked what it is, in a source's probe directory: no target's tools are on its PATH.
186
+ export function runProbe([program, ...args], { work, env = {} }) {
187
+ return spawnSync(program, args, { ...SPAWN_OPTIONS, cwd: work.dir, env: { ...allowedEnv(), ...env } });
188
+ }
189
+
190
+ export default function runPackageSource(command, {
191
+ source, config, target, work, env = {},
192
+ }) {
193
+ const [program, ...args] = command;
194
+ const role = imageRoleFor(target);
195
+ if (work.runner === 'LOCAL') {
196
+ const hostEnv = target.platform === 'ios' ? iosEnv() : allowedEnv();
197
+ return spawnSync(program, args, { ...SPAWN_OPTIONS, cwd: work.dir, env: withAppleTools(work, { ...hostEnv, ...env }, target) });
198
+ }
199
+
200
+ // Google ships the linux NDK for x86_64 only.
201
+ const platform = target.platform === 'android' ? 'linux/amd64' : undefined;
202
+ if (work.runner === 'REMOTE') {
203
+ return spawnSync(...remoteExecParams({
204
+ remote: runnerFor(role, config.system).remote,
205
+ role,
206
+ image: getDockerImage(role, platform),
207
+ mounts: [
208
+ // Of the store, only the folders the source names come back: they never change once written.
209
+ { host: work.store, container: work.storePath(), inputRoots: [], outputRoots: source.packageSource.storeUnits },
210
+ { host: work.dir, container: work.toolDir, inputRoots: ['.'], outputRoots: ['.'] },
211
+ ],
212
+ cwd: work.toolDir,
213
+ argv: command,
214
+ env,
215
+ }, SPAWN_OPTIONS));
216
+ }
217
+
218
+ const containerRoot = containerRootOf(source.name);
219
+ const envArgs = Object.entries(env).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
220
+ let runnerArgs;
221
+ if (work.runner === 'DOCKER_EXEC') {
222
+ const name = getDockerContainerName(config.paths.base, role);
223
+ assertExecContainer({
224
+ name,
225
+ mounts: [[containerRoot, work.root]],
226
+ workdir: work.toolDir,
227
+ hint: `Recreate it with \`crossbind docker stop ${role}\`, \`crossbind docker delete ${role}\` and \`crossbind docker create ${role}\`, which mount ${work.root} at ${containerRoot}.`,
228
+ mountNote: `It predates the ${source.name} mount.`,
229
+ });
230
+ runnerArgs = ['exec', ...envArgs, '--user', getOsUserAndGroupId(), '--workdir', work.toolDir, name];
231
+ } else {
232
+ pullDockerImage(role, platform);
233
+ runnerArgs = [
234
+ 'run',
235
+ '--rm',
236
+ ...(platform ? ['--platform', platform] : []),
237
+ ...DOCKER_RUN_SECURITY_ARGS,
238
+ '-v',
239
+ `${work.store}:${work.storePath()}`,
240
+ '-v',
241
+ `${work.dir}:${work.toolDir}`,
242
+ ...envArgs,
243
+ '--user',
244
+ getOsUserAndGroupId(),
245
+ '--workdir',
246
+ work.toolDir,
247
+ getDockerImage(role, platform),
248
+ ];
249
+ }
250
+ return spawnSync('docker', [...runnerArgs, ...command], SPAWN_OPTIONS);
251
+ }
@@ -267,7 +267,7 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
267
267
  '',
268
268
  '[dependencies]',
269
269
  `embind-rs = { path = "${relPath(dir, resolveEmbindRsDir())}" }`,
270
- // The app config's top-level cargoDependencies, so an app-local surface can use
270
+ // The app config's packages.cargo, so an app-local surface can use
271
271
  // upstream crates directly (values: a version string, or a verbatim `{ ... }` spec).
272
272
  ...Object.entries(cargoDependencies).map(([name, spec]) => (
273
273
  String(spec).trim().startsWith('{') ? `${name} = ${spec}` : `${name} = "${spec}"`
@@ -295,7 +295,7 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
295
295
  return { bridgeDir: dir, crateName: `${stem}_crossbind_app`, model };
296
296
  }
297
297
 
298
- // Direct CRATE import (`import { X } from 'cargo:uuid'` with top-level `cargoDependencies`
298
+ // Direct CRATE import (`import { X } from 'cargo:uuid'` with `packages.cargo`
299
299
  // declaring `uuid`, or `cargo:<crate>/<module>` for one public module): no surface file and no
300
300
  // package - the bridge is generated from the upstream crate's OWN multi-file source. cargo
301
301
  // metadata (which fetches on first run) locates the source and the resolved feature set; the
@@ -608,7 +608,7 @@ function findModule(lib, modulePath, enabled, srcDir) {
608
608
  : undefined;
609
609
  file = decl && modFile(file, name);
610
610
  if (!file) {
611
- throw new Error(`crossbind: rust bridge: module '${where}' not found in ${srcDir} - a module behind a cargo feature needs that feature in cargoDependencies, and inline mod { } blocks are not read`);
611
+ throw new Error(`crossbind: rust bridge: module '${where}' not found in ${srcDir} - a module behind a cargo feature needs that feature in packages.cargo, and inline mod { } blocks are not read`);
612
612
  }
613
613
  if (!decl.isPub) throw new Error(`crossbind: rust bridge: module '${where}' in ${srcDir} is private to the crate`);
614
614
  }
@@ -13,7 +13,20 @@ export default function loadSystemConfig() {
13
13
  return { ...system, ...Object.fromEntries(defaults) };
14
14
  }
15
15
 
16
- // Sets keys of ~/.crossbind.json, as `crossbind config set` does, keeping the others.
17
- export function updateSystemConfig(values) {
18
- writeJson(systemConfigPath(), { ...loadSystemConfig(), ...values });
16
+ const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
17
+
18
+ // The settings of an extension's own, which ~/.crossbind.json keeps apart from crossbind's, under extensions by the
19
+ // extension's name.
20
+ export function extensionSettingsOf(system, name) {
21
+ const own = system.extensions === undefined ? {} : isObject(system.extensions) && (system.extensions[name] ?? {});
22
+ if (!isObject(own)) throw new Error(`crossbind: ${systemConfigPath()} keeps the settings of the extension ${name} as no object of them, under extensions: fix it.`);
23
+ return own;
24
+ }
25
+
26
+ // Sets keys of ~/.crossbind.json, as `crossbind config set` does, keeping the others, and with { name, values } settings
27
+ // of that extension's own.
28
+ export function updateSystemConfig(values, extension = null) {
29
+ const system = loadSystemConfig();
30
+ const own = extension && { extensions: { ...system.extensions, [extension.name]: { ...extensionSettingsOf(system, extension.name), ...extension.values } } };
31
+ writeJson(systemConfigPath(), { ...system, ...values, ...own });
19
32
  }
@@ -0,0 +1,77 @@
1
+ import { WASM_EXCEPTION_FLAGS, targetArchiveFlags } from './archiveFlags.js';
2
+ import { ANDROID_NDK, ANDROID_API_LEVEL } from './androidToolchain.js';
3
+ import { IOS_DEPLOYMENT_TARGET } from './iosToolchain.js';
4
+ import {
5
+ LINUX_ARCHIVE_FLAGS, linuxBuildEnv, linuxTool, linuxToolchainFile, linuxTriple,
6
+ } from './linuxToolchain.js';
7
+ import {
8
+ DARWIN_CC, DARWIN_CXX, DARWIN_DEPLOYMENT_TARGET, DARWIN_HOST_PACKAGE_PREFIXES,
9
+ } from './darwinToolchain.js';
10
+ import {
11
+ windowsBuildEnv, windowsTool, windowsToolchainFile, windowsTriple,
12
+ } from './windowsToolchain.js';
13
+
14
+ // What crossbind builds a target's own archives with, as plain data: a package source builds its packages to match,
15
+ // since they link into the same module.
16
+ const CPP_STANDARD = '20';
17
+ const NDK_BIN = `${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin`;
18
+ const LINUX_LIBCS = { linux: 'glibc', linuxmusl: 'musl' };
19
+
20
+ const linux = (target) => ({
21
+ flags: LINUX_ARCHIVE_FLAGS,
22
+ compilers: { c: linuxTool(target, 'clang'), cpp: linuxTool(target, 'clang++') },
23
+ env: linuxBuildEnv(target),
24
+ cmakeToolchainFile: linuxToolchainFile(target),
25
+ triple: linuxTriple(target),
26
+ libc: LINUX_LIBCS[target.platform],
27
+ });
28
+
29
+ const PLATFORMS = {
30
+ wasm: () => ({
31
+ flags: WASM_EXCEPTION_FLAGS,
32
+ compilers: { c: 'emcc', cpp: 'em++' },
33
+ env: {
34
+ CC: 'emcc', CXX: 'em++', AR: 'emar', NM: 'emnm', RANLIB: 'emranlib', STRIP: 'emstrip',
35
+ },
36
+ }),
37
+ android: () => ({
38
+ compilers: { c: `${NDK_BIN}/clang`, cpp: `${NDK_BIN}/clang++` },
39
+ ndk: ANDROID_NDK,
40
+ apiLevel: ANDROID_API_LEVEL,
41
+ }),
42
+ // No compilers: Xcode's own build it, which xcrun finds for the SDK the arch names.
43
+ ios: () => ({ deploymentTarget: IOS_DEPLOYMENT_TARGET }),
44
+ linux,
45
+ linuxmusl: linux,
46
+ darwin: () => ({
47
+ compilers: { c: DARWIN_CC, cpp: DARWIN_CXX },
48
+ env: { PKG_CONFIG_LIBDIR: '' },
49
+ deploymentTarget: DARWIN_DEPLOYMENT_TARGET,
50
+ ignoredPrefixes: [...DARWIN_HOST_PACKAGE_PREFIXES],
51
+ }),
52
+ win32: (target) => ({
53
+ compilers: {
54
+ c: windowsTool(target, 'clang'), cpp: windowsTool(target, 'clang++'), rc: windowsTool(target, 'windres'),
55
+ },
56
+ env: windowsBuildEnv(target),
57
+ cmakeToolchainFile: windowsToolchainFile(target),
58
+ triple: windowsTriple(target),
59
+ }),
60
+ };
61
+
62
+ export const TOOLCHAIN_PLATFORMS = Object.keys(PLATFORMS);
63
+
64
+ // Null for a platform package sources build nothing for yet.
65
+ export function targetToolchain(target) {
66
+ const own = PLATFORMS[target.platform]?.(target);
67
+ if (!own) return null;
68
+ return {
69
+ platform: target.platform,
70
+ arch: target.arch,
71
+ runtime: target.runtime,
72
+ cppStandard: CPP_STANDARD,
73
+ env: {},
74
+ ...own,
75
+ flags: [...(own.flags ?? []), ...targetArchiveFlags(target)],
76
+ };
77
+ }
package/vitest.config.js CHANGED
@@ -52,7 +52,12 @@ export default defineConfig({
52
52
  // exercised a machine without that image already cached, and android builds shipped
53
53
  // broken to every Apple Silicon user. Which command line these actions build IS the
54
54
  // behaviour, so the layer is measured.
55
- include: ['src/utils/**/*.js', 'src/state/**/*.js', 'src/assets/**/*.js', 'src/actions/**/*.js'],
55
+ // crossbind's own extensions' build-side code, which left src/ as packages of their own, is measured with it.
56
+ include: [
57
+ 'src/utils/**/*.js', 'src/state/**/*.js', 'src/assets/**/*.js', 'src/actions/**/*.js',
58
+ '../../extensions/*/crossbind.extension.js', '../../extensions/*/src/**/*.js', '../../core/store-kit/*.js',
59
+ ],
60
+ allowExternal: true,
56
61
  all: true,
57
62
  // A floor with a one-point band, not a ratchet. Coverage is not identical across
58
63
  // environments - resolveEmbindRust walks the real node_modules layout, so which of its
@@ -1,116 +0,0 @@
1
- import state from '../state/index.js';
2
- import { askLine, askYesNo } from '../utils/ask.js';
3
- import { entry, writeAppOption } from '../utils/configWriter.js';
4
- import { PRIVACY_CATEGORIES } from '../utils/appOptions.js';
5
- import { readAppIcon } from '../utils/appIcon.js';
6
-
7
- // What only the project can answer, which `crossbind app publish` asks on a terminal and writes into crossbind.config.js
8
- // once the user says yes.
9
-
10
- // An id both stores take: dot-separated parts of letters and digits, each starting with a letter, as Apple's take no
11
- // underscore and Google's no hyphen.
12
- const STORE_APP_ID = /^[A-Za-z][A-Za-z0-9]*(\.[A-Za-z][A-Za-z0-9]*)+$/;
13
-
14
- const REASONS_PAGE = 'https://developer.apple.com/documentation/bundleresources/describing-use-of-required-reason-api';
15
- // Apple's reasons an app may give, in short; that page has their rules. 0A2A.1 and C56D.1 are for third-party SDKs alone.
16
- const REASONS = {
17
- 'DDA9.1': 'to show file timestamps to the user; they stay on the device',
18
- 'C617.1': "for the timestamps, size or other metadata of files in the app's container, app group container or CloudKit container",
19
- '3B52.1': 'for those of files or folders the user granted access to, as with a document picker',
20
- '35F9.1': 'to measure the time between events in the app, or for timers; only that time may leave the device',
21
- '8FFB.1': "to turn the times of the app's events into absolute timestamps, which may leave the device; the boot time may not",
22
- '3D61.1': 'in an optional bug report the user chooses to send, which shows it prominently',
23
- '85F4.1': 'to show disk space to the user; it stays on the device',
24
- 'E174.1': 'to check for space before writing files, or to delete files when space runs low, in a way the user notices; it stays on the device',
25
- '7D9E.1': 'in an optional bug report the user chooses to send, which shows it prominently',
26
- 'B728.1': 'in a health research app, to tell participants that low disk space hampers the research',
27
- '3EC4.1': 'in a custom keyboard app, to find the keyboards active on the device',
28
- '54BD.1': 'to show the right interface for the active keyboard in text fields, in a way the user notices; it stays on the device',
29
- 'CA92.1': 'for information only the app itself reads and writes',
30
- '1C8F.1': "for information only the apps and extensions of the app's App Group read and write",
31
- 'AC6B.1': 'for the managed app configuration MDM sets, or the feedback MDM reads',
32
- };
33
-
34
- // Writes app.<key> into crossbind.config.js once the user says yes, and into this run's config. One the config sets
35
- // already is the user's to change.
36
- async function writeAppAnswer(key, value) {
37
- const text = entry(key, value);
38
- if (state.config.app[key] !== undefined) throw new Error(`crossbind: set app.${text} in crossbind.config.js in place of the app.${key} there, and publish again.`);
39
- if (!await askYesNo(`Write app.${text} into crossbind.config.js? [Y/n] `)) throw new Error(`crossbind: set app.${text} in crossbind.config.js to publish.`);
40
- if (!writeAppOption(state.config.paths.project, key, value)) {
41
- throw new Error(`crossbind: crossbind.config.js is no config crossbind can write app.${key} into safely: set app.${text} in it yourself.`);
42
- }
43
- state.config.app = { ...state.config.app, [key]: value };
44
- }
45
-
46
- // Asks for the app's id, which the store keeps for good, under the name the store gives it.
47
- export async function askAppId(store, term) {
48
- console.log(`crossbind: ${store} knows an app by its ${term}, app.id, and keeps it for good.`);
49
- for (;;) {
50
- const id = (await askLine(`The ${term}, as com.example.myapp: `)).trim();
51
- if (STORE_APP_ID.test(id)) {
52
- await writeAppAnswer('id', id);
53
- return id;
54
- }
55
- console.log(`That is no ${term} both stores take, which is dot-separated parts of letters and digits, each starting with a letter.`);
56
- }
57
- }
58
-
59
- function iconProblem(icon) {
60
- try {
61
- readAppIcon({ ...state.config, app: { ...state.config.app, icon } });
62
- return null;
63
- } catch (error) {
64
- return error.message;
65
- }
66
- }
67
-
68
- // Asks for the app's icon, without which App Store Connect takes no iOS app.
69
- export async function askIcon() {
70
- console.log('crossbind: App Store Connect takes no iOS app without an icon, app.icon: a square PNG, at best 1024 × 1024.');
71
- for (;;) {
72
- const icon = (await askLine("Its path, from the project's folder: ")).trim();
73
- const problem = iconProblem(icon);
74
- if (!problem) {
75
- await writeAppAnswer('icon', icon);
76
- return;
77
- }
78
- console.log(problem);
79
- }
80
- }
81
-
82
- // Asks whether the app's encryption is iOS's own alone, which App Store Connect otherwise asks for every build. The answer
83
- // is a legal statement, so there is no default.
84
- export async function askEncryption() {
85
- console.log('crossbind: App Store Connect asks for every build whether the app uses encryption that export rules do not exempt, until app.encryption answers it.');
86
- for (;;) {
87
- const answer = (await askLine('Does the app use only the encryption iOS provides, as HTTPS through iOS, or none at all? [y/n] ')).trim();
88
- if (/^[yn]/i.test(answer)) {
89
- await writeAppAnswer('encryption', /^n/i.test(answer));
90
- return;
91
- }
92
- console.log('Answer y or n.');
93
- }
94
- }
95
-
96
- async function askReasons(reasons) {
97
- for (;;) {
98
- const picks = (await askLine('Which apply, as 1 or 1,2? ')).split(',').map((pick) => pick.trim()).filter(Boolean);
99
- const chosen = picks.map((pick) => reasons[Number(pick) - 1]);
100
- if (chosen.length > 0 && chosen.every(Boolean)) return [...new Set(chosen)];
101
- console.log(`Name one or more of 1 to ${reasons.length}, separated by commas.`);
102
- }
103
- }
104
-
105
- // Asks for Apple's reasons for each category of required reason API the app calls with none in app.privacy.
106
- export async function askPrivacy(unreasoned) {
107
- console.log(`crossbind: App Store Connect refuses an iOS app that calls required reason API without Apple's reasons for it (ITMS-91053), which app.privacy gives; ${REASONS_PAGE} has the rules of each.`);
108
- const privacy = { ...state.config.app.privacy };
109
- for (const { category, symbols } of unreasoned) {
110
- const reasons = PRIVACY_CATEGORIES[category].reasons.filter((reason) => Object.hasOwn(REASONS, reason));
111
- console.log(`The app calls ${category} API (${symbols.join(', ')}). Apple's reasons for it:`);
112
- reasons.forEach((reason, index) => console.log(` ${index + 1}) ${reason} ${REASONS[reason]}`));
113
- privacy[category] = await askReasons(reasons);
114
- }
115
- await writeAppAnswer('privacy', privacy);
116
- }