peachsim 0.1.1641 → 0.1.1644
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/cli/app-bundle-resources.ts +57 -0
- package/cli/commands/control.ts +12 -0
- package/cli/drivers/playwright.ts +39 -0
- package/cli/outbound-endpoints.ts +4 -2
- package/detox/motion-change.cjs +7 -5
- package/detox/motion-change.d.cts +1 -0
- package/detox/motion-leg-starts.cjs +20 -0
- package/detox/motion-leg-starts.d.cts +8 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +3 -3
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +719 -572
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +7 -6
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +656 -509
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +811 -658
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +89 -41
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +14 -6
- package/dist-lib/menu.mjs +14 -6
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +88 -41
- package/dist-lib/native-app-identity-project.cjs +90 -41
- package/dist-lib/native-app-identity-project.mjs +89 -41
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +2 -1
- package/dist-lib/sdk.mjs +2 -1
- package/dist-lib/skills.cjs +1716 -1366
- package/dist-lib/swift-compile.cjs +297 -215
- package/dist-lib/swift-compile.mjs +315 -227
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +620 -473
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/package.json +1 -1
- package/src/appBundleResourcesContract.ts +51 -0
- package/src/backend-origin.ts +3 -1
- package/src/host/bridge-host.ts +17 -11
- package/src/host/staged-host-files.ts +28 -4
- package/src/launchMetadataContract.ts +1 -0
- package/src/menu.ts +12 -4
- package/src/native-app-identity-project.ts +47 -38
- package/src/runtime-delivery.ts +6 -7
- package/src/swift-compile.ts +18 -7
- package/src/swift-compiler-digest.ts +5 -3
- package/src/swift-objc-compile.ts +34 -4
- package/src/swift-root-lock.ts +241 -92
- package/src/xcode-project.ts +67 -4
package/dist-lib/vite.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! peach v0.1.
|
|
1
|
+
/*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PeachBundleAssetReference } from './bundle-asset-resource'
|
|
2
|
+
|
|
3
|
+
// copied native resources keep their bundle-relative namespace, independently
|
|
4
|
+
// of the host URLs used to deliver their immutable bytes.
|
|
5
|
+
export type AppBundleResources = Record<string, PeachBundleAssetReference>
|
|
6
|
+
|
|
7
|
+
export function isAppBundleResourcePath(path: string): boolean {
|
|
8
|
+
return (
|
|
9
|
+
path.length > 0 &&
|
|
10
|
+
!path.includes('\0') &&
|
|
11
|
+
!/[\\?#]/.test(path) &&
|
|
12
|
+
path.split('/').every((part) => part !== '' && part !== '.' && part !== '..')
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readAppBundleResources(wire: string | null): AppBundleResources {
|
|
17
|
+
if (wire === null) return {}
|
|
18
|
+
const value: unknown = JSON.parse(wire)
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
20
|
+
throw new Error('invalid native app bundle resources')
|
|
21
|
+
const resources: AppBundleResources = Object.create(null)
|
|
22
|
+
for (const [path, resource] of Object.entries(value)) {
|
|
23
|
+
if (
|
|
24
|
+
!isAppBundleResourcePath(path) ||
|
|
25
|
+
!resource ||
|
|
26
|
+
typeof resource !== 'object' ||
|
|
27
|
+
!('uri' in resource) ||
|
|
28
|
+
typeof resource.uri !== 'string' ||
|
|
29
|
+
!('sha256' in resource) ||
|
|
30
|
+
typeof resource.sha256 !== 'string' ||
|
|
31
|
+
!/^[a-f0-9]{64}$/.test(resource.sha256) ||
|
|
32
|
+
!('contentType' in resource) ||
|
|
33
|
+
typeof resource.contentType !== 'string' ||
|
|
34
|
+
!('byteLength' in resource) ||
|
|
35
|
+
typeof resource.byteLength !== 'number' ||
|
|
36
|
+
!Number.isSafeInteger(resource.byteLength) ||
|
|
37
|
+
resource.byteLength < 0
|
|
38
|
+
)
|
|
39
|
+
throw new Error('invalid native app bundle resource')
|
|
40
|
+
const url = new URL(resource.uri)
|
|
41
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
42
|
+
throw new Error('invalid native app bundle resource URL')
|
|
43
|
+
resources[path] = {
|
|
44
|
+
uri: url.href,
|
|
45
|
+
sha256: resource.sha256,
|
|
46
|
+
contentType: resource.contentType,
|
|
47
|
+
byteLength: resource.byteLength,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return resources
|
|
51
|
+
}
|
package/src/backend-origin.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://
|
|
1
|
+
const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://peachsim.com'
|
|
2
2
|
|
|
3
3
|
export const LOOPBACK_PROBE_HOSTNAMES = ['127.0.0.1', '::1'] as const
|
|
4
4
|
|
|
@@ -115,6 +115,8 @@ export function isContrastBackendHost(hostname: string): boolean {
|
|
|
115
115
|
isLoopbackHost(normalized) ||
|
|
116
116
|
normalized === 'contrast.dev' ||
|
|
117
117
|
normalized.endsWith('.contrast.dev') ||
|
|
118
|
+
normalized === 'peachsim.com' ||
|
|
119
|
+
normalized === 'www.peachsim.com' ||
|
|
118
120
|
normalized === PEACHSIM_NATIVE_HOST
|
|
119
121
|
)
|
|
120
122
|
}
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -1530,7 +1530,21 @@ export class SootSimBridgeHost {
|
|
|
1530
1530
|
* non-upgrade routes that don't match serve index.html (SPA behavior) so
|
|
1531
1531
|
* electron's webContents can navigate freely inside the runtime. */
|
|
1532
1532
|
private handleHttpRequest(req: IncomingMessage, res: ServerResponse) {
|
|
1533
|
-
|
|
1533
|
+
const method = (req.method || 'GET').toUpperCase()
|
|
1534
|
+
let url: URL
|
|
1535
|
+
try {
|
|
1536
|
+
url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
|
|
1537
|
+
} catch {
|
|
1538
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1539
|
+
res.end('invalid request URL or host')
|
|
1540
|
+
return
|
|
1541
|
+
}
|
|
1542
|
+
const publicEngineFont =
|
|
1543
|
+
(method === 'GET' || method === 'HEAD') &&
|
|
1544
|
+
/^\/(?:runtime\/|sootsim\/)?engine\/fonts\/[^\\]*\.(ttf|otf|woff2?)$/.test(
|
|
1545
|
+
url.pathname,
|
|
1546
|
+
)
|
|
1547
|
+
if (!publicEngineFont && rejectUntrustedBridgeOrigin(req, res)) return
|
|
1534
1548
|
if (req.url === '/__peach/fetch-transport') {
|
|
1535
1549
|
res.writeHead(200, {
|
|
1536
1550
|
'Content-Type': 'application/json',
|
|
@@ -1619,16 +1633,6 @@ export class SootSimBridgeHost {
|
|
|
1619
1633
|
return
|
|
1620
1634
|
}
|
|
1621
1635
|
|
|
1622
|
-
const method = (req.method || 'GET').toUpperCase()
|
|
1623
|
-
let url: URL
|
|
1624
|
-
try {
|
|
1625
|
-
url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
|
|
1626
|
-
} catch {
|
|
1627
|
-
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
1628
|
-
res.end('invalid request URL or host')
|
|
1629
|
-
return
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
1636
|
// /__sootsim/shared-config is the one awaited HTTP write. ordinary
|
|
1633
1637
|
// settings fan out over WS, but destructive reloads need confirmation
|
|
1634
1638
|
// that the disk source of truth accepted their final reset first.
|
|
@@ -1972,6 +1976,8 @@ export class SootSimBridgeHost {
|
|
|
1972
1976
|
res.writeHead(200, {
|
|
1973
1977
|
'Content-Type': contentType,
|
|
1974
1978
|
'Cache-Control': 'no-store',
|
|
1979
|
+
// native file documents have an opaque origin when reading public fonts.
|
|
1980
|
+
...(publicEngineFont ? { 'Access-Control-Allow-Origin': '*' } : {}),
|
|
1975
1981
|
})
|
|
1976
1982
|
if (method === 'HEAD') {
|
|
1977
1983
|
res.end()
|
|
@@ -51,11 +51,21 @@ export function stageHostBytes(
|
|
|
51
51
|
origin: string,
|
|
52
52
|
extension = '',
|
|
53
53
|
): string {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const directory = prepareStagedDirectory()
|
|
55
|
+
return writeStagedBytes(bytes, origin, extension, directory)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// a resource pack prunes once, rather than scanning the cache for every file.
|
|
59
|
+
export function stageHostByteFiles(
|
|
60
|
+
files: readonly Uint8Array[],
|
|
61
|
+
origin: string,
|
|
62
|
+
): string[] {
|
|
63
|
+
const directory = prepareStagedDirectory()
|
|
64
|
+
return files.map((bytes) => writeStagedBytes(bytes, origin, '', directory))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function prepareStagedDirectory(): string {
|
|
57
68
|
const directory = stagedDir()
|
|
58
|
-
const destination = path.join(directory, name)
|
|
59
69
|
fs.mkdirSync(directory, { recursive: true })
|
|
60
70
|
const now = Date.now()
|
|
61
71
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
@@ -63,6 +73,20 @@ export function stageHostBytes(
|
|
|
63
73
|
if (entry.isFile() && now - fs.statSync(stale).mtimeMs > STAGED_RETENTION_MS)
|
|
64
74
|
fs.rmSync(stale, { force: true })
|
|
65
75
|
}
|
|
76
|
+
return directory
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeStagedBytes(
|
|
80
|
+
bytes: Uint8Array,
|
|
81
|
+
origin: string,
|
|
82
|
+
extension: string,
|
|
83
|
+
directory: string,
|
|
84
|
+
): string {
|
|
85
|
+
const digest = createHash('sha256').update(bytes).digest('hex')
|
|
86
|
+
extension = extension.toLowerCase()
|
|
87
|
+
const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
|
|
88
|
+
const destination = path.join(directory, name)
|
|
89
|
+
const now = Date.now()
|
|
66
90
|
if (fs.existsSync(destination)) {
|
|
67
91
|
fs.utimesSync(destination, new Date(now), new Date(now))
|
|
68
92
|
} else {
|
package/src/menu.ts
CHANGED
|
@@ -71,7 +71,13 @@ export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, s
|
|
|
71
71
|
// engine's wireframe store draws. 'wireframe' is the clean structural sketch;
|
|
72
72
|
// the others are its screen-space styles that ship as named effects.
|
|
73
73
|
export type SootSimScreenEffect = 'none' | 'wireframe' | 'blueprint' | 'panel' | 'green'
|
|
74
|
-
export const SOOTSIM_SCREEN_EFFECTS = [
|
|
74
|
+
export const SOOTSIM_SCREEN_EFFECTS = [
|
|
75
|
+
'none',
|
|
76
|
+
'wireframe',
|
|
77
|
+
'blueprint',
|
|
78
|
+
'panel',
|
|
79
|
+
'green',
|
|
80
|
+
] as const
|
|
75
81
|
|
|
76
82
|
export function isSootSimScreenEffect(value: unknown): value is SootSimScreenEffect {
|
|
77
83
|
return (
|
|
@@ -373,9 +379,9 @@ function deviceMenu(
|
|
|
373
379
|
},
|
|
374
380
|
{ type: 'separator' },
|
|
375
381
|
{ label: 'Trigger Screenshot', action: 'screenshot' },
|
|
376
|
-
// pose controls for the current device: open/close on
|
|
377
|
-
// rotate left/right wherever a turn changes geometry, the way
|
|
378
|
-
// simulator holds them (rotate left is cmd+left, rotate right
|
|
382
|
+
// pose controls for the current device: open/close, book and laptop on
|
|
383
|
+
// foldables, rotate left/right wherever a turn changes geometry, the way
|
|
384
|
+
// ios simulator holds them (rotate left is cmd+left, rotate right
|
|
379
385
|
// cmd+right). truthiness, not !== null: a partial state must hide
|
|
380
386
|
// these, never show duo items for the wrong device.
|
|
381
387
|
...(state.devicePosture || state.canRotate
|
|
@@ -390,6 +396,8 @@ function deviceMenu(
|
|
|
390
396
|
: 'Open iPhone Duo',
|
|
391
397
|
action: 'toggle-duo-posture',
|
|
392
398
|
},
|
|
399
|
+
{ label: 'Book Pose', action: 'fold-duo-book' },
|
|
400
|
+
{ label: 'Laptop Pose', action: 'fold-duo-laptop' },
|
|
393
401
|
]
|
|
394
402
|
: []),
|
|
395
403
|
...(state.canRotate
|
|
@@ -326,44 +326,8 @@ function readIosNativeAppIdentity(
|
|
|
326
326
|
parseInfoPlist: (source: string) => unknown,
|
|
327
327
|
iosTarget: string | undefined,
|
|
328
328
|
): NativeAppIdentity | null {
|
|
329
|
-
const
|
|
330
|
-
if (!
|
|
331
|
-
const projects = fs
|
|
332
|
-
.readdirSync(iosRoot, { withFileTypes: true })
|
|
333
|
-
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
|
|
334
|
-
if (projects.length === 0) return null
|
|
335
|
-
if (projects.length > 1 && !iosTarget) {
|
|
336
|
-
throw new NativeAppTargetSelectionError(
|
|
337
|
-
`native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
|
|
338
|
-
)
|
|
339
|
-
}
|
|
340
|
-
let project = projects[0]!
|
|
341
|
-
if (iosTarget && projects.length > 1) {
|
|
342
|
-
const matches = projects.filter((entry) =>
|
|
343
|
-
listXcodeAppTargets(path.join(iosRoot, entry.name), {
|
|
344
|
-
requireSources: false,
|
|
345
|
-
}).includes(iosTarget),
|
|
346
|
-
)
|
|
347
|
-
if (matches.length === 0) {
|
|
348
|
-
throw new Error(`native app identity has no application target ${iosTarget}`)
|
|
349
|
-
}
|
|
350
|
-
if (matches.length > 1) {
|
|
351
|
-
throw new Error(
|
|
352
|
-
`native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
project = matches[0]!
|
|
356
|
-
}
|
|
357
|
-
const xcodeproj = path.join(iosRoot, project.name)
|
|
358
|
-
const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
|
|
359
|
-
if (!iosTarget && appTargets.length > 1) {
|
|
360
|
-
throw new NativeAppTargetSelectionError(
|
|
361
|
-
`native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
|
|
362
|
-
)
|
|
363
|
-
}
|
|
364
|
-
const app = readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
|
|
365
|
-
requireSources: false,
|
|
366
|
-
})
|
|
329
|
+
const app = readIosAppTarget(projectRoot, iosTarget)
|
|
330
|
+
if (!app) return null
|
|
367
331
|
const identity = readNativeAppIdentityFromXcodeTarget(app, parseInfoPlist)
|
|
368
332
|
const projectManifestPath = path.join(projectRoot, 'package.json')
|
|
369
333
|
const projectRequire = createRequire(projectManifestPath)
|
|
@@ -422,3 +386,48 @@ function readIosNativeAppIdentity(
|
|
|
422
386
|
}
|
|
423
387
|
return { ...identity, reanimatedStaticFeatureFlags: flags }
|
|
424
388
|
}
|
|
389
|
+
|
|
390
|
+
// native identity and copied resources must select the same application target.
|
|
391
|
+
export function readIosAppTarget(
|
|
392
|
+
projectRoot: string,
|
|
393
|
+
iosTarget?: string,
|
|
394
|
+
): XcodeAppTarget | null {
|
|
395
|
+
const iosRoot = path.join(projectRoot, 'ios')
|
|
396
|
+
if (!fs.existsSync(iosRoot)) return null
|
|
397
|
+
const projects = fs
|
|
398
|
+
.readdirSync(iosRoot, { withFileTypes: true })
|
|
399
|
+
.filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
|
|
400
|
+
if (projects.length === 0) return null
|
|
401
|
+
if (projects.length > 1 && !iosTarget) {
|
|
402
|
+
throw new NativeAppTargetSelectionError(
|
|
403
|
+
`native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
let project = projects[0]!
|
|
407
|
+
if (iosTarget && projects.length > 1) {
|
|
408
|
+
const matches = projects.filter((entry) =>
|
|
409
|
+
listXcodeAppTargets(path.join(iosRoot, entry.name), {
|
|
410
|
+
requireSources: false,
|
|
411
|
+
}).includes(iosTarget),
|
|
412
|
+
)
|
|
413
|
+
if (matches.length === 0) {
|
|
414
|
+
throw new Error(`native app identity has no application target ${iosTarget}`)
|
|
415
|
+
}
|
|
416
|
+
if (matches.length > 1) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
project = matches[0]!
|
|
422
|
+
}
|
|
423
|
+
const xcodeproj = path.join(iosRoot, project.name)
|
|
424
|
+
const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
|
|
425
|
+
if (!iosTarget && appTargets.length > 1) {
|
|
426
|
+
throw new NativeAppTargetSelectionError(
|
|
427
|
+
`native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
return readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
|
|
431
|
+
requireSources: false,
|
|
432
|
+
})
|
|
433
|
+
}
|
package/src/runtime-delivery.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the machinery itself (hosted manifest, sha256-verified tarballs, channels,
|
|
4
4
|
// channel resolution, and auto-update) lives in @contrast/runtime-delivery and
|
|
5
5
|
// knows nothing about peach. this file supplies peach's identity, public env
|
|
6
|
-
// overrides, and the
|
|
6
|
+
// overrides, and the peachsim.com/runtimes hosting layout.
|
|
7
7
|
//
|
|
8
8
|
// @contrast/runtime-delivery is an internal workspace package that is never
|
|
9
9
|
// published, so both the CLI bundle and dist-lib inline it (see the `alias`
|
|
@@ -20,14 +20,13 @@ import {
|
|
|
20
20
|
runtimesDir,
|
|
21
21
|
writeActiveRuntime,
|
|
22
22
|
} from './home-paths.ts'
|
|
23
|
-
import {
|
|
23
|
+
import { peachPublicBrand } from './public-brand.ts'
|
|
24
24
|
|
|
25
|
-
export const DEFAULT_RUNTIME_CDN_ORIGIN =
|
|
25
|
+
export const DEFAULT_RUNTIME_CDN_ORIGIN = peachPublicBrand.origin
|
|
26
26
|
|
|
27
|
-
/** the official curated changelog and the machine-readable release feed.
|
|
28
|
-
|
|
29
|
-
export const
|
|
30
|
-
export const RNX_CHANGELOG_API_URL = 'https://contrast.dev/api/changelog/sootsim'
|
|
27
|
+
/** the official curated changelog and the machine-readable release feed. */
|
|
28
|
+
export const RNX_CHANGELOG_URL = `${peachPublicBrand.origin}/changelog`
|
|
29
|
+
export const RNX_CHANGELOG_API_URL = `${peachPublicBrand.origin}/api/changelog/sootsim`
|
|
31
30
|
|
|
32
31
|
export const RNX_RUNTIME_PRODUCT: RuntimeProduct = {
|
|
33
32
|
id: 'peach',
|
package/src/swift-compile.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
spawn,
|
|
3
|
+
execFileSync,
|
|
4
|
+
type ExecFileOptions,
|
|
5
|
+
type SpawnOptions,
|
|
6
|
+
} from 'node:child_process'
|
|
2
7
|
import { createHash } from 'node:crypto'
|
|
3
8
|
import {
|
|
4
9
|
cpSync,
|
|
@@ -33,7 +38,7 @@ import {
|
|
|
33
38
|
swiftTargetModuleName,
|
|
34
39
|
type SwiftPackageTarget,
|
|
35
40
|
} from './swift-package'
|
|
36
|
-
import { withSwiftRootLock } from './swift-root-lock'
|
|
41
|
+
import { withSwiftRootLock, type SwiftRootLease } from './swift-root-lock'
|
|
37
42
|
import { swiftEnvironment, swiftSdkOptions, type SwiftWasmSdk } from './swift-toolchain'
|
|
38
43
|
import { isSwiftResourceBundles } from './swiftContract'
|
|
39
44
|
import type { SwiftImageUpload } from './swift-submit'
|
|
@@ -43,24 +48,29 @@ export { swiftIcuAsset, type SwiftIcuAsset }
|
|
|
43
48
|
|
|
44
49
|
// compiler drivers own descendants such as admission and swiftpm workers.
|
|
45
50
|
// cancellation waits for their pipes to close before releasing the build root.
|
|
51
|
+
// a `lease` records the group under the root's lock, so the root stays held
|
|
52
|
+
// while the compiler lives even if this process does not.
|
|
46
53
|
// `stream` echoes the output as it arrives: all of it, or only matching lines.
|
|
47
54
|
// a successful run then echoes the rest minus lines matching `quiet`, its
|
|
48
55
|
// warnings; a failed run's output goes to the caller's one diagnostic.
|
|
49
56
|
async function runCompiler(
|
|
50
57
|
command: string,
|
|
51
58
|
args: string[],
|
|
52
|
-
options: ExecFileOptions & { signal?: AbortSignal },
|
|
59
|
+
options: ExecFileOptions & { signal?: AbortSignal; lease?: SwiftRootLease },
|
|
53
60
|
stream: boolean | RegExp = false,
|
|
54
61
|
quiet?: RegExp,
|
|
55
62
|
): Promise<void> {
|
|
56
63
|
const { signal } = options
|
|
57
64
|
signal?.throwIfAborted()
|
|
58
|
-
const
|
|
65
|
+
const launch = {
|
|
59
66
|
cwd: options.cwd,
|
|
60
67
|
env: swiftEnvironment(options.env),
|
|
61
68
|
detached: true,
|
|
62
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
-
}
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
} satisfies SpawnOptions
|
|
71
|
+
const child = options.lease
|
|
72
|
+
? options.lease.spawn(command, args, launch)
|
|
73
|
+
: spawn(command, args, launch)
|
|
64
74
|
let stopping: Promise<void> | undefined
|
|
65
75
|
try {
|
|
66
76
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -6242,7 +6252,7 @@ export function compileSwiftProjectUnit(
|
|
|
6242
6252
|
// using it, so one process writes, builds and reads them at a time.
|
|
6243
6253
|
return withSwiftRootLock(
|
|
6244
6254
|
root,
|
|
6245
|
-
async () => {
|
|
6255
|
+
async (lease) => {
|
|
6246
6256
|
// a process that held the root while this one waited may have built
|
|
6247
6257
|
// this exact address.
|
|
6248
6258
|
const built = readCompiledArtifact(path.join(sharedRoot, 'artifact'), hash)
|
|
@@ -6314,6 +6324,7 @@ export function compileSwiftProjectUnit(
|
|
|
6314
6324
|
maxBuffer: 64 * 1024 * 1024,
|
|
6315
6325
|
timeout: options.timeoutMs,
|
|
6316
6326
|
signal: options.signal,
|
|
6327
|
+
lease,
|
|
6317
6328
|
}
|
|
6318
6329
|
report('compiling and linking Swift modules')
|
|
6319
6330
|
try {
|
|
@@ -3,11 +3,13 @@ import { readFileSync } from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
// the lowering and manifest code in swift-compile.ts and its design-time lowering
|
|
6
|
-
// decide the emitted output,
|
|
7
|
-
//
|
|
6
|
+
// decide the emitted output, and the root lock decides whose sources and
|
|
7
|
+
// executable an address names, so their text addresses every swift artifact.
|
|
8
|
+
// bundled builds cannot read these files at runtime, so build-cli injects this
|
|
9
|
+
// digest.
|
|
8
10
|
export function swiftCompilerSourceDigest(srcDir: string): string {
|
|
9
11
|
const hash = createHash('sha256')
|
|
10
|
-
for (const file of ['swift-compile.ts', 'swift-design-time.ts']) {
|
|
12
|
+
for (const file of ['swift-compile.ts', 'swift-design-time.ts', 'swift-root-lock.ts']) {
|
|
11
13
|
hash.update(readFileSync(path.join(srcDir, file)))
|
|
12
14
|
}
|
|
13
15
|
return hash.digest('hex')
|
|
@@ -37,6 +37,19 @@ export function compileXcodeObjCImplementations(
|
|
|
37
37
|
)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// a link's target, or null when the target is proven absent; other failures surface.
|
|
41
|
+
function linkedStat(file: string): fs.Stats | null {
|
|
42
|
+
try {
|
|
43
|
+
return fs.statSync(file)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null
|
|
46
|
+
throw error
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isSdkInclude = (file: string): boolean =>
|
|
51
|
+
file === '/usr/include' || file.startsWith('/usr/include/')
|
|
52
|
+
|
|
40
53
|
/** prepares genuine objects without authorizing unsupported framework linkage. */
|
|
41
54
|
export function prepareXcodeObjCImplementations(
|
|
42
55
|
app: XcodeAppTarget,
|
|
@@ -63,13 +76,24 @@ export function prepareXcodeObjCImplementations(
|
|
|
63
76
|
),
|
|
64
77
|
)
|
|
65
78
|
}
|
|
79
|
+
const walked = new Set<string>()
|
|
80
|
+
const ancestors = new Set<string>()
|
|
66
81
|
const addHeaders = (directory: string): void => {
|
|
82
|
+
// a versioned framework links Headers to a real directory: stage each path it is reached by, once, never around a link cycle.
|
|
83
|
+
const real = fs.realpathSync(directory)
|
|
84
|
+
if (walked.has(directory) || ancestors.has(real)) return
|
|
85
|
+
walked.add(directory)
|
|
86
|
+
ancestors.add(real)
|
|
67
87
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
68
|
-
if (entry.
|
|
88
|
+
if (entry.name.startsWith('.') && !entry.isFile()) continue
|
|
69
89
|
const file = path.join(directory, entry.name)
|
|
70
|
-
|
|
90
|
+
// a link to nothing is never opened by the compiler: a header that is needed fails there as not found.
|
|
91
|
+
const linked = entry.isSymbolicLink() ? linkedStat(file) : null
|
|
92
|
+
if (entry.isSymbolicLink() && !linked) continue
|
|
93
|
+
if (entry.isDirectory() || linked?.isDirectory()) addHeaders(file)
|
|
71
94
|
else if (/\.(h|hh|hpp|hxx|inc|inl|modulemap)$/.test(entry.name)) add(file, true)
|
|
72
95
|
}
|
|
96
|
+
ancestors.delete(real)
|
|
73
97
|
}
|
|
74
98
|
const targets: {
|
|
75
99
|
name: string
|
|
@@ -152,6 +176,12 @@ export function prepareXcodeObjCImplementations(
|
|
|
152
176
|
),
|
|
153
177
|
])
|
|
154
178
|
add(file, true)
|
|
179
|
+
// search paths name directories of headers the target includes; /usr/include maps to the wasm sysroot.
|
|
180
|
+
for (const directory of [
|
|
181
|
+
...target.objc.headerSearchPaths,
|
|
182
|
+
...target.objc.frameworkSearchPaths,
|
|
183
|
+
])
|
|
184
|
+
if (!isSdkInclude(directory) && fs.existsSync(directory)) addHeaders(directory)
|
|
155
185
|
targets.push({
|
|
156
186
|
name: target.moduleName,
|
|
157
187
|
deploymentTarget: target.iosDeploymentTarget,
|
|
@@ -290,8 +320,8 @@ export function prepareXcodeObjCImplementations(
|
|
|
290
320
|
return path.join(work, name, relative)
|
|
291
321
|
}
|
|
292
322
|
// native SDK include paths select the wasm SDK, never host system headers.
|
|
293
|
-
if (file
|
|
294
|
-
return path.join(sdk.sysroot, 'include', file.slice('/usr/include
|
|
323
|
+
if (isSdkInclude(file))
|
|
324
|
+
return path.join(sdk.sysroot, 'include', file.slice('/usr/include'.length))
|
|
295
325
|
throw new Error(
|
|
296
326
|
`Objective-C compiler input is outside the project and its pinned packages: ${file}`,
|
|
297
327
|
)
|