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 +9 -4
- package/export-ios.mjs +51 -30
- package/ios/Snapback4Device.xcframework/Info.plist +4 -4
- package/ios/Snapback4Device.xcframework/ios-arm64/libsnapback4_device.a +0 -0
- package/ios/Snapback4Device.xcframework/ios-arm64-simulator/libsnapback4_device.a +0 -0
- package/package.json +1 -1
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`
|
|
70
|
-
|
|
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.
|
|
99
|
-
|
|
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 —
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(
|
|
121
|
-
return
|
|
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(
|
|
126
|
-
finally {
|
|
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
|
|
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>
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snapback4-expo",
|
|
3
|
-
"version": "0.4.
|
|
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",
|