crossbind 2.0.0-beta.74 → 2.0.0-beta.76

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.74",
3
+ "version": "2.0.0-beta.76",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": {
@@ -38,7 +38,7 @@
38
38
  "vitest": "^5.0.1"
39
39
  },
40
40
  "dependencies": {
41
- "@crossbind/extension-conan": "2.0.0-beta.74",
41
+ "@crossbind/extension-conan": "2.0.0-beta.76",
42
42
  "@rollup/plugin-commonjs": "^29.0.3",
43
43
  "@rollup/plugin-json": "^6.1.0",
44
44
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -55,7 +55,7 @@
55
55
  "upath": "^3.0.8"
56
56
  },
57
57
  "optionalDependencies": {
58
- "@crossbind/bridge-tools": "2.0.0-beta.74"
58
+ "@crossbind/bridge-tools": "2.0.0-beta.76"
59
59
  },
60
60
  "keywords": [
61
61
  "webassembly",
@@ -119,9 +119,15 @@ export function packageAndroidRelease(project) {
119
119
  const key = machineUploadKey();
120
120
  const env = key ? { ...process.env, ...uploadKeyEnv(key) } : process.env;
121
121
  const signing = releaseSigning(env);
122
+ const workdir = inContainer(project);
122
123
  pullDockerImage(ROLE, PLATFORM);
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);
124
+ removeOrphanedDevContainers();
125
+ try {
126
+ docker(['run', '--rm', ...containerArgs(signing, isOnline(project)), '-w', workdir, getDockerImage(ROLE, PLATFORM),
127
+ ...gradleOf(project), '--no-daemon', 'assembleRelease', ...(signing ? ['bundleRelease'] : [])], `packaging ${path.basename(project)}`, env);
128
+ } catch (error) {
129
+ throw withCacheHolders(error);
130
+ }
125
131
  const outputs = `${project}/app/build/outputs`;
126
132
  fs.copyFileSync(`${outputs}/apk/release/app-release.apk`, `${project}.apk`);
127
133
  fs.rmSync(`${project}.aab`, { force: true });
@@ -138,6 +144,42 @@ export function packageAndroidRelease(project) {
138
144
  }
139
145
  }
140
146
 
147
+ // Every Android build on this machine shares Gradle's caches, which a development container's daemon holds until the
148
+ // container goes: Gradle cannot hand them to another container. A development container carries the PID of the session it
149
+ // serves, so that one a session left behind without its exit hooks, as a crash or a SIGKILL leaves, goes before the next
150
+ // build instead of stopping it.
151
+ const SESSION_LABEL = 'dev.crossbind.android-app.session';
152
+
153
+ const isRunning = (pid) => {
154
+ try {
155
+ process.kill(pid, 0);
156
+ return true;
157
+ } catch (error) {
158
+ return error.code === 'EPERM';
159
+ }
160
+ };
161
+
162
+ function removeOrphanedDevContainers() {
163
+ const ps = spawnSync('docker', ['ps', '--all', '--filter', `label=${SESSION_LABEL}`, '--format', `{{.Names}} {{.Label "${SESSION_LABEL}"}}`], { encoding: 'utf8' });
164
+ if (ps.status !== 0) return;
165
+ for (const line of ps.stdout.split('\n')) {
166
+ const [name, pid] = line.trim().split(' ');
167
+ if (!name || !/^\d+$/.test(pid ?? '') || isRunning(Number(pid))) continue;
168
+ console.warn(`crossbind: removing ${name}, the Android development container of a session that has ended.`);
169
+ spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
170
+ }
171
+ }
172
+
173
+ // What holds Gradle's caches when a release cannot take them: the containers of development sessions that run, and those of
174
+ // a crossbind before the label, which name no session.
175
+ function withCacheHolders(error) {
176
+ if (!error.message.includes('Timeout waiting to lock')) return error;
177
+ const ps = spawnSync('docker', ['ps', '--filter', 'name=crossbind-android-app-', '--format', '{{.Names}}'], { encoding: 'utf8' });
178
+ const names = ps.status === 0 ? ps.stdout.split('\n').map((name) => name.trim()).filter(Boolean) : [];
179
+ if (names.length === 0) return error;
180
+ return new Error(`${error.message}\ncrossbind: Gradle's caches are held by ${names.join(', ')}, the container ${names.length > 1 ? 'each ' : ''}of a \`crossbind app dev\`: end that session, or run \`docker rm --force ${names.join(' ')}\` if none is running.`, { cause: error });
181
+ }
182
+
141
183
  // During development one container per project, whose Gradle daemon stays warm from one build to the next; it goes with
142
184
  // the process, and comes anew when the project comes to need the network, or no longer does.
143
185
  let devContainer = null;
@@ -145,10 +187,11 @@ let devContainer = null;
145
187
  function startDevContainer(isNetworked) {
146
188
  const name = `crossbind-android-app-${getContentHash(state.config.paths.project).slice(0, 12)}`;
147
189
  pullDockerImage(ROLE, PLATFORM);
190
+ removeOrphanedDevContainers();
148
191
  // One a crashed session, or the project's need of the network before, left behind holds the name.
149
192
  spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
150
- docker(['run', '--detach', '--rm', '--init', '--name', name, ...containerArgs(null, isNetworked), getDockerImage(ROLE, PLATFORM), 'sleep', 'infinity'],
151
- 'starting the development container');
193
+ docker(['run', '--detach', '--rm', '--init', '--name', name, '--label', `${SESSION_LABEL}=${process.pid}`, ...containerArgs(null, isNetworked),
194
+ getDockerImage(ROLE, PLATFORM), 'sleep', 'infinity'], 'starting the development container');
152
195
  if (!devContainer) {
153
196
  process.once('exit', () => spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' }));
154
197
  // A closed terminal ends the session through its exit hooks, as Ctrl-C does.
@@ -10,7 +10,8 @@ import run from './run.js';
10
10
  import createLib from './createLib.js';
11
11
  import getData from './getData.js';
12
12
  import getEnv from './getEnv.js';
13
- import { copyWindowsNotices, filesUnder, linkInputs } from './buildNode.js';
13
+ import { filesUnder, linkInputs } from './buildNode.js';
14
+ import copyNativeNotices from './nativeNotices.js';
14
15
  import { suppressNodeBuiltinWarnings } from './buildJs.js';
15
16
  import { isNativeSourceNewerThan } from './isSourceNewer.js';
16
17
  import { createNativeBridges } from './nativeBridges.js';
@@ -180,7 +181,7 @@ async function hostInputs(target) {
180
181
  assertAppRunner(target);
181
182
  const inputs = [getDockerImage(imageRoleFor(target), target.platform === 'android' ? 'linux/amd64' : undefined)];
182
183
  if (target.platform === 'android') {
183
- return { hermes: `${IMAGE_HERMES}/android`, cmakeArgs: ['-DANDROID_STL=c++_shared'], inputs };
184
+ return { hermes: `${IMAGE_HERMES}/android`, cmakeArgs: [], inputs };
184
185
  }
185
186
  if (target.platform === 'win32') {
186
187
  return { hermes: `${IMAGE_HERMES}/win32-${target.arch}`, cmakeArgs: ['-DCROSSBIND_WEBVIEW2_INCLUDE=/opt/crossbind/webview2/include'], inputs };
@@ -667,7 +668,7 @@ export default async function buildApp(target, { bridges = [], force = false } =
667
668
  ...cmakeArgs,
668
669
  ], platformPrefix, target);
669
670
  run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
670
- if (target.platform === 'win32') copyWindowsNotices(target);
671
+ copyNativeNotices(target);
671
672
 
672
673
  const systemConfig = `export default ${JSON.stringify({ env: getEnv(target) })};`;
673
674
  await bundle(`${appHostRoot}/js/host.js`, `${workDir}/host.js`, [
@@ -19,10 +19,9 @@ import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmb
19
19
  import scopedEmbind from '../utils/scopedEmbind.js';
20
20
  import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
21
21
  import { rustStdLibsFor } from '../utils/cargoTarget.js';
22
- import toolchainNoticesDir from '../utils/toolchainNotices.js';
22
+ import copyNativeNotices from './nativeNotices.js';
23
23
 
24
24
  const cpuCount = Math.max(1, os.cpus().length - 1);
25
- const WINDOWS_IMAGE_NOTICES = '/opt/licenses/llvm-mingw';
26
25
 
27
26
  // What a build's output, a package's own packages and a repository's history hold, which no build reads.
28
27
  const UNREAD_FOLDERS = ['.crossbind', 'node_modules', '.git'];
@@ -124,13 +123,6 @@ export function bundleNodeLoader(target) {
124
123
  return bundleLoader(target, resolveEmbindNapiRoot(), nodeLoaderConfig(target));
125
124
  }
126
125
 
127
- // The mingw-w64 runtime and winpthreads in a Windows addon or app ask for their notices in binary
128
- // distributions. The windows image carries them; a host toolchain may not.
129
- export function copyWindowsNotices(target) {
130
- const notices = toolchainNoticesDir('win32');
131
- run(null, ['sh', '-c', `if [ -d ${WINDOWS_IMAGE_NOTICES} ]; then mkdir -p ${notices} && cp ${WINDOWS_IMAGE_NOTICES}/* ${notices}/; fi`], null, target);
132
- }
133
-
134
126
  // Links one Node-API addon from the project's archives and bundles the loader that picks the
135
127
  // addon matching the running process.
136
128
  export default async function buildNode(target, options = {}) {
@@ -185,7 +177,7 @@ export default async function buildNode(target, options = {}) {
185
177
  `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
186
178
  ], platformPrefix, target);
187
179
  run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target, { exclusive: true });
188
- if (target.platform === 'win32') copyWindowsNotices(target);
180
+ copyNativeNotices(target);
189
181
  fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.addonName}`, `${build}/${target.addonName}`);
190
182
  logger.doneStep(target, 'addon');
191
183
 
@@ -6,6 +6,7 @@ import getLinkInputs from './getLinkInputs.js';
6
6
  import getData from './getData.js';
7
7
  import getEnv from './getEnv.js';
8
8
  import buildJs from './buildJs.js';
9
+ import copyWasmNotices, { hasWasmNotices } from './wasmNotices.js';
9
10
  import state from '../state/index.js';
10
11
  import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
11
12
  import logger from '../utils/logger.js';
@@ -15,9 +16,10 @@ import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgum
15
16
  import { separateCallArguments } from '../utils/embindCallArguments.js';
16
17
  import { queueJspiCalls } from '../utils/embindJspiQueue.js';
17
18
  import { releaseOwnedHandles } from '../utils/embindObjectLifetime.js';
19
+ import { keepPointerOwners } from '../utils/embindPointerOwners.js';
18
20
  import { spawnPthreadsFromMainScript } from '../utils/pthreadMainScript.js';
19
21
 
20
- const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/pthreadMainScript.js']
22
+ const GLUE_REWRITES = ['../utils/embindArgumentGuards.js', '../utils/embindCallArguments.js', '../utils/embindJspiQueue.js', '../utils/embindObjectLifetime.js', '../utils/embindPointerOwners.js', '../utils/pthreadMainScript.js']
21
23
  .map((file) => fileURLToPath(new URL(file, import.meta.url)));
22
24
 
23
25
  // embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
@@ -76,6 +78,17 @@ function releaseOwnedObjects(target) {
76
78
  }
77
79
  }
78
80
 
81
+ // A pointer a member hands out outlived the object it points into once JavaScript freed that object
82
+ // (utils/embindPointerOwners.js); each runtimeEnv links its own glue, so each one calls this after its link.
83
+ function keepPointerOwnersAlive(target) {
84
+ const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
85
+ const { text, missed } = keepPointerOwners(fs.readFileSync(gluePath, 'utf8'));
86
+ fs.writeFileSync(gluePath, text);
87
+ if (missed) {
88
+ logger.error('embind pointer owner rewrite missed (emscripten glue format changed?): a pointer a method returns can outlive the object it points into');
89
+ }
90
+ }
91
+
79
92
  // A direct-mode mt init in a browser spawns its pthreads from "/undefined" (utils/pthreadMainScript.js).
80
93
  function pointPthreadSpawn(target) {
81
94
  const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
@@ -192,9 +205,12 @@ export default async function buildWasm(target, options = {}) {
192
205
  || fs.readFileSync(linkFingerprintFile, { encoding: 'utf8' }) !== linkFingerprint;
193
206
 
194
207
  if (!options.force && !linkChanged && fs.existsSync(`${state.config.paths.build}/${target.jsName}`) && fs.existsSync(`${state.config.paths.build}/${target.wasmName}`)) {
208
+ // A build made before crossbind kept the toolchain's notices has none.
209
+ if (!hasWasmNotices()) copyWasmNotices(target);
195
210
  logger.cachedStep(target, 'wasm+js');
196
211
  return false;
197
212
  }
213
+ copyWasmNotices(target);
198
214
 
199
215
  if (target.runtimeEnv === 'browser') {
200
216
  logger.startStep(target, 'wasm');
@@ -244,6 +260,7 @@ export default async function buildWasm(target, options = {}) {
244
260
  separateEmbindCalls(target);
245
261
  queueJspiEmbindCalls(target);
246
262
  releaseOwnedObjects(target);
263
+ keepPointerOwnersAlive(target);
247
264
  await buildJs(target);
248
265
  // fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
249
266
  // fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
@@ -286,6 +303,7 @@ export default async function buildWasm(target, options = {}) {
286
303
  separateEmbindCalls(target);
287
304
  queueJspiEmbindCalls(target);
288
305
  releaseOwnedObjects(target);
306
+ keepPointerOwnersAlive(target);
289
307
  await buildJs(target);
290
308
  logger.doneStep(target, 'js');
291
309
  }
@@ -323,6 +341,7 @@ export default async function buildWasm(target, options = {}) {
323
341
  separateEmbindCalls(target);
324
342
  queueJspiEmbindCalls(target);
325
343
  releaseOwnedObjects(target);
344
+ keepPointerOwnersAlive(target);
326
345
  await buildJs(target);
327
346
  if (emccFlags.includes('FETCH')) {
328
347
  fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');