snapback4-expo 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,8 +66,13 @@ runner, commands and artifact contract are in `snapback4 guide witness`
66
66
  “Proving it on an iOS simulator”. No Expo Go or Metro process supplies a
67
67
  Release witness.
68
68
 
69
- At every milestone, `npm run export:ios` keeps Hermes in `dist/` and also proves the app runs on iOS when Xcode/runtime are present (warm runs take about 18 s for the plain starter and 27 s for the social one, cold runs 175–230 s, on this mini).
70
- `npm run native:smoke` always runs the full Release build; `SNAPBACK4_NATIVE_SMOKE=off` skips the automatic smoke. Unavailable hosts skip, app/SDK failures fail.
69
+ At every milestone, `npm run export:ios` publishes Hermes to `dist/` and returns.
70
+ Native proof runs in the background at background QoS while you keep building.
71
+ The next export and `npx snapback4 doctor` print the result; for your final report
72
+ run `npx snapback4 witness status --wait`. A newer export supersedes pending proof.
73
+ Failures stay visible with log paths without failing a successful export;
74
+ unavailable hosts report a skip. Background preparation allows 900 seconds without output activity and at most 60 minutes total. Activity alone does not establish build progress; repeated output can keep a stalled build occupied until that cap.
75
+ `npm run native:smoke` explicitly waits for the full build and smoke.
71
76
  The smoke prepares and installs your Release app, chooses its own simulator and free
72
77
  port, and keeps receipts in `artifacts/native-smoke/<timestamp>`. It proves local
73
78
  store acquisition, cold offline reopen and reconnect. Pending write replay, media
@@ -95,5 +100,5 @@ After JS edits, repeat `npm run export:ios`; simulator selection and fresh
95
100
  artifact directories are automatic. Matching native inputs reuse the Release
96
101
  app and install the exact exported bundle/assets. Native changes rebuild;
97
102
  `npm run native:smoke` always runs Xcode. Both routes retain the same loaded-file
98
- hash and prime/offline/restart proof. Measured on the Apartment M5 Pro mini,
99
- 18 cores/64 GiB, Xcode/iOS 27, with tools/dependencies installed first.
103
+ hash and prime/offline/restart proof. Automatic smoke creates and removes its own
104
+ recorded simulator, with project and device leases held through cleanup.
package/export-ios.mjs CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // @ref LLP 3000.030#15 — exact export, explicit environment skips, bounded evidence.
2
+ // @ref LLP 3000.030#15 — publish Hermes first; detached proof retains its exact snapshot.
3
3
  import { spawn, spawnSync } from 'node:child_process';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
6
6
  import fs from 'node:fs';
7
7
  import { pathToFileURL } from 'node:url';
8
- import { createHash } from 'node:crypto';
8
+ import { createHash, randomUUID } from 'node:crypto';
9
9
  import { isDeepStrictEqual } from 'node:util';
10
10
 
11
11
  const sha256 = bytes => createHash('sha256').update(bytes).digest('hex');
@@ -36,6 +36,18 @@ export function verifyPublication(artifacts, source, target, expected) {
36
36
  }
37
37
  }
38
38
 
39
+ // The marker is part of the atomically published inventory. A fresh generation
40
+ // distinguishes equal-byte exports and survives a failed latest.json handoff.
41
+ const publicationMarker = '.snapback4-publication.json';
42
+ function markPublication(root, skip) {
43
+ const metadata = JSON.parse(fs.readFileSync(path.join(root, 'metadata.json')));
44
+ const bundle = metadata?.fileMetadata?.ios?.bundle;
45
+ if (typeof bundle !== 'string' || path.isAbsolute(bundle) || bundle.split(/[\\/]/).includes('..')) throw Error('export metadata has no in-tree iOS bundle');
46
+ const marker = { generation: randomUUID(), exportHash: sha256(fs.readFileSync(path.join(root, bundle))),
47
+ mode: skip ? 'skipped' : 'background', reason: skip, publishedAt: Date.now() };
48
+ fs.writeFileSync(path.join(root, publicationMarker), JSON.stringify(marker));
49
+ }
50
+
39
51
  export function skipReason(platform, env) {
40
52
  if (env.SNAPBACK4_NATIVE_SMOKE === 'off') return 'SNAPBACK4_NATIVE_SMOKE=off';
41
53
  if (platform !== 'darwin') return 'macOS is required';
@@ -80,50 +92,59 @@ export function retainEvidence(root, completed, binary = 'snapback4') {
80
92
  // The helper locks a duplicate of this descriptor, so the kernel lease stays
81
93
  // with this process after the helper exits. Children do not inherit it unless
82
94
  // explicitly listed in stdio (the publisher must keep it if this wrapper dies).
83
- export function acquireExportLease(artifacts, binary = 'snapback4') {
84
- const fd = fs.openSync(path.join(artifacts, 'export.lock'), 'a+');
85
- const result = spawnSync(binary, ['witness', 'export-lease', artifacts], { encoding: 'utf8', env: { ...process.env, SNAPBACK4_EXPORT_WRAPPER_PID: String(process.pid) }, stdio: ['ignore', 'pipe', 'pipe', fd] });
95
+ export function acquireExportLease(artifacts, binary = 'snapback4', initialize = false) {
96
+ const fd = fs.openSync(path.join(artifacts, initialize ? 'initialization.lock' : 'export.lock'), 'a+');
97
+ const result = spawnSync(binary, ['witness', 'export-lease', artifacts, ...(initialize ? ['--initialize'] : [])], { encoding: 'utf8', env: { ...process.env, SNAPBACK4_EXPORT_WRAPPER_PID: String(process.pid) }, stdio: ['ignore', 'pipe', 'pipe', fd] });
86
98
  if (result.status !== 0) { fs.closeSync(fd); throw Error(result.stderr || result.error?.message || 'export lease failed'); }
87
99
  return fd;
88
100
  }
89
101
  export async function main() {
90
102
  const req = createRequire(path.resolve('package.json'));
103
+ // Status is informational: neither an earlier failed proof nor its reporter
104
+ // can fail a new export. The detached worker owns maintenance and cleanup.
105
+ spawnSync('snapback4', ['witness', 'status', '--if-present'], { stdio: 'inherit', timeout: 5000 });
91
106
  const skip = skipReason(process.platform, process.env);
92
- if (skip) {
93
- const exported = await runChild(process.execPath, [req.resolve('expo/bin/cli'), 'export', '--platform', 'ios', '--dump-assetmap']);
94
- if (exported.status !== 0) return exported.status;
95
- console.log(`native smoke skipped: ${skip}`); return 0;
96
- }
97
107
  const root = path.resolve('artifacts/native-smoke');
98
108
  const artifacts = path.join(root, `${Date.now()}-${process.pid}`);
99
109
  const privateExport = path.join(artifacts, 'export');
100
- const receiptPath = path.join(artifacts, 'smoke-ios-run.json');
101
- fs.mkdirSync(artifacts, { recursive: true });
102
- const lease = acquireExportLease(artifacts);
103
- let publicationAttempted = false;
110
+ const publicationExport = path.join(artifacts, 'publication-export');
111
+ let lease, initialization, created = false, publicationAttempted = false;
104
112
  try {
113
+ fs.mkdirSync(root, { recursive: true });
114
+ const maintenance = spawnSync('snapback4', ['witness', 'export-maintenance', root], { stdio: 'inherit', timeout: 5000 });
115
+ if (maintenance.status !== 0) console.error('native smoke evidence cleanup deferred; the next export will retry maintenance');
116
+ initialization = acquireExportLease(root, 'snapback4', true);
117
+ fs.mkdirSync(artifacts); created = true;
118
+ lease = acquireExportLease(artifacts);
119
+ fs.closeSync(initialization); initialization = undefined;
105
120
  const exported = await runChild(process.execPath, [req.resolve('expo/bin/cli'), 'export', '--platform', 'ios', '--dump-assetmap', '--output-dir', privateExport]);
106
121
  if (exported.status !== 0) return exported.status;
107
- const result = await runChild('snapback4', ['witness', 'ios', '--build', '.', '--smoke', '--export', privateExport, '--artifacts', artifacts], { env: { ...process.env, SNAPBACK4_EXPORT_WRAPPER_PID: String(process.pid) } });
108
- let receipt;
109
- try { receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8')); } catch {}
110
- const outcome = result.cancelled ? { status: result.status, message: 'native smoke interrupted' } : smokeResult(result.status, receipt);
111
- if (outcome.status === 0) {
112
- const expected = publicationExpectation(artifacts, privateExport, result.status !== 0, receipt);
113
- publicationAttempted = true;
114
- const published = await runChild('snapback4', ['witness', 'export-publish', privateExport, path.resolve('dist'), ...(result.status === 0 ? [] : ['--environment-skip'])], { stdio: ['inherit', 'inherit', 'inherit', lease] });
115
- try {
116
- verifyPublication(artifacts, privateExport, path.resolve('dist'), expected);
117
- if (published.status !== 0) throw Error(`publisher exited with status ${published.status}`);
118
- } catch (error) { outcome.status = published.status || 1; outcome.message = `export output publication failed: ${error.message}`; }
122
+ markPublication(privateExport, skip);
123
+ const expected = publicationExpectation(artifacts, privateExport, true);
124
+ // APFS clones avoid copying bundle bytes. Both trees are independently
125
+ // verified by the publisher; replacing dist cannot mutate the smoke input.
126
+ fs.cpSync(privateExport, publicationExport, { recursive: true, mode: fs.constants.COPYFILE_FICLONE });
127
+ publicationAttempted = true;
128
+ const published = await runChild('snapback4', ['witness', 'export-publish', publicationExport, path.resolve('dist'), skip ? '--environment-skip' : '--background'], { stdio: ['inherit', 'inherit', 'inherit', lease] });
129
+ try {
130
+ verifyPublication(artifacts, publicationExport, path.resolve('dist'), expected);
131
+ if (published.status !== 0) throw Error(`publisher exited with status ${published.status}`);
132
+ } catch (error) {
133
+ console.log(`export output publication failed: ${error.message}`);
134
+ return published.status || 1;
119
135
  }
120
- console.log(`${outcome.message}; receipt: ${receiptPath}`);
121
- return outcome.status;
136
+ if (skip) console.log(`native smoke skipped: ${skip}`);
137
+ return 0;
122
138
  } finally {
123
139
  // After a publication attempt this path may be the displaced last good
124
140
  // export. Only the publisher, under its destination lock, may remove it.
125
- try { if (!publicationAttempted) fs.rmSync(privateExport, { recursive: true, force: true }); }
126
- finally { fs.closeSync(lease); retainEvidence(root, artifacts); }
141
+ try { if (created && !publicationAttempted) fs.rmSync(artifacts, { recursive: true, force: true }); }
142
+ finally {
143
+ try {
144
+ if (lease !== undefined) fs.closeSync(lease);
145
+ else if (created) fs.rmSync(artifacts, { recursive: true, force: true });
146
+ } finally { if (initialization !== undefined) fs.closeSync(initialization); }
147
+ }
127
148
  }
128
149
  }
129
150
  if (process.argv[1] && import.meta.url === pathToFileURL(fs.realpathSync(process.argv[1])).href) {
@@ -10,7 +10,7 @@
10
10
  <key>HeadersPath</key>
11
11
  <string>Headers</string>
12
12
  <key>LibraryIdentifier</key>
13
- <string>ios-arm64-simulator</string>
13
+ <string>ios-arm64</string>
14
14
  <key>LibraryPath</key>
15
15
  <string>libsnapback4_device.a</string>
16
16
  <key>SupportedArchitectures</key>
@@ -19,8 +19,6 @@
19
19
  </array>
20
20
  <key>SupportedPlatform</key>
21
21
  <string>ios</string>
22
- <key>SupportedPlatformVariant</key>
23
- <string>simulator</string>
24
22
  </dict>
25
23
  <dict>
26
24
  <key>BinaryPath</key>
@@ -28,7 +26,7 @@
28
26
  <key>HeadersPath</key>
29
27
  <string>Headers</string>
30
28
  <key>LibraryIdentifier</key>
31
- <string>ios-arm64</string>
29
+ <string>ios-arm64-simulator</string>
32
30
  <key>LibraryPath</key>
33
31
  <string>libsnapback4_device.a</string>
34
32
  <key>SupportedArchitectures</key>
@@ -37,6 +35,8 @@
37
35
  </array>
38
36
  <key>SupportedPlatform</key>
39
37
  <string>ios</string>
38
+ <key>SupportedPlatformVariant</key>
39
+ <string>simulator</string>
40
40
  </dict>
41
41
  </array>
42
42
  <key>CFBundlePackageType</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapback4-expo",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
4
4
  "type": "module",
5
5
  "description": "Snapback 4's native module for Expo: the device in Rust (the partition in its own SQLite, the stream applied, the outbox kept, queries and predictions answered) behind one JSON call, and the opener `snapback4/native` takes.",
6
6
  "main": "src/index.ts",