peachsim 0.1.2198 → 0.1.2200

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 (135) hide show
  1. package/cli/commands/inspect/settling.ts +20 -20
  2. package/cli/outbound-endpoints.ts +12 -0
  3. package/dist-lib/agent-daemon-client.cjs +1 -1
  4. package/dist-lib/agent-events.cjs +1 -1
  5. package/dist-lib/attached-projects.cjs +1 -1
  6. package/dist-lib/auth/shared-session.cjs +1 -1
  7. package/dist-lib/backend-origin.cjs +1 -1
  8. package/dist-lib/beta.cjs +1 -1
  9. package/dist-lib/beta.mjs +1 -1
  10. package/dist-lib/bridge-constants.cjs +1 -1
  11. package/dist-lib/bridge-contract-input.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.mjs +1 -1
  13. package/dist-lib/bridge-contract.cjs +1 -1
  14. package/dist-lib/bridge-contract.mjs +1 -1
  15. package/dist-lib/bundle-channel.cjs +1532 -1170
  16. package/dist-lib/capture-contract.cjs +1 -1
  17. package/dist-lib/capture-contract.mjs +1 -1
  18. package/dist-lib/cloud-contract.cjs +1 -1
  19. package/dist-lib/cloud-contract.mjs +1 -1
  20. package/dist-lib/cloud-test-args.cjs +1 -1
  21. package/dist-lib/cloud-test-args.mjs +1 -1
  22. package/dist-lib/cloud.cjs +1 -1
  23. package/dist-lib/cloud.mjs +1 -1
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/config.mjs +1 -1
  26. package/dist-lib/detox/index.cjs +22 -22
  27. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  28. package/dist-lib/expo-swift-bundle.cjs +45875 -39899
  29. package/dist-lib/expo-swift-metadata.cjs +1 -1
  30. package/dist-lib/expo-swift-metadata.mjs +1 -1
  31. package/dist-lib/home-paths.cjs +1 -1
  32. package/dist-lib/host/bridge-host.cjs +50942 -49780
  33. package/dist-lib/host/fetch-proxy-handler.cjs +16 -4
  34. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  36. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  37. package/dist-lib/host/websocket-proxy.cjs +1 -1
  38. package/dist-lib/index.cjs +36 -24
  39. package/dist-lib/jump-to-source-babel.cjs +1 -1
  40. package/dist-lib/jump-to-source-native.cjs +1 -1
  41. package/dist-lib/kotlin-host.cjs +100 -10
  42. package/dist-lib/menu.cjs +1 -1
  43. package/dist-lib/menu.mjs +1 -1
  44. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  46. package/dist-lib/metro-production-bundle.cjs +1 -1
  47. package/dist-lib/metro-production-bundle.mjs +1 -1
  48. package/dist-lib/metro.cjs +16 -4
  49. package/dist-lib/native-app-identity-project.cjs +14 -2
  50. package/dist-lib/native-app-identity-project.mjs +14 -2
  51. package/dist-lib/native-app-identity.cjs +1 -1
  52. package/dist-lib/native-app-identity.mjs +1 -1
  53. package/dist-lib/native-firebase-metadata.cjs +1 -1
  54. package/dist-lib/native-firebase-metadata.mjs +1 -1
  55. package/dist-lib/pdfium-raster.cjs +1 -1
  56. package/dist-lib/pdfium-raster.mjs +1 -1
  57. package/dist-lib/plist.cjs +1 -1
  58. package/dist-lib/plist.mjs +1 -1
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/public-origins-lib-esm.json +24 -24
  62. package/dist-lib/public-origins-lib.json +302 -289
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/reap-browser.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +21 -21
  68. package/dist-lib/sdk.mjs +21 -21
  69. package/dist-lib/skills.cjs +3622 -2962
  70. package/dist-lib/swift-compile.cjs +34 -3
  71. package/dist-lib/swift-compile.mjs +34 -3
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift-toolchain.cjs +1 -1
  77. package/dist-lib/swift.cjs +1 -1
  78. package/dist-lib/vite-swift.cjs +129 -43
  79. package/dist-lib/vite.cjs +16 -4
  80. package/dist-lib/worklet-executable.cjs +1 -1
  81. package/dist-lib/worklet-executable.mjs +1 -1
  82. package/dist-types/packages/peach-engine/src/engine/scene-table.d.ts +2 -0
  83. package/dist-types/packages/peach-engine/src/render-worker/app-prefetch-contract.d.ts +5 -0
  84. package/dist-types/packages/peach-engine/src/render-worker/protocol.d.ts +2 -0
  85. package/dist-types/packages/peach-engine/src/render-worker/tree-sync-port.d.ts +5 -0
  86. package/kotlin/sdk.tar.gz +0 -0
  87. package/package.json +1 -1
  88. package/runtime-companion.json +1 -1
  89. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h +2 -0
  90. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSBundle.h +1 -1
  91. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSByteCountFormatter.h +1 -1
  92. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSCharacterSet.h +1 -1
  93. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSCompoundPredicate.h +1 -1
  94. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSDateIntervalFormatter.h +1 -1
  95. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSDecimalNumber.h +1 -1
  96. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSEnergyFormatter.h +1 -1
  97. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSExpression.h +1 -1
  98. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSFileHandle.h +2 -2
  99. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSISO8601DateFormatter.h +1 -1
  100. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSIndexSet.h +1 -1
  101. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSLengthFormatter.h +1 -1
  102. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSLock.h +3 -3
  103. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSMassFormatter.h +1 -1
  104. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSMeasurementFormatter.h +1 -1
  105. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSNotification.h +1 -1
  106. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSNotificationQueue.h +1 -1
  107. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSObjCRuntime.h +4 -0
  108. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSOrthography.h +1 -1
  109. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSScanner.h +1 -1
  110. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSThread.h +1 -1
  111. package/sdk/ios/System/Library/Frameworks/Foundation.framework/Headers/NSUnit.h +25 -25
  112. package/sdk/ios/System/Library/Frameworks/QuartzCore.framework/Headers/CAValueFunction.h +1 -1
  113. package/sdk/ios/System/Library/Frameworks/QuartzCore.framework/Headers/CoreAnimation.h +1 -0
  114. package/sdk/ios/System/Library/Frameworks/QuartzCore.framework/Modules/module.modulemap +1 -0
  115. package/sdk/ios/System/Library/Frameworks/UIKitText.framework/Headers/UIKitText.apinotes +14 -0
  116. package/src/android-layout-ids.ts +50 -0
  117. package/src/android-view-classes.ts +41 -0
  118. package/src/expo-native-workspace.ts +2 -3
  119. package/src/expo-swift-host.ts +6 -1
  120. package/src/java-module-abi.ts +2 -0
  121. package/src/javaAbiContract.ts +2 -0
  122. package/src/kotlin-app.ts +33 -7
  123. package/src/kotlin-host.ts +83 -6
  124. package/src/kotlin-maven-sources.ts +248 -0
  125. package/src/native-prebuilt.ts +36 -2
  126. package/src/native-swift-slices.ts +43 -2
  127. package/src/react-native-bridge-modules.ts +7 -1
  128. package/src/react-native-core-build.ts +176 -0
  129. package/src/react-native-core-digest.ts +1 -1
  130. package/src/react-native-core.ts +24 -7
  131. package/src/swift-packages.ts +3 -0
  132. package/src/swift-standin-emit.ts +9 -1
  133. package/src/swift-standins.ts +5 -2
  134. package/src/swift-wasm-prune.ts +41 -3
  135. package/src/turbo-compile.ts +2 -1
@@ -0,0 +1,248 @@
1
+ // a dependency outside Peach's pinned closure compiles from the sources jar
2
+ // the app's Maven repositories publish for that coordinate
3
+ import { createHash } from 'node:crypto'
4
+ import fs from 'node:fs'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ import { inflateRawSync } from 'node:zlib'
8
+
9
+ const pending = new Map<string, Promise<string[]>>()
10
+
11
+ function braceBlock(text: string, from: number): { body: string; end: number } | null {
12
+ const open = text.indexOf('{', from)
13
+ if (open < 0) return null
14
+ let depth = 0
15
+ for (let index = open; index < text.length; index++) {
16
+ const char = text[index]
17
+ if (char === '{') depth++
18
+ else if (char === '}') {
19
+ depth--
20
+ if (depth === 0) return { body: text.slice(open + 1, index), end: index }
21
+ }
22
+ }
23
+ return null
24
+ }
25
+
26
+ function repositoryRoot(value: string, where: string): string {
27
+ if (value.startsWith('https://')) return value.replace(/\/+$/, '')
28
+ if (value.startsWith('/')) return value.replace(/\/+$/, '')
29
+ if (value.startsWith('file:')) {
30
+ const pathname = new URL(value).pathname
31
+ if (!pathname.startsWith('/'))
32
+ throw new Error(`${where}: Maven repository ${value} is not a local path`)
33
+ return decodeURIComponent(pathname).replace(/\/+$/, '')
34
+ }
35
+ throw new Error(
36
+ `${where}: Maven repository ${value} is not an https URL or a local path`,
37
+ )
38
+ }
39
+
40
+ // google(), mavenCentral(), mavenLocal(), or one literal maven URL
41
+ function withoutComments(text: string): string {
42
+ let out = ''
43
+ for (let index = 0; index < text.length; index++) {
44
+ const char = text[index]
45
+ const next = text[index + 1]
46
+ if (char === '"' || char === "'") {
47
+ const quote = char
48
+ out += char
49
+ index++
50
+ while (index < text.length && text[index] !== quote) {
51
+ out += text[index]
52
+ if (text[index] === '\\') {
53
+ index++
54
+ out += text[index] ?? ''
55
+ }
56
+ index++
57
+ }
58
+ out += text[index] ?? ''
59
+ continue
60
+ }
61
+ if (char === '/' && next === '/') {
62
+ while (index < text.length && text[index] !== '\n') index++
63
+ out += '\n'
64
+ continue
65
+ }
66
+ if (char === '/' && next === '*') {
67
+ index += 2
68
+ while (index < text.length && !(text[index] === '*' && text[index + 1] === '/'))
69
+ index++
70
+ index++
71
+ continue
72
+ }
73
+ out += char
74
+ }
75
+ return out
76
+ }
77
+
78
+ function repositories(body: string, where: string): string[] {
79
+ const stripped = withoutComments(body)
80
+ const roots: string[] = []
81
+ const leftover = stripped.replace(
82
+ /\b(google|mavenCentral|mavenLocal)\s*\(\s*\)|\bmaven\s*(?:\(\s*["']([^"']+)["']\s*\)|\{([^{}]*)\})/g,
83
+ (match, name: string, direct: string | undefined, block: string | undefined) => {
84
+ if (name === 'google') roots.push('https://dl.google.com/dl/android/maven2')
85
+ else if (name === 'mavenCentral') roots.push('https://repo1.maven.org/maven2')
86
+ else if (name === 'mavenLocal')
87
+ roots.push(path.join(os.homedir(), '.m2', 'repository'))
88
+ else {
89
+ const declared = direct ?? /["']([^"']+)["']/.exec(block ?? '')?.[1]
90
+ if (!declared)
91
+ throw new Error(`${where}: ${match.trim()} declares no repository URL`)
92
+ roots.push(repositoryRoot(declared, where))
93
+ }
94
+ return ''
95
+ },
96
+ )
97
+ if (leftover.replace(/[\s;]/g, ''))
98
+ throw new Error(`${where}: ${leftover.trim()} is not a literal Maven repository`)
99
+ return roots
100
+ }
101
+
102
+ function mavenRepositories(projectDir: string, where: string): string[] {
103
+ const file = ['settings.gradle.kts', 'settings.gradle']
104
+ .map((name) => path.join(projectDir, name))
105
+ .find((candidate) => fs.existsSync(candidate))
106
+ if (!file) throw new Error(`${where}: ${projectDir} has no settings.gradle`)
107
+ const text = fs.readFileSync(file, 'utf8')
108
+ const management = text.search(/\bdependencyResolutionManagement\b/)
109
+ if (management < 0)
110
+ throw new Error(`${where}: ${file} declares no dependency repositories`)
111
+ const managed = braceBlock(text, management)
112
+ if (!managed)
113
+ throw new Error(`${where}: ${file} dependencyResolutionManagement block never closes`)
114
+ const repositoriesAt = managed.body.search(/\brepositories\b/)
115
+ if (repositoriesAt < 0)
116
+ throw new Error(`${where}: ${file} declares no dependency repositories`)
117
+ const block = braceBlock(managed.body, repositoriesAt)
118
+ if (!block) throw new Error(`${where}: ${file} repositories block never closes`)
119
+ const roots = repositories(block.body, `${where}: ${file}`)
120
+ if (roots.length === 0)
121
+ throw new Error(`${where}: ${file} declares no dependency repositories`)
122
+ return roots
123
+ }
124
+
125
+ function zipSources(bytes: Buffer, where: string): { name: string; data: Buffer }[] {
126
+ let end = -1
127
+ const earliest = Math.max(0, bytes.length - 22 - 0xffff)
128
+ for (let index = bytes.length - 22; index >= earliest; index--) {
129
+ if (bytes.readUInt32LE(index) === 0x06054b50) {
130
+ end = index
131
+ break
132
+ }
133
+ }
134
+ if (end < 0) throw new Error(`${where}: sources jar is not a zip`)
135
+ const count = bytes.readUInt16LE(end + 8)
136
+ if (count !== bytes.readUInt16LE(end + 10))
137
+ throw new Error(`${where}: sources jar spans zip disks`)
138
+ let offset = bytes.readUInt32LE(end + 16)
139
+ if (offset === 0xffffffff) throw new Error(`${where}: sources jar is zip64`)
140
+ const entries: { name: string; data: Buffer }[] = []
141
+ for (let index = 0; index < count; index++) {
142
+ if (bytes.readUInt32LE(offset) !== 0x02014b50)
143
+ throw new Error(`${where}: sources jar has a broken central directory`)
144
+ const flags = bytes.readUInt16LE(offset + 8)
145
+ const method = bytes.readUInt16LE(offset + 10)
146
+ const compressed = bytes.readUInt32LE(offset + 20)
147
+ const nameLength = bytes.readUInt16LE(offset + 28)
148
+ const extraLength = bytes.readUInt16LE(offset + 30)
149
+ const commentLength = bytes.readUInt16LE(offset + 32)
150
+ const localOffset = bytes.readUInt32LE(offset + 42)
151
+ const name = bytes.toString('utf8', offset + 46, offset + 46 + nameLength)
152
+ offset += 46 + nameLength + extraLength + commentLength
153
+ if (name.endsWith('/')) continue
154
+ if (flags & 1) throw new Error(`${where}: ${name} is encrypted`)
155
+ if (name.startsWith('/') || name.split('/').includes('..'))
156
+ throw new Error(`${where}: ${name} escapes the sources jar`)
157
+ if (bytes.readUInt32LE(localOffset) !== 0x04034b50)
158
+ throw new Error(`${where}: ${name} has no local header`)
159
+ const localName = bytes.readUInt16LE(localOffset + 26)
160
+ const localExtra = bytes.readUInt16LE(localOffset + 28)
161
+ const start = localOffset + 30 + localName + localExtra
162
+ const payload = bytes.subarray(start, start + compressed)
163
+ const data =
164
+ method === 0 ? Buffer.from(payload) : method === 8 ? inflateRawSync(payload) : null
165
+ if (!data) throw new Error(`${where}: ${name} uses zip method ${method}`)
166
+ entries.push({ name, data })
167
+ }
168
+ return entries
169
+ }
170
+
171
+ function extract(bytes: Buffer, where: string): Promise<string[]> {
172
+ const key = createHash('sha256').update(bytes).digest('hex')
173
+ const cached = pending.get(key)
174
+ if (cached) return cached
175
+ const request = (async () => {
176
+ const dest = path.join(os.tmpdir(), 'peach-maven-sources', key)
177
+ const marker = path.join(dest, '.complete')
178
+ if (!fs.existsSync(marker)) {
179
+ const entries = zipSources(bytes, where)
180
+ const java = entries.find((entry) => entry.name.endsWith('.java'))
181
+ if (java) throw new Error(`${where}: ${java.name} is a Java source`)
182
+ const kotlin = entries.filter((entry) => entry.name.endsWith('.kt'))
183
+ if (kotlin.length === 0) throw new Error(`${where}: sources jar has no Kotlin`)
184
+ const staging = `${dest}.partial-${process.pid}`
185
+ fs.rmSync(staging, { recursive: true, force: true })
186
+ for (const entry of kotlin) {
187
+ const file = path.join(staging, entry.name)
188
+ fs.mkdirSync(path.dirname(file), { recursive: true })
189
+ fs.writeFileSync(file, entry.data)
190
+ }
191
+ fs.writeFileSync(path.join(staging, '.complete'), '')
192
+ try {
193
+ fs.renameSync(staging, dest)
194
+ } catch (error) {
195
+ fs.rmSync(staging, { recursive: true, force: true })
196
+ if (!fs.existsSync(marker)) throw error
197
+ }
198
+ }
199
+ return fs
200
+ .readdirSync(dest, { recursive: true })
201
+ .flatMap((entry) => {
202
+ const name = String(entry)
203
+ return name.endsWith('.kt') ? [path.join(dest, name)] : []
204
+ })
205
+ .sort()
206
+ })().catch((error: unknown) => {
207
+ pending.delete(key)
208
+ throw error
209
+ })
210
+ pending.set(key, request)
211
+ return request
212
+ }
213
+
214
+ async function sourcesJar(
215
+ root: string,
216
+ group: string,
217
+ artifact: string,
218
+ version: string,
219
+ ): Promise<Buffer | null> {
220
+ const parts = [
221
+ ...group.split('.'),
222
+ artifact,
223
+ version,
224
+ `${artifact}-${version}-sources.jar`,
225
+ ]
226
+ if (root.startsWith('https://')) {
227
+ const url = `${root}/${parts.join('/')}`
228
+ const response = await fetch(url)
229
+ if (response.status === 404) return null
230
+ if (!response.ok) throw new Error(`${url} answered ${response.status}`)
231
+ return Buffer.from(await response.arrayBuffer())
232
+ }
233
+ const file = path.join(root, ...parts)
234
+ return fs.existsSync(file) ? fs.readFileSync(file) : null
235
+ }
236
+
237
+ export async function mavenKotlinSources(
238
+ projectDir: string,
239
+ coordinate: { group: string; artifact: string; version: string },
240
+ where: string,
241
+ ): Promise<string[] | null> {
242
+ const { group, artifact, version } = coordinate
243
+ for (const root of mavenRepositories(projectDir, where)) {
244
+ const jar = await sourcesJar(root, group, artifact, version)
245
+ if (jar) return extract(jar, `${where}: ${group}:${artifact}:${version}`)
246
+ }
247
+ return null
248
+ }
@@ -58,7 +58,7 @@ export function nativeEntryKey(inputs: Record<string, string>): string {
58
58
  export const nativeEntryPrefix = (kind: NativeEntryKind, name: string, key: string) =>
59
59
  `native/${kind}/${name}/${key}`
60
60
 
61
- const entryCache = (kind: NativeEntryKind, name: string, key: string) =>
61
+ export const nativeEntryCache = (kind: NativeEntryKind, name: string, key: string) =>
62
62
  path.join(peachCacheDir(), 'native-prebuilt', kind, name, key)
63
63
 
64
64
  // one GET; a missing object is null. a network failure is a miss too, since a
@@ -111,7 +111,7 @@ export function fetchNativeEntry(
111
111
  name: string,
112
112
  key: string,
113
113
  ): string | null {
114
- const dir = entryCache(kind, name, key)
114
+ const dir = nativeEntryCache(kind, name, key)
115
115
  const descriptorFile = path.join(dir, 'descriptor.json')
116
116
  if (fs.existsSync(descriptorFile)) return dir
117
117
  const base = `${NATIVE_PREBUILT_ORIGIN}/${nativeEntryPrefix(kind, name, key)}`
@@ -146,6 +146,40 @@ export function fetchNativeEntry(
146
146
  }
147
147
  }
148
148
 
149
+ /**
150
+ * installs a locally produced entry directory (descriptor beside its files) as
151
+ * the cache's entry for its key, after the same checks a download gets.
152
+ * a private distribution has no registry entries, so its host produces them.
153
+ */
154
+ export function installNativeEntry(
155
+ kind: NativeEntryKind,
156
+ name: string,
157
+ key: string,
158
+ source: string,
159
+ ): string {
160
+ const files = parseFiles(
161
+ fs.readFileSync(path.join(source, 'descriptor.json'), 'utf8'),
162
+ kind,
163
+ name,
164
+ key,
165
+ )
166
+ for (const file of files) {
167
+ const target = path.join(source, file.path)
168
+ if (fs.statSync(target).size !== file.size || sha256(target) !== file.sha256)
169
+ throw new Error(
170
+ `native prebuilt ${kind}/${name}/${key}: ${file.path} does not match its descriptor`,
171
+ )
172
+ }
173
+ const dir = entryCache(kind, name, key)
174
+ fs.mkdirSync(path.dirname(dir), { recursive: true })
175
+ const staging = `${dir}.${process.pid}.partial`
176
+ fs.rmSync(staging, { recursive: true, force: true })
177
+ fs.cpSync(source, staging, { recursive: true })
178
+ fs.rmSync(dir, { recursive: true, force: true })
179
+ fs.renameSync(staging, dir)
180
+ return dir
181
+ }
182
+
149
183
  /** the descriptor for files a publisher is about to upload. */
150
184
  export function describeNativeEntry(
151
185
  kind: NativeEntryKind,
@@ -1,12 +1,20 @@
1
+ import { execFileSync } from 'node:child_process'
1
2
  import { createHash } from 'node:crypto'
2
3
  import fs from 'node:fs'
4
+ import os from 'node:os'
3
5
  import path from 'node:path'
4
- import { fetchNativeEntry, nativeEntryKey } from './native-prebuilt'
6
+ import {
7
+ fetchNativeEntry,
8
+ installNativeEntry,
9
+ nativeEntryKey,
10
+ nativeEntryPrefix,
11
+ } from './native-prebuilt'
5
12
  import {
6
13
  swiftDistributionAt,
7
14
  swiftInterfaceDeclarations,
8
15
  withoutSwiftDeclarations,
9
16
  } from './swift-compile'
17
+ import { privateSwiftDistributionRoot, sourceSwiftCheckout } from './swift-toolchain'
10
18
  import { simulatorNativeLibrary } from './swift-xcframework'
11
19
  import type { SwiftStandin } from './swift-standins'
12
20
 
@@ -44,6 +52,37 @@ export function nativeSwiftSliceEntry(
44
52
  return { name: SENTRY_SWIFT_SLICE.name, inputs, key: nativeEntryKey(inputs) }
45
53
  }
46
54
 
55
+ // the registry holds entries only for the pinned distribution; a private one
56
+ // (a canary or local candidate) has none, so its host produces the slice.
57
+ function privateSentrySlice(
58
+ entry: ReturnType<typeof nativeSwiftSliceEntry>,
59
+ moduleRoot: string,
60
+ ): string | null {
61
+ const privateRoot = privateSwiftDistributionRoot()
62
+ if (!privateRoot || fs.realpathSync(privateRoot) !== fs.realpathSync(moduleRoot))
63
+ return null
64
+ const script = path.join(sourceSwiftCheckout(), 'scripts/build-sentry-wasi.ts')
65
+ if (!fs.existsSync(script))
66
+ throw new Error(
67
+ 'the Sentry slice for a private Swift distribution builds from a Contrast source checkout',
68
+ )
69
+ const staging = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'sentry-slice-'))
70
+ try {
71
+ process.stderr.write('peach: building the Sentry slice for a private distribution…\n')
72
+ execFileSync('bun', [script, '--output', staging, '--module-root', moduleRoot], {
73
+ stdio: ['ignore', 2, 2],
74
+ })
75
+ return installNativeEntry(
76
+ 'slice',
77
+ entry.name,
78
+ entry.key,
79
+ path.join(staging, nativeEntryPrefix('slice', entry.name, entry.key)),
80
+ )
81
+ } finally {
82
+ fs.rmSync(staging, { recursive: true, force: true })
83
+ }
84
+ }
85
+
47
86
  export function nativeSwiftSlice(
48
87
  standin: SwiftStandin,
49
88
  moduleRoot: string,
@@ -66,7 +105,9 @@ export function nativeSwiftSlice(
66
105
  const distribution = swiftDistributionAt(moduleRoot)
67
106
  if (!distribution) throw new Error('native Swift slices need a compiled distribution')
68
107
  const entry = nativeSwiftSliceEntry(distribution, generator)
69
- const directory = fetchNativeEntry('slice', entry.name, entry.key)
108
+ const directory =
109
+ fetchNativeEntry('slice', entry.name, entry.key) ??
110
+ privateSentrySlice(entry, moduleRoot)
70
111
  if (!directory)
71
112
  throw new Error(
72
113
  `native Swift slice ${entry.name}/${entry.key} is not built for this compiler and SDK`,
@@ -477,7 +477,13 @@ export function collectBridgeModuleSources(
477
477
  if (swiftClass) {
478
478
  const bridgingHeader = target.objc?.bridgingHeader
479
479
  if (bridgingHeader && target.objc && app) {
480
- nativeInputs ??= xcodeObjCInputs(app, [], null, undefined, projectRoot)
480
+ nativeInputs ??= xcodeObjCInputs(
481
+ app,
482
+ [],
483
+ null,
484
+ undefined,
485
+ installedWorkspace ? app.rootDir : projectRoot,
486
+ )
481
487
  const native = target.objc
482
488
  // an installed package a monorepo hoists above the project keeps its
483
489
  // absolute path under one reserved prefix.
@@ -0,0 +1,176 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { cacheDir } from './home-paths'
5
+ import { describeNativeEntry, nativeEntryCache, nativeEntryKey } from './native-prebuilt'
6
+ import {
7
+ reactNativeCoreEntry,
8
+ reactNativeCoreFloor,
9
+ type ReactNativeCoreMode,
10
+ } from './react-native-core'
11
+ import { compileSwiftProjectUnit } from './swift-compile'
12
+ import { swiftModuleToolchain } from './swift-distribution'
13
+ import { isSwiftNativeTranslationUnit, swiftManifestTargets } from './swift-package'
14
+ import { withSwiftRootLock } from './swift-root-lock'
15
+ import { collectTurboCompileInput } from './turbo-compile'
16
+ import { collectTurboSources, prepareTurboSourceDependencies } from './turbo-source'
17
+
18
+ /** builds the React Native core for an installed app into the local
19
+ * native-prebuilt cache, the directory a published entry downloads into, and
20
+ * returns that directory. an entry already in the cache is returned as is. */
21
+ export async function buildReactNativeCore(
22
+ project: string,
23
+ reactNativeRoot: string,
24
+ mode: ReactNativeCoreMode,
25
+ moduleRoot: string,
26
+ ): Promise<string> {
27
+ const entry = reactNativeCoreEntry(reactNativeRoot, moduleRoot, mode)
28
+ const directory = nativeEntryCache('react-native', entry.name, entry.key)
29
+ const work = path.join(cacheDir(), 'react-native-core-build', entry.key)
30
+ return withSwiftRootLock(work, async () => {
31
+ if (fs.existsSync(path.join(directory, 'descriptor.json'))) return directory
32
+ const module = { root: moduleRoot, ...swiftModuleToolchain(moduleRoot) }
33
+ const floor = reactNativeCoreFloor(reactNativeRoot)
34
+ await prepareTurboSourceDependencies(reactNativeRoot)
35
+ const codegenDir = path.join(work, 'codegen')
36
+ fs.mkdirSync(work, { recursive: true })
37
+ fs.rmSync(codegenDir, { recursive: true, force: true })
38
+ const collection = await collectTurboSources({
39
+ appRoot: project,
40
+ projectRoot: project,
41
+ reactNativeRoot,
42
+ providers: [],
43
+ codegenDir,
44
+ ...(mode === 'turbo-cxx'
45
+ ? { reactNativeCore: true }
46
+ : { reactNativeProvider: true }),
47
+ ...(mode === 'turbo' ? { turboDispatch: true } : {}),
48
+ })
49
+ const producer = {
50
+ ...collection,
51
+ files: { ...collection.files },
52
+ targets: [...collection.targets],
53
+ }
54
+ if (mode === 'turbo') {
55
+ // only the producer executable supplies this host callback; it is absent from the archives.
56
+ producer.targets.push({
57
+ name: 'PeachCoreProducerHost',
58
+ path: 'Sources/PeachCoreProducerHost',
59
+ executable: false,
60
+ languageMode: 5,
61
+ mainActorIsolation: false,
62
+ resources: false,
63
+ clangSettings: [{ tool: 'c', flags: ['-fblocks'] }],
64
+ })
65
+ producer.files['Sources/PeachCoreProducerHost/include/PeachCoreProducerHost.h'] =
66
+ '#pragma once\n'
67
+ producer.files['Sources/PeachCoreProducerHost/Dispatch.m'] =
68
+ '#import <dispatch/dispatch.h>\nvoid peachTurboDispatchAsync(dispatch_block_t block) { dispatch_async(dispatch_get_main_queue(), block); }\n'
69
+ }
70
+ const input = collectTurboCompileInput(producer, null, floor)
71
+ const { compiler: _compiler, recipe: _recipe, ...nativeInputs } = entry.inputs
72
+ const buildRoot = path.join(
73
+ cacheDir(),
74
+ 'react-native-core-products',
75
+ nativeEntryKey(nativeInputs),
76
+ )
77
+ await compileSwiftProjectUnit(input, {
78
+ ...module,
79
+ moduleRoot: module.root,
80
+ buildRoot,
81
+ intermediates: true,
82
+ })
83
+ const products = path.join(buildRoot, '.build/wasm32-unknown-wasip1/release')
84
+ const emitted = swiftManifestTargets(
85
+ fs.readFileSync(path.join(buildRoot, 'Package.swift'), 'utf8'),
86
+ )
87
+ const libraries: Record<string, string> = {}
88
+ const binaryModules: Record<string, string> = {}
89
+ for (const target of collection.targets) {
90
+ const objects = emitted
91
+ .filter((product) => !product.executable && product.path === target.path)
92
+ .flatMap((product) => {
93
+ const built = path.join(products, `${product.name}.build`)
94
+ return fs
95
+ .readdirSync(built, { recursive: true, encoding: 'utf8' })
96
+ .filter((file) => file.endsWith('.o'))
97
+ .map((file) => path.join(built, file))
98
+ })
99
+ .sort()
100
+ if (objects.length === 0)
101
+ throw new Error(`${target.name}: core build produced no objects`)
102
+ const members = path.join(work, 'members', target.name)
103
+ fs.rmSync(members, { recursive: true, force: true })
104
+ fs.mkdirSync(members, { recursive: true })
105
+ const stripped = objects.map((file, index) => {
106
+ const output = path.join(members, `${index}-${path.basename(file)}`)
107
+ execFileSync(path.join(module.toolchainBin, 'llvm-objcopy'), [
108
+ '--strip-debug',
109
+ file,
110
+ output,
111
+ ])
112
+ return output
113
+ })
114
+ const archive = path.join(work, `lib${target.name}.a`)
115
+ fs.rmSync(archive, { force: true })
116
+ execFileSync(path.join(module.toolchainBin, 'llvm-ar'), [
117
+ 'rcsD',
118
+ archive,
119
+ ...stripped,
120
+ ])
121
+ libraries[target.name] = fs.readFileSync(archive).toString('base64')
122
+ const swiftModule = path.join(products, 'Modules', `${target.name}.swiftmodule`)
123
+ if (fs.existsSync(swiftModule)) {
124
+ binaryModules[target.name] = fs.readFileSync(swiftModule).toString('base64')
125
+ collection.files[
126
+ `Sources/TurboCore/include/${target.name}/${target.name}-Swift.h`
127
+ ] = fs.readFileSync(
128
+ path.join(buildRoot, '.build/peach-cxx-headers', `${target.name}-Swift.h`),
129
+ 'utf8',
130
+ )
131
+ }
132
+ // keep textually included implementations available to the preprocessor.
133
+ target.exclude = [
134
+ ...new Set([
135
+ ...(target.exclude ?? []),
136
+ ...Object.keys(collection.files)
137
+ .filter(
138
+ (file) =>
139
+ file.startsWith(`${target.path}/`) &&
140
+ (isSwiftNativeTranslationUnit(file) || file.endsWith('.swift')),
141
+ )
142
+ .map((file) => file.slice(target.path.length + 1)),
143
+ ]),
144
+ ]
145
+ collection.files[`${target.path}/PeachPrebuiltCore.c`] = '\n'
146
+ collection.files[`${target.path}/include/PeachPrebuiltCore.h`] = '#pragma once\n'
147
+ }
148
+ const prebuilt = collectTurboCompileInput(collection, null, floor)
149
+ const core = {
150
+ files: prebuilt.files,
151
+ entry: prebuilt.entry,
152
+ libraries,
153
+ binaryModules,
154
+ staticClangTargets: prebuilt.staticClangTargets ?? [],
155
+ ...(prebuilt.headerAliases ? { headerAliases: prebuilt.headerAliases } : {}),
156
+ ...(prebuilt.nativeTargets ? { nativeTargets: prebuilt.nativeTargets } : {}),
157
+ }
158
+ // the descriptor lands last, by rename, so a reader never sees a partial entry.
159
+ const staging = `${directory}.${process.pid}.building`
160
+ fs.rmSync(staging, { recursive: true, force: true })
161
+ fs.mkdirSync(staging, { recursive: true })
162
+ const coreFile = path.join(staging, 'core.json')
163
+ fs.writeFileSync(coreFile, JSON.stringify(core))
164
+ const descriptor = describeNativeEntry('react-native', entry.name, entry.inputs, [
165
+ coreFile,
166
+ ])
167
+ fs.writeFileSync(path.join(staging, 'descriptor.json'), JSON.stringify(descriptor))
168
+ fs.mkdirSync(path.dirname(directory), { recursive: true })
169
+ fs.rmSync(directory, { recursive: true, force: true })
170
+ fs.renameSync(staging, directory)
171
+ console.error(
172
+ `[peach] React Native core ${entry.name}: built ${collection.targets.length} targets locally`,
173
+ )
174
+ return directory
175
+ })
176
+ }
@@ -10,7 +10,7 @@ export const REACT_NATIVE_CORE_SOURCES = [
10
10
  'nitro-dependencies.ts',
11
11
  'nitroAdapterContract.ts',
12
12
  'podspec-sources.ts',
13
- '../../../scripts/lib/react-native-core.ts',
13
+ 'react-native-core-build.ts',
14
14
  ]
15
15
 
16
16
  export function reactNativeCoreDigest(directory: string): string {
@@ -152,17 +152,34 @@ export function reactNativeCoreEntry(
152
152
  return { name, inputs, key: nativeEntryKey(inputs) }
153
153
  }
154
154
 
155
- export function prebuiltReactNativeCore(
155
+ // the published core is a cache of this build: a cli whose compiler or a
156
+ // runtime whose distribution has no published entry yet builds it once into the
157
+ // same local cache instead of refusing the app.
158
+ const building = new Map<string, Promise<string>>()
159
+
160
+ export async function prebuiltReactNativeCore(
161
+ project: string,
156
162
  reactNativeRoot: string,
157
163
  moduleRoot: string,
158
164
  mode: ReactNativeCoreMode,
159
- ): ReactNativeCoreInput {
165
+ ): Promise<ReactNativeCoreInput> {
160
166
  const entry = reactNativeCoreEntry(reactNativeRoot, moduleRoot, mode)
161
- const dir = fetchNativeEntry('react-native', entry.name, entry.key)
162
- if (!dir)
163
- throw new Error(
164
- `no prebuilt React Native ${entry.inputs['react-native']} ${mode} core for headers ${entry.inputs.headers}; publish ${NATIVE_PREBUILT_ORIGIN}/${nativeEntryPrefix('react-native', entry.name, entry.key)}/descriptor.json before opening this app`,
165
- )
167
+ let dir = fetchNativeEntry('react-native', entry.name, entry.key)
168
+ if (!dir) {
169
+ let pending = building.get(entry.key)
170
+ if (!pending) {
171
+ console.error(
172
+ `[peach] no published React Native ${entry.name} core at ${NATIVE_PREBUILT_ORIGIN}/${nativeEntryPrefix('react-native', entry.name, entry.key)}; building it locally`,
173
+ )
174
+ pending = import('./react-native-core-build')
175
+ .then(({ buildReactNativeCore }) =>
176
+ buildReactNativeCore(project, reactNativeRoot, mode, moduleRoot),
177
+ )
178
+ .finally(() => building.delete(entry.key))
179
+ building.set(entry.key, pending)
180
+ }
181
+ dir = await pending
182
+ }
166
183
  const file = path.join(dir, 'core.json')
167
184
  const descriptor: unknown = JSON.parse(
168
185
  fs.readFileSync(path.join(dir, 'descriptor.json'), 'utf8'),
@@ -778,6 +778,9 @@ export function resolveSwiftPackages(options: {
778
778
  )
779
779
  )
780
780
  return false
781
+ // an assembly target stands in from its public headers, so a stand-in
782
+ // peach built for it stays provided instead of compiling its sources
783
+ if (providedTargets.has(name) && assemblySources(files, target)) return false
781
784
  claimed.add(key)
782
785
  providedTargets.delete(name)
783
786
  for (const dependency of target.dependencies) {
@@ -1296,8 +1296,16 @@ export function emitSwiftStandin(
1296
1296
  [...superChain(owner.get(member.identifier.precise) ?? '')].some((at) =>
1297
1297
  options.requiredInits?.has(`${pathOf(byId.get(at)!)}.init()`),
1298
1298
  )
1299
+ // clang imports an init a class takes from a protocol requirement as
1300
+ // required, which a non-final class must declare to conform
1301
+ const protocolInit =
1302
+ member.kind.identifier === 'swift.init' &&
1303
+ /^c:objc\(pl\)\w+\(im\).*::SYNTHESIZED::c:objc\(cs\)/.test(
1304
+ member.identifier.precise,
1305
+ )
1299
1306
  const required =
1300
- context.inClass && (flags.has('required') || requiredInit || restatesRequired)
1307
+ context.inClass &&
1308
+ (flags.has('required') || requiredInit || restatesRequired || protocolInit)
1301
1309
  const final = context.inClass && flags.has('final') ? 'final ' : ''
1302
1310
  const mutating = flags.has('mutating')
1303
1311
  ? 'mutating '
@@ -659,9 +659,12 @@ function cxxOnly(surface: Surface): boolean {
659
659
  if (surface.kind !== 'c' && surface.kind !== 'objc') return false
660
660
  return files(surface.headers, (name) => name.endsWith('.h')).some((file) => {
661
661
  const guards: boolean[] = []
662
- for (const line of fs
662
+ // documentation prose such as "class that conforms to" is not a declaration
663
+ const code = fs
663
664
  .readFileSync(path.join(surface.headers, file), 'utf8')
664
- .split('\n')) {
665
+ .replace(/\/\*[\s\S]*?\*\//g, '')
666
+ .replace(/\/\/.*$/gm, '')
667
+ for (const line of code.split('\n')) {
665
668
  if (/^\s*#\s*if/.test(line))
666
669
  guards.push(/__cplusplus/.test(line) && !/!\s*defined|ifndef/.test(line))
667
670
  else if (/^\s*#\s*endif/.test(line)) guards.pop()