crossbind 2.0.0-beta.68 → 2.0.0-beta.69

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 (57) hide show
  1. package/README.md +1 -1
  2. package/package.json +69 -59
  3. package/src/actions/addExtension.js +191 -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 +172 -74
  8. package/src/actions/buildNode.js +6 -1
  9. package/src/actions/buildWasm.js +0 -9
  10. package/src/actions/createInterface.js +17 -8
  11. package/src/actions/createLib.js +0 -4
  12. package/src/actions/extensionApi.js +298 -0
  13. package/src/actions/extensionCommands.js +41 -0
  14. package/src/actions/extensionsInApp.js +19 -0
  15. package/src/actions/gradleProject.js +196 -16
  16. package/src/actions/iosDevice.js +2 -2
  17. package/src/actions/iosSimulator.js +3 -3
  18. package/src/actions/licenses.js +83 -34
  19. package/src/actions/linuxPackages.js +48 -0
  20. package/src/actions/mavenVerification.js +37 -0
  21. package/src/actions/publish.js +220 -0
  22. package/src/actions/windowsSigning.js +1 -1
  23. package/src/actions/xcodeProject.js +49 -24
  24. package/src/actions/xcodeTargets.js +122 -0
  25. package/src/actions/xcodebuild.js +65 -0
  26. package/src/app/index.d.ts +126 -133
  27. package/src/app/index.js +126 -188
  28. package/src/assets/configure/install.sh +0 -0
  29. package/src/assets/toolchain-digests.json +20 -20
  30. package/src/bin.js +20 -28
  31. package/src/index.js +2 -1
  32. package/src/program.js +32 -3
  33. package/src/state/index.js +2 -1
  34. package/src/state/loadConfig.js +16 -5
  35. package/src/utils/appExtensions.js +761 -0
  36. package/src/utils/appFiles.js +58 -0
  37. package/src/utils/appLaunches.js +249 -0
  38. package/src/utils/appOptions.js +109 -100
  39. package/src/utils/appTargets.js +168 -0
  40. package/src/utils/bridgeTools.js +66 -0
  41. package/src/utils/configChanges.js +30 -0
  42. package/src/utils/configWriter.js +143 -26
  43. package/src/utils/extensionSteps.js +135 -0
  44. package/src/utils/openUrl.js +2 -1
  45. package/src/utils/plist.js +5 -0
  46. package/src/utils/resolveAppHost.js +7 -0
  47. package/src/utils/systemConfig.js +16 -3
  48. package/vitest.config.js +6 -1
  49. package/src/actions/appSetup.js +0 -116
  50. package/src/actions/appStoreConnect.js +0 -188
  51. package/src/actions/appUpdate.js +0 -68
  52. package/src/actions/extensions.js +0 -7
  53. package/src/actions/googlePlay.js +0 -213
  54. package/src/actions/googleSignIn.js +0 -78
  55. package/src/actions/playSetup.js +0 -19
  56. package/src/actions/publishAndroid.js +0 -102
  57. package/src/actions/publishIos.js +0 -146
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  <h1>crossbind</h1>
7
7
 
8
8
  <p>
9
- <strong>Import C++ and Rust like JavaScript modules.</strong><br>
9
+ <strong>Build and ship JS apps. Add C++ and Rust as needed.</strong><br>
10
10
  Build, package, and run native libraries across browsers, Node.js, edge
11
11
  runtimes, iOS, Android, and WASI, or ship them in a desktop or mobile app.
12
12
  </p>
package/package.json CHANGED
@@ -1,62 +1,72 @@
1
1
  {
2
- "name": "crossbind",
3
- "version": "2.0.0-beta.68",
4
- "license": "MIT",
5
- "homepage": "https://crossbind.dev",
6
- "repository": "https://github.com/crossbind/crossbind.git",
7
- "description": "Bind C++ to JavaScript without extra code. crossbind supports WebAssembly and React Native for cross-platform compatibility.",
8
- "type": "module",
9
- "engines": {
10
- "node": ">=24"
2
+ "name": "crossbind",
3
+ "version": "2.0.0-beta.69",
4
+ "license": "MIT",
5
+ "homepage": "https://crossbind.dev",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/crossbind/crossbind.git"
9
+ },
10
+ "description": "Bind C++ to JavaScript without extra code. crossbind supports WebAssembly and React Native for cross-platform compatibility.",
11
+ "type": "module",
12
+ "engines": {
13
+ "node": ">=26"
14
+ },
15
+ "bin": {
16
+ "crossbind": "./src/cli.js"
17
+ },
18
+ "main": "src/index.js",
19
+ "exports": {
20
+ ".": "./src/index.js",
21
+ "./node/dev": "./src/node/dev.mjs",
22
+ "./app": {
23
+ "types": "./src/app/index.d.ts",
24
+ "default": "./src/app/index.js"
11
25
  },
12
- "bin": {
13
- "crossbind": "./src/cli.js"
26
+ "./app/core": {
27
+ "types": "./src/app/index.d.ts",
28
+ "default": "./src/app/index.js"
14
29
  },
15
- "main": "src/index.js",
16
- "exports": {
17
- ".": "./src/index.js",
18
- "./node/dev": "./src/node/dev.mjs",
19
- "./app": {
20
- "types": "./src/app/index.d.ts",
21
- "default": "./src/app/index.js"
22
- },
23
- "./*": "./*"
24
- },
25
- "scripts": {
26
- "test": "vitest run",
27
- "test:watch": "vitest",
28
- "test:coverage": "vitest run --coverage"
29
- },
30
- "devDependencies": {
31
- "@vitest/coverage-v8": "^5.0.1",
32
- "vitest": "^5.0.1"
33
- },
34
- "dependencies": {
35
- "@rollup/plugin-commonjs": "^29.0.3",
36
- "@rollup/plugin-json": "^6.1.0",
37
- "@rollup/plugin-node-resolve": "^16.0.3",
38
- "@rollup/plugin-virtual": "^3.0.2",
39
- "comlink": "^4.4.2",
40
- "commander": "^15.0.0",
41
- "glob": "^14.0.3",
42
- "log-update": "^8.0.0",
43
- "picocolors": "^1.1.1",
44
- "pngjs": "^7.0.0",
45
- "replace": "^1.2.2",
46
- "rollup": "^4.63.4",
47
- "spdx-expression-parse": "^5.0.0",
48
- "upath": "^3.0.8"
49
- },
50
- "keywords": [
51
- "webassembly",
52
- "react-native",
53
- "bind",
54
- "c++",
55
- "emscripten",
56
- "embind",
57
- "swig",
58
- "android",
59
- "ios",
60
- "mobile"
61
- ]
62
- }
30
+ "./*": "./*"
31
+ },
32
+ "devDependencies": {
33
+ "@vitest/coverage-v8": "^5.0.1",
34
+ "vitest": "^5.0.1"
35
+ },
36
+ "dependencies": {
37
+ "@rollup/plugin-commonjs": "^29.0.3",
38
+ "@rollup/plugin-json": "^6.1.0",
39
+ "@rollup/plugin-node-resolve": "^16.0.3",
40
+ "@rollup/plugin-virtual": "^3.0.2",
41
+ "comlink": "^4.4.2",
42
+ "commander": "^15.0.0",
43
+ "glob": "^14.0.3",
44
+ "log-update": "^8.0.0",
45
+ "picocolors": "^1.1.1",
46
+ "pngjs": "^7.0.0",
47
+ "replace": "^1.2.2",
48
+ "rollup": "^4.63.4",
49
+ "spdx-expression-parse": "^5.0.0",
50
+ "upath": "^3.0.8"
51
+ },
52
+ "optionalDependencies": {
53
+ "@crossbind/bridge-tools": "2.0.0-beta.69"
54
+ },
55
+ "keywords": [
56
+ "webassembly",
57
+ "react-native",
58
+ "bind",
59
+ "c++",
60
+ "emscripten",
61
+ "embind",
62
+ "swig",
63
+ "android",
64
+ "ios",
65
+ "mobile"
66
+ ],
67
+ "scripts": {
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "test:coverage": "vitest run --coverage"
71
+ }
72
+ }
@@ -0,0 +1,191 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { configFileOf, writeExtension } from '../utils/configWriter.js';
5
+ import { isUnderBase } from '../utils/replaceBasePathForDocker.js';
6
+ import { ownPackages } from '../utils/appExtensions.js';
7
+ import resolveAppHostRoot from '../utils/resolveAppHost.js';
8
+
9
+ // The lockfiles by which a project names the package manager it is installed with.
10
+ const LOCKFILES = [
11
+ ['pnpm-lock.yaml', 'pnpm'], ['yarn.lock', 'yarn'], ['bun.lock', 'bun'], ['bun.lockb', 'bun'],
12
+ ['package-lock.json', 'npm'], ['npm-shrinkwrap.json', 'npm'],
13
+ ];
14
+ const MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
15
+ // A package as a registry names it, with a version, tag or range after an @; a tarball's file name is none.
16
+ const REGISTRY_SPEC = /^((?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*)(?:@.+)?$/;
17
+ const TARBALL = /\.(tgz|tar|tar\.gz)$/;
18
+ // What a package may be given as on Windows, which runs npm, pnpm and yarn in a shell: nothing the shell reads more into.
19
+ const WINDOWS_SPEC = /^[\w@/\\.~:+=#-]+$/;
20
+ // What a package's crossbind.extension may name: a module of its own, as ./crossbind.extension.js.
21
+ const EXTENSION_ENTRY = /^\.\/(?:[\w-][\w.-]*\/)*[\w-][\w.-]*\.m?js$/;
22
+
23
+ // Whether a package's exports let another import a module of its by its subpath, as Node resolves one: a package
24
+ // without exports gives every file, and one whose exports name no subpath gives its main module alone.
25
+ function isExported(exports, entry) {
26
+ if (exports === undefined) return true;
27
+ if (exports === null || typeof exports !== 'object' || Array.isArray(exports)) return false;
28
+ if (Object.hasOwn(exports, entry)) return exports[entry] !== null;
29
+ const pattern = Object.keys(exports).filter((key) => key.startsWith('./') && key.includes('*'))
30
+ .sort((a, b) => b.length - a.length)
31
+ .find((key) => {
32
+ const [head, tail] = key.split('*');
33
+ return entry.length >= head.length + tail.length && entry.startsWith(head) && entry.endsWith(tail);
34
+ });
35
+ return pattern !== undefined && exports[pattern] !== null;
36
+ }
37
+ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'optionalDependencies'];
38
+ // The words module code keeps, which can name no import.
39
+ const RESERVED = new Set(('arguments await break case catch class const continue debugger default delete do else enum eval '
40
+ + 'export extends false finally for function if implements import in instanceof interface let new null package private '
41
+ + 'protected public return static super switch this throw true try typeof var void while with yield').split(' '));
42
+
43
+ const foldersUp = (dir) => (path.dirname(dir) === dir ? [dir] : [dir, ...foldersUp(path.dirname(dir))]);
44
+
45
+ // The package manager a project is installed with: the one its nearest lockfile names, else the one that runs
46
+ // crossbind, else npm.
47
+ export function packageManagerOf(project, userAgent) {
48
+ for (const dir of foldersUp(project)) {
49
+ const lockfile = LOCKFILES.find(([file]) => fs.existsSync(path.join(dir, file)));
50
+ if (lockfile) return lockfile[1];
51
+ }
52
+ const launcher = userAgent?.split('/')[0];
53
+ return MANAGERS.includes(launcher) ? launcher : 'npm';
54
+ }
55
+
56
+ // The package a spec names on a registry, or null where it is a path, a tarball or a repository.
57
+ export function registryName(spec) {
58
+ return TARBALL.test(spec) ? null : (REGISTRY_SPEC.exec(spec)?.[1] ?? null);
59
+ }
60
+
61
+ // The name a config imports an extension under: its package's, without the scope and an extension- prefix, in
62
+ // camelCase, with Extension after a word module code keeps.
63
+ export function importNameOf(packageName) {
64
+ const words = packageName.replace(/^@[^/]+\//, '').replace(/^(crossbind-)?extension-/, '').split(/[^a-zA-Z0-9]+/).filter(Boolean);
65
+ const camel = words.map((word, i) => (i === 0 ? word : `${word[0].toUpperCase()}${word.slice(1)}`)).join('');
66
+ if (!/^[a-zA-Z]/.test(camel)) return `extension${camel}`;
67
+ return RESERVED.has(camel) ? `${camel}Extension` : camel;
68
+ }
69
+
70
+ const dependencyNames = (manifest) => {
71
+ const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
72
+ return new Set(DEPENDENCY_FIELDS.flatMap((field) => Object.keys(pkg[field] ?? {})));
73
+ };
74
+
75
+ // The folder of an installed package, as Node finds it from the project: in the nearest node_modules that has it.
76
+ const packageDir = (project, name) => foldersUp(project).map((dir) => path.join(dir, 'node_modules', name))
77
+ .find((dir) => fs.existsSync(path.join(dir, 'package.json')));
78
+
79
+ const commonFolder = (a, b) => {
80
+ const [partsA, partsB] = [a.split(path.sep), b.split(path.sep)];
81
+ const length = partsA.findIndex((part, i) => part !== partsB[i]);
82
+ return partsA.slice(0, length === -1 ? partsA.length : length).join(path.sep) || path.sep;
83
+ };
84
+
85
+ // Installs the spec into the project with its package manager, as its owner would at a terminal.
86
+ function install(manager, spec, project, platform, spawn) {
87
+ const args = [manager === 'npm' ? 'install' : 'add', spec];
88
+ const command = [manager, ...args].join(' ');
89
+ console.log(`crossbind: ${command}`);
90
+ // On Windows npm, pnpm and yarn are .cmd files, which a shell alone runs; WINDOWS_SPEC keeps what it reads out of the
91
+ // spec.
92
+ const result = platform === 'win32'
93
+ ? spawn(command, { cwd: project, stdio: 'inherit', shell: true })
94
+ : spawn(manager, args, { cwd: project, stdio: 'inherit' });
95
+ if (result.error) {
96
+ throw new Error(`crossbind: ${manager} did not start (${result.error.message}): install it, or install ${spec} with the package manager you use and run crossbind add with its name.`);
97
+ }
98
+ if (result.status !== 0) {
99
+ throw new Error(`crossbind: ${command} failed, so crossbind.config.js lists nothing new: install ${spec} yourself, then run crossbind add with its name.`);
100
+ }
101
+ }
102
+
103
+ // The spec as a package manager would take it, or why not: a spec that opens with a dash is an option to it.
104
+ function assertSpec(spec, platform) {
105
+ if (spec.startsWith('-')) throw new Error(`crossbind: crossbind add takes a package, and ${spec} is an option to a package manager.`);
106
+ if (platform === 'win32' && !WINDOWS_SPEC.test(spec)) {
107
+ throw new Error(`crossbind: on Windows crossbind add takes a package's name, with a version or tag after an @, or its path, without spaces, quotes or ^&|<>%!() for the shell its package manager runs in to read: install ${spec} yourself, then run crossbind add with its name.`);
108
+ }
109
+ }
110
+
111
+ // An extension an app has by having the host is none to add. A project without a host yet has none of them.
112
+ function assertNotOwn(spec) {
113
+ const name = registryName(spec);
114
+ let root;
115
+ try {
116
+ root = resolveAppHostRoot();
117
+ } catch {
118
+ return;
119
+ }
120
+ if (ownPackages(root).includes(name)) {
121
+ throw new Error(`crossbind: an app has ${name} by having @crossbind/core-app-host, which brings it, so there is nothing to add; an extension takes its place with replaces, as extensions.md says.`);
122
+ }
123
+ }
124
+
125
+ // The installed package the spec named: the one the install added to package.json, as a spec's own text names no package
126
+ // for every package manager, else the one a registry spec names, which package.json had already.
127
+ function installedName(spec, before, after, manifest) {
128
+ const added = [...after].filter((name) => !before.has(name));
129
+ const registry = registryName(spec);
130
+ const name = added.length === 1 ? added[0] : (added.length === 0 && after.has(registry) && registry);
131
+ if (!name) {
132
+ throw new Error(`crossbind: crossbind add cannot tell which package ${spec} is, as ${manifest} gained ${added.length === 0 ? 'none' : 'more than one'}: run crossbind add with its name once this project's package.json lists it.`);
133
+ }
134
+ return { name, isAdded: added.length === 1 };
135
+ }
136
+
137
+ // Installs an extension's package into an app and lists it in crossbind.config.js's extensions; a package that
138
+ // package.json has already, given by its name alone, is listed without installing it again.
139
+ export default function addExtension(spec, {
140
+ config, env = process.env, platform = process.platform, spawn = spawnSync,
141
+ }) {
142
+ if (config.target.runtimeEnv !== 'app') {
143
+ throw new Error("crossbind: an extension builds into an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
144
+ }
145
+ assertSpec(spec, platform);
146
+ assertNotOwn(spec);
147
+ const { project, base } = config.paths;
148
+ const manifest = path.join(project, 'package.json');
149
+ if (!fs.existsSync(manifest)) throw new Error(`crossbind: crossbind add installs into the project's package.json, which ${project} has none of.`);
150
+ const manager = packageManagerOf(project, env.npm_config_user_agent);
151
+ const before = dependencyNames(manifest);
152
+ if (registryName(spec) === spec && before.has(spec)) console.log(`crossbind: package.json has ${spec} already.`);
153
+ else install(manager, spec, project, platform, spawn);
154
+ const { name, isAdded } = installedName(spec, before, dependencyNames(manifest), manifest);
155
+ const dir = packageDir(project, name);
156
+ if (!dir) throw new Error(`crossbind: ${name} is in package.json, but not installed: run ${manager} install.`);
157
+ const removal = isAdded ? ` ${manager} ${manager === 'npm' ? 'uninstall' : 'remove'} ${name} removes it.` : '';
158
+ const own = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
159
+ const entry = own.crossbind?.extension;
160
+ if (typeof entry !== 'string') {
161
+ throw new Error(`crossbind: ${name} is installed, but it is no crossbind extension: its package.json has no crossbind.extension.${removal}`);
162
+ }
163
+ if (!EXTENSION_ENTRY.test(entry)) {
164
+ throw new Error(`crossbind: ${name}'s crossbind.extension, ${JSON.stringify(entry)}, is no module of its own as ./crossbind.extension.js is, so crossbind.config.js lists nothing new.${removal}`);
165
+ }
166
+ if (!fs.statSync(path.join(dir, entry), { throwIfNoEntry: false })?.isFile()) {
167
+ throw new Error(`crossbind: ${name}'s crossbind.extension names ${entry}, which its package has not, so crossbind.config.js lists nothing new.${removal}`);
168
+ }
169
+ if (!isExported(own.exports, entry)) {
170
+ throw new Error(`crossbind: ${name}'s package.json exports no ${entry}, which crossbind.config.js would import, so it lists nothing new: its exports has to give it, as "./crossbind.extension.js": "./crossbind.extension.js" does.${removal}`);
171
+ }
172
+ // A build sees paths.base alone, and an extension's root is where its files are, links followed.
173
+ const real = fs.realpathSync(dir);
174
+ if (!isUnderBase(real, base)) {
175
+ // A Windows project's path comes with forward slashes, its real path with backslashes.
176
+ const home = path.resolve(project);
177
+ const above = path.relative(home, commonFolder(home, real)).split(path.sep).join('/') || '.';
178
+ throw new Error(`crossbind: ${name} is in ${real}, outside paths.base, ${base}, which is all a build sees, so crossbind.config.js lists nothing new: set paths: { base: '${above}' } in it, a folder above both, and run crossbind add ${name} again.`);
179
+ }
180
+ const specifier = `${name}/${entry.slice(2)}`;
181
+ const identifier = importNameOf(name);
182
+ const written = writeExtension(project, specifier, identifier);
183
+ if (!written) {
184
+ throw new Error(`crossbind: ${name} is installed, but ${path.basename(configFileOf(project) ?? 'crossbind.config.js')} is no config crossbind can list it in safely: write import ${identifier} from '${specifier}' and extensions: [${identifier}()] into it yourself.`);
185
+ }
186
+ const file = path.basename(written.file);
187
+ console.log(written.isListed
188
+ ? `crossbind: ${file} lists ${name} already, as ${written.identifier}().`
189
+ : `crossbind: listed ${name} in ${file} as ${written.identifier}(), which crossbind app build builds in.`);
190
+ return { name, ...written };
191
+ }
@@ -14,7 +14,9 @@ import { secretFile } from '../utils/secretFile.js';
14
14
  const ROLE = 'android-app';
15
15
  const PLATFORM = 'linux/amd64';
16
16
  const KEYSTORE = '/tmp/crossbind/keystore';
17
- const GRADLE = ['gradle', '--offline', '--no-watch-fs', '--console=plain', '-q'];
17
+ // An extension's Kotlin compiles inside Gradle's own process: with the container's home /, the Kotlin daemon's client
18
+ // walked files for hours looking for a daemon to use.
19
+ const GRADLE = ['gradle', '--offline', '--no-watch-fs', '--console=plain', '-q', '-Pkotlin.compiler.execution.strategy=in-process'];
18
20
  const OUTPUT_LIMIT = 64 * 1024 * 1024;
19
21
  const FAILURE_LINES = 40;
20
22
 
@@ -74,11 +76,16 @@ function homes() {
74
76
  return [[path.join(root, 'gradle'), '/var/cache/crossbind/gradle'], [path.join(root, 'home'), '/var/cache/crossbind/android']];
75
77
  }
76
78
 
77
- // A container of crossbind's android-app image, with no network: the project under the build's base, the homes, and
78
- // the keystore read-only, its passwords passed by name from this environment.
79
- function containerArgs(signing) {
79
+ // A project whose extensions ask Maven for libraries carries the verification metadata of every file it resolves, and
80
+ // resolves what the image's cache lacks online; any other builds offline against the cache alone.
81
+ const isOnline = (project) => fs.existsSync(path.join(project, 'gradle', 'verification-metadata.xml'));
82
+ const gradleOf = (project) => (isOnline(project) ? GRADLE.filter((arg) => arg !== '--offline') : GRADLE);
83
+
84
+ // A container of crossbind's android-app image, with no network but for an online project: the project under the
85
+ // build's base, the homes, and the keystore read-only, its passwords passed by name from this environment.
86
+ function containerArgs(signing, isNetworked = false) {
80
87
  return [
81
- ...DOCKER_RUN_SECURITY_ARGS, '--network', 'none', '--platform', PLATFORM, '--user', getOsUserAndGroupId(),
88
+ ...DOCKER_RUN_SECURITY_ARGS, ...(isNetworked ? [] : ['--network', 'none']), '--platform', PLATFORM, '--user', getOsUserAndGroupId(),
82
89
  '-v', `${state.config.paths.base}:${DOCKER_BASE}`,
83
90
  ...homes().flatMap(([host, container]) => {
84
91
  fs.mkdirSync(host, { recursive: true });
@@ -113,8 +120,8 @@ export function packageAndroidRelease(project) {
113
120
  const env = key ? { ...process.env, ...uploadKeyEnv(key) } : process.env;
114
121
  const signing = releaseSigning(env);
115
122
  pullDockerImage(ROLE, PLATFORM);
116
- docker(['run', '--rm', ...containerArgs(signing), '-w', inContainer(project), getDockerImage(ROLE, PLATFORM),
117
- ...GRADLE, '--no-daemon', 'assembleRelease', ...(signing ? ['bundleRelease'] : [])], `packaging ${path.basename(project)}`, env);
123
+ docker(['run', '--rm', ...containerArgs(signing, isOnline(project)), '-w', inContainer(project), getDockerImage(ROLE, PLATFORM),
124
+ ...gradleOf(project), '--no-daemon', 'assembleRelease', ...(signing ? ['bundleRelease'] : [])], `packaging ${path.basename(project)}`, env);
118
125
  const outputs = `${project}/app/build/outputs`;
119
126
  fs.copyFileSync(`${outputs}/apk/release/app-release.apk`, `${project}.apk`);
120
127
  fs.rmSync(`${project}.aab`, { force: true });
@@ -132,25 +139,27 @@ export function packageAndroidRelease(project) {
132
139
  }
133
140
 
134
141
  // During development one container per project, whose Gradle daemon stays warm from one build to the next; it goes with
135
- // the process.
142
+ // the process, and comes anew when the project comes to need the network, or no longer does.
136
143
  let devContainer = null;
137
144
 
138
- function startDevContainer() {
145
+ function startDevContainer(isNetworked) {
139
146
  const name = `crossbind-android-app-${getContentHash(state.config.paths.project).slice(0, 12)}`;
140
147
  pullDockerImage(ROLE, PLATFORM);
141
- // One a crashed session left behind holds the name.
148
+ // One a crashed session, or the project's need of the network before, left behind holds the name.
142
149
  spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
143
- docker(['run', '--detach', '--rm', '--init', '--name', name, ...containerArgs(null), getDockerImage(ROLE, PLATFORM), 'sleep', 'infinity'],
150
+ docker(['run', '--detach', '--rm', '--init', '--name', name, ...containerArgs(null, isNetworked), getDockerImage(ROLE, PLATFORM), 'sleep', 'infinity'],
144
151
  'starting the development container');
145
- process.once('exit', () => spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' }));
146
- // A closed terminal ends the session through its exit hooks, as Ctrl-C does.
147
- process.once('SIGHUP', () => process.exit(129));
148
- return name;
152
+ if (!devContainer) {
153
+ process.once('exit', () => spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' }));
154
+ // A closed terminal ends the session through its exit hooks, as Ctrl-C does.
155
+ process.once('SIGHUP', () => process.exit(129));
156
+ }
157
+ return { name, isNetworked };
149
158
  }
150
159
 
151
160
  // Builds an Android app's Gradle project as a debug APK, signed with the debug key the homes keep, and returns the APK.
152
161
  export function buildApk(project) {
153
- devContainer ??= startDevContainer();
154
- docker(['exec', '-w', inContainer(project), devContainer, ...GRADLE, 'assembleDebug'], `building ${path.basename(project)}`);
162
+ if (devContainer?.isNetworked !== isOnline(project)) devContainer = startDevContainer(isOnline(project));
163
+ docker(['exec', '-w', inContainer(project), devContainer.name, ...gradleOf(project), 'assembleDebug'], `building ${path.basename(project)}`);
155
164
  return path.join(project, 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
156
165
  }