Bridge pod pattern and kn gc fix#52
Conversation
…mework object tests - SharedData.kt in foundation + SharedDataProcessor.kt in business for cross-framework type check - RuntimeDuplicateTest.swift: ObjC class hierarchy, dladdr, objc_copyClassList proofs - ContentView.swift: integrated test UI showing is-check failure across runtimes - app-binary-analyzer.py: analyze embedded frameworks in .app bundle (7821 dup symbols / 98.2%) - business/build.gradle.kts: add export + api for foundation types in ObjC header - Evidence report with 4 dimensions: symbols, GC threads (2x2), ObjC classes, object passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ked generated files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Exhaustively explored 5 technical paths for achieving K/N runtime sharing
between two separate XCFrameworks (foundationKit + businessKit):
- Path A: Post-build strip — not feasible (strip can't remove code, only symbol table)
- Path B: isStatic=true — not feasible (local 't' symbols still duplicate; verified 4 GC threads)
- Path C: Re-link from .o files — not feasible (K/N produces no intermediate .o files)
- Path D: Umbrella/merge XCFramework — technically works but user declined (wants split not merge)
- Path E: K/N compiler flags / linkerOpts — not feasible:
- -nostdlib: doesn't affect runtime embedding
- -Xruntime=/dev/null: requires valid bitcode, K/N LTO-merges it unconditionally
- -linker-options: adds load dependency but runtime still embedded
- Weak symbol coalescing: _IsInstance is weak external but called via direct PC-relative
branch (not GOT), so dyld coalescing cannot redirect calls
Key technical finding: K/N's LTO compilation hardcodes runtime calls as direct
ARM64 bl instructions (PC-relative). iOS two-level namespace + absence of GOT
indirection makes runtime sharing impossible without K/N compiler changes (KMT-2364).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…inking foundationKit (producer): - Add exportRuntimeSymbols=true binary option so makeRuntimeSymbolsExported pass exports runtime C symbols (including __ZTI18ExceptionObjHolder) with default visibility after LTO, making them resolvable by consumer frameworks businessKit (consumer): - Add -undefined dynamic_lookup linker flag so undefined runtime symbols are accepted at link time and resolved from foundationKit at dyld load time (required when cacheKind=none disables the pre-compiled stdlib cache) Both frameworks depend on kotlin.native.cacheKind=none in local.properties to force IR-level linking with the patched runtime.bc (which contains Kotlin_ObjCExport_initializeClassWithAdapters). The pre-compiled stdlib cache was built before this function existed and cannot be used. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
生产者(foundationKit)通过 exportKlibSymbols 导出 klib 符号, 消费者(businessKit)通过 externalKlibs 将其设为外部引用。 两个框架在运行时共享同一份 Kotlin class descriptor, 修复了跨框架 is/as 类型检查失败的问题。 测试结果: - 7 种 foundation 类型的 is 检查全部通过(数据类 + sealed class 层次) - as 强制转换不再抛 ClassCastException - 集合类型过滤正确 - 已知限制:跨框架对象的字段访问返回空值,sealed class 字段访问 crash (需进一步调查 K/N 编译器 available_externally 内联行为) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- kmt-2364-fix-report.md: Phase 1 修复报告(runtime 符号导出 + consumer 链接) - kmt-2364-phase2-fix-report.md: Phase 2 修复报告(klib 符号外部化) - shared-runtime-compiler-design.md: K/N 编译器共享运行时方案设计 - kn-shared-runtime-poc/README.md: POC 说明 - .gitignore: 忽略 *_runtime_exports.txt 生成文件 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
将散落在 kn-shared-runtime-poc/、xcframework_viz/kotlin_native_toolchain/、
xcframework_viz/reports/ 三处的文档统一迁移到 docs/ 目录,按类别分文件夹:
docs/
├── design/ 设计文档
│ ├── business-module-design.md
│ ├── shared-runtime-compiler-design.md
│ ├── shared-runtime-poc.md ← 英→中
│ └── v3-split-delivery-paths-report.md
├── analysis/ 分析报告
│ ├── kn-xcframework-symbol-analysis-2026-03-30.md
│ ├── v3-dual-runtime-evidence-report.md
│ └── v3-duplicate-symbol-analysis-2026-03-30.md
└── fix-reports/ 修复报告
├── kmt-2364-fix-report.md
├── kmt-2364-phase2-fix-report.md ← 英→中
└── kmt-2364-phase2-milestone.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/analysis/v3-dual-runtime-problem.md(原 android#1+android#2+android#3) - docs/fix-reports/kmt-2364-fix.md(原 android#8+android#9+android#10,含 Phase 1+2) - docs/design/shared-runtime-design.md(原 android#4+android#5+android#6+android#7) - 同步更新 CLAUDE.md 中的文档路径引用 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
根本原因:businessKit 构建时对 Foundation 类型执行了完整的 ObjC bridge 生成,
导致 FoundationKitResponseResult 等 ObjC class 在两个 framework 中各注册一次,
BackRef→ObjHeader* 映射被破坏,所有字段读取返回 0/""。
修复方案(将 foundationKit 当作系统 framework 对待):
- business/build.gradle.kts:移除 export(project(":foundation")),防止 Foundation
类型的 ObjC bridge 在 businessKit 中重复生成;将 -undefined dynamic_lookup 替换
为显式 -framework foundationKit -F <path>,让链接器明确知道 Foundation ObjC
class 已在 foundationKit.framework 中定义
- foundation/build.gradle.kts + business/build.gradle.kts:移除不需要的 iosX64 target,
只保留 iosArm64 + iosSimulatorArm64
同步内容:
- NetworkProcessor.kt:使用 foundation accessor 函数避免 GEP 偏移错误
- TypeTestModels.kt:新增 Phase 2 类型测试辅助函数
- ContentView.swift:Phase 2 综合测试 (T1-T7) 及诊断日志
- docs/design/:根因分析文档和静态验证脚本
测试结果:T1✅ T2✅ T3✅ T4✅ T5✅ T6✅ T7✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KMT-2364 Phase 2:修复跨框架 ObjC bridge 字段读取 (T2/T3/T4/T7 全通过) See merge request: !2
Xcode 15+ Explicit Module Build 在 Build Phase 之前做模块规划, CocoaPods 的 xcframeworks.sh 是 Build Phase,首次干净构建时 XCFrameworkIntermediates 为空,导致 "Unable to find module dependency"。 在 post_install 里把 XCFramework slice 路径直接加入主 target 的 FRAMEWORK_SEARCH_PATHS,让模块规划阶段直接找到 .swiftmodule, 彻底解决 clean DerivedData 后无法从 Xcode IDE 构建的问题。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: Podfile post_install 修复 Xcode 干净构建时 module 找不到的问题 See merge request: !3
- foundation/GCTestKit.kt (new, iosMain): testStrongRefSurvivesGC, testWeakRefClearedAfterGC, gcCollect, createGCTestPayload — verifies single shared GC instance between foundationKit and businessKit - business/GCCrossFrameworkProcessor.kt (new, iosMain): holdThroughGC and readEndpointAfterGC — businessKit holds foundationKit objects through a GC cycle and checks type identity + field access survive - TypeTestModels.kt: remove debug-only helpers (createTestRequest, createTestResponse, debugDumpResponse, debugDumpAnyResponse, createAndReadInKotlin); keep RAUW field accessors used by NetworkProcessor - ContentView.swift: remove all NSLog diagnostic spam, file-writing code, pre-T7 flush block, and direct RAUW accessor calls; add runGCTests() driving T8-T11; add "GC Tests" button alongside existing "Type Tests" and "All Tests" buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KMT-2364: Add GC tests (T8-T11) and clean up debug artifacts See merge request: !4
GCTestKit.kt: - Isolate object creation in makeWeakRef() so RequestPayload is off all stack frames when GC.collect() is called; LLVM may retain the old pointer in a register if strong=null and GC.collect() are in the same stack frame, keeping the object alive as a GC root - Remove "survived while held" pre-check (now implicit: if object is already collected before GC we'd get true — acceptable simplification) ContentView.swift: - Auto-run runGCTests() on startup (1.5s after appear) alongside runPhase2Tests(); all tests now appear in simctl logs automatically - Add per-test NSLog in runGCTests for T8/T9/T10/T11 diagnosis Result: T1-T11 all pass (7/7 type + 4/4 GC) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KMT-2364: Fix T9 WeakRef test + auto-run GC tests on startup See merge request: !5
…bridge Module layout changes: foundation/ (was a module, now a group directory) ├── KMPFoundation/ (was :foundation) └── foundation-bridge/ (was :foundation-ios-bridge, "ios" removed from name) business/ (was a module, now a group directory) ├── KMPBusiness/ (was :business) └── business-bridge/ (was :business-ios-bridge, "ios" removed from name) Gradle path changes: :foundation → :foundation:KMPFoundation :foundation-ios-bridge → :foundation:foundation-bridge :business → :business:KMPBusiness :business-ios-bridge → :business:business-bridge iOS bridge (cinterop) implementation: - foundation-bridge/: KMPPlatformInfoProvider + KMPLoggerDelegate ObjC protocols - business-bridge/: KMPAuthDelegate + KMPNetworkDelegate ObjC protocols - Storage via ObjC C functions in .def native section (avoids arm64_adrp_lo12 cross-framework fixup errors caused by Kotlin internal var + externalKlibs) - buildIOSDebug/buildIOSRelease tasks inject bridge headers into XCFramework so Swift can see full protocol definitions for conformance declarations - BridgeProviders.swift: concrete iOS implementations registered at App.init() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…free
Module layout:
foundation/
foundationCommon/ ← NEW: all source code + cinterop + produces foundationKit.xcframework
KMPFoundation/ ← NOW: zero Kotlin sources, pure CocoaPods delivery orchestration
foundation-bridge/ ← unchanged
business/
businessCommon/ ← NEW: all source code + cinterop + produces businessKit.xcframework
KMPBusiness/ ← NOW: zero Kotlin sources, pure CocoaPods delivery orchestration
business-bridge/ ← unchanged
Why this split:
• Common modules own the code and dependency graph; IDE navigation works naturally
• KMP modules have no sources to distract — just build config + delivery tasks
• XCFramework is built inside Common's build/ dir; KMP modules copy the podspec there
and inject bridge ObjC headers so Swift conformances compile
androidApp now depends on :foundation:foundationCommon (Android artifact)
Podfile paths updated: foundationCommon/build/... and businessCommon/build/...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The iosBridgeMain srcDir trick (adding the same physical directory to
iosArm64Main + iosSimulatorArm64Main via kotlin.srcDir) existed only to
work around cinterop types not being visible in the iosMain intermediate
source set without commonization.
With kotlin.mpp.enableCInteropCommonization=true already in gradle.properties,
the cinterop types ARE visible in iosMain directly. The hack is unnecessary.
Changes:
- foundation/foundationCommon: move BridgeSetup.kt, KMPLogger.kt, Platform.ios.kt
from src/iosBridgeMain/ → src/iosMain/ ; delete iosBridgeMain directory
- business/businessCommon: move BusinessBridgeSetup.kt, BridgeClient.kt
from src/iosBridgeMain/ → src/iosMain/ ; delete iosBridgeMain directory
- Remove the getByName("iosArm64Main") { kotlin.srcDir(...) } blocks from both
build.gradle.kts files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rip demo + fix K/N GC crash ## Architecture changes - Move iosApp/ out of get-started/ to repo root (sibling of get-started/) - Add kmp-local-repo/ as binary distribution layer (KMP → iOS via XCFramework podspecs) - Forbid source-code dependency between iOS and KMP projects ## Bridge pod pattern (TikTok-style) - foundationBridge pod: header-only ObjC protocol declarations (iOS-owned) - businessBridge pod: header-only ObjC protocol declarations (iOS-owned) - foundationBridgeImpl pod: concrete ObjC .m implementations (AppPlatformProvider, AppLoggerDelegate) - businessBridgeImpl pod: concrete ObjC .m implementations (AppAuthDelegate, AppNetworkDelegate) - XCFramework podspecs declare spec.dependency on bridge pods (no umbrella-header injection) ## KMP side - Remove injectBridgeHeaders hack; add publishIOSDebug/Release tasks to kmp-local-repo/ - Add ProfileResult.kt + ProfileLoader.kt (iosMain): Swift→Kotlin→ObjC→Kotlin→Swift round-trip - Add TypeTestModels.kt (foundation) + NetworkProcessor.kt (business) for Phase 2 type tests - Add publishKMPIOSDebug/Release aggregate tasks in root build.gradle.kts ## iOS side - Bridge Round-Trip Demo button in ContentView: 8-step call chain with real ObjC delegates - RuntimeDuplicateTest: fix swift_dynamicCast crash via UnsafeRawPointer.load pattern - Phase 2 type tests (T1-T7) and GC tests (T8-T11) all pass at startup ## Bug fix: K/N GC crash (NSForwarding: __NSGenericDeallocHandler abort) - Root cause: dispatch_after on background GCD thread held the only ObjC retain on the K/N-wrapped Kotlin completion lambda; K/N GC collected the lambda before the 50ms callback fired, causing use-after-free when the stale block was invoked - Fix: AppNetworkDelegate.requestURL calls completion synchronously; the Kotlin lambda is still on the call stack when fired so GC cannot reclaim it; UI async effect is preserved via Swift's DispatchQueue.main.async inside the Kotlin callback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive research project and proof-of-concept for a Kotlin Multiplatform (KMP) V3 split-delivery architecture, specifically addressing the 'dual runtime' problem in iOS XCFrameworks. It includes new Gradle modules for Foundation and Business logic, ObjC bridge implementations, and a suite of diagnostic tools and verification scripts. Feedback focuses on improving the portability of shell scripts and documentation by removing hardcoded paths and device IDs, correcting relative paths in the Podfile to ensure clean builds, and refactoring Gradle build scripts to reduce configuration duplication across iOS targets. Additionally, it is recommended that the Business module adopts the more robust C-based bridge storage pattern used in the Foundation module.
| DEVICE=93C1CE99-BB65-4FB8-874C-48BD1056E350 | ||
| BUNDLE=com.exampe.kmp.getstarted.KMPGetStartedCodelab | ||
| WORKSPACE=/Users/bytedance/codelab-android-kmp/get-started/iosApp/KMPGetStartedCodelab.xcworkspace |
There was a problem hiding this comment.
The script contains hardcoded values for the device UDID and the workspace path. This makes the script non-portable and will cause it to fail on other developers' machines. These should be made dynamic, for example by using arguments or environment variables, or by deriving them relative to the script's location.
| DEVICE=93C1CE99-BB65-4FB8-874C-48BD1056E350 | |
| BUNDLE=com.exampe.kmp.getstarted.KMPGetStartedCodelab | |
| WORKSPACE=/Users/bytedance/codelab-android-kmp/get-started/iosApp/KMPGetStartedCodelab.xcworkspace | |
| DEVICE=${1:-$(xcrun simctl list devices booted | grep -o '[A-F0-9]\\{8\\}-[A-F0-9]\\{4\\}-[A-F0-9]\\{4\\}-[A-F0-9]\\{4\\}-[A-F0-9]\\{12\\}' | head -n 1)} | |
| BUNDLE=com.exampe.kmp.getstarted.KMPGetStartedCodelab | |
| WORKSPACE="$(dirname "$0")/iosApp/KMPGetStartedCodelab.xcworkspace" |
| "\"$(PODS_ROOT)/../../kmp-local-repo/foundationKit/foundationKit.xcframework/ios-arm64-simulator\"", | ||
| "\"$(PODS_ROOT)/../../kmp-local-repo/foundationKit/foundationKit.xcframework/ios-arm64\"", | ||
| "\"$(PODS_ROOT)/../../kmp-local-repo/businessKit/businessKit.xcframework/ios-arm64-simulator\"", | ||
| "\"$(PODS_ROOT)/../../kmp-local-repo/businessKit/businessKit.xcframework/ios-arm64\"", |
There was a problem hiding this comment.
The framework search paths in the post_install hook use a relative path $(PODS_ROOT)/../.. which resolves to the get-started directory. Based on the project structure and Gradle scripts, the kmp-local-repo directory is a sibling of get-started, not a child. This will cause build failures on a clean checkout. The path should be corrected to point three levels up from PODS_ROOT to reach the parent of get-started.
"\"$(PODS_ROOT)/../../../kmp-local-repo/foundationKit/foundationKit.xcframework/ios-arm64-simulator\"",
"\"$(PODS_ROOT)/../../../kmp-local-repo/foundationKit/foundationKit.xcframework/ios-arm64\"",
"\"$(PODS_ROOT)/../../../kmp-local-repo/businessKit/businessKit.xcframework/ios-arm64-simulator\"",
"\"$(PODS_ROOT)/../../../kmp-local-repo/businessKit/businessKit.xcframework/ios-arm64\""
| --- | ||
|
|
||
| *最后更新:2026-04-01* | ||
| *项目路径:`/Users/bytedance/codelab-android-kmp/get-started`* |
There was a problem hiding this comment.
The documentation contains a hardcoded absolute file path. This is specific to one developer's machine and reduces the portability and usefulness of the documentation for others. It should be replaced with a relative path or a placeholder to make it universally applicable.
| *项目路径:`/Users/bytedance/codelab-android-kmp/get-started`* | |
| *项目路径:`./get-started` (项目根目录)* |
| iosArm64 { | ||
| compilations["main"].cinterops.create("businessBridge") { | ||
| defFile(project.file("../business-bridge/businessBridge.def")) | ||
| includeDirs(project.file("../business-bridge/headers")) | ||
| } | ||
| binaries.framework { | ||
| baseName = xcfName | ||
| xcf.add(this) | ||
| // KMT-2364: Do NOT re-export Foundation types — their ObjC bridge lives in | ||
| // foundationKit.framework only. Re-exporting causes duplicate ObjC class | ||
| // registration, corrupting the BackRef→ObjHeader* mapping. | ||
| binaryOption("embedRuntime", "false") | ||
| binaryOption("externalKlibs", "com.example.kmp.foundation") | ||
| val suffix = if (buildType == NativeBuildType.DEBUG) "debug" else "release" | ||
| linkerOpts( | ||
| "-framework", "foundationKit", | ||
| // foundationKit.framework is produced by foundationCommon (not KMPFoundation). | ||
| "-F", "${projectDir}/../../foundation/foundationCommon/build/bin/iosArm64/${suffix}Framework" | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| iosSimulatorArm64 { | ||
| compilations["main"].cinterops.create("businessBridge") { | ||
| defFile(project.file("../business-bridge/businessBridge.def")) | ||
| includeDirs(project.file("../business-bridge/headers")) | ||
| } | ||
| binaries.framework { | ||
| baseName = xcfName | ||
| xcf.add(this) | ||
| binaryOption("embedRuntime", "false") | ||
| binaryOption("externalKlibs", "com.example.kmp.foundation") | ||
| val suffix = if (buildType == NativeBuildType.DEBUG) "debug" else "release" | ||
| linkerOpts( | ||
| "-framework", "foundationKit", | ||
| "-F", "${projectDir}/../../foundation/foundationCommon/build/bin/iosSimulatorArm64/${suffix}Framework" | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
The configurations for iosArm64 and iosSimulatorArm64 are nearly identical. This duplication can be reduced by iterating over a list of targets, which would make the script more concise and easier to maintain.
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { target ->
target.compilations["main"].cinterops.create("businessBridge") {
defFile(project.file("../business-bridge/businessBridge.def"))
includeDirs(project.file("../business-bridge/headers"))
}
target.binaries.framework {
baseName = xcfName
xcf.add(this)
// KMT-2364: Do NOT re-export Foundation types — their ObjC bridge lives in
// foundationKit.framework only. Re-exporting causes duplicate ObjC class
// registration, corrupting the BackRef→ObjHeader* mapping.
binaryOption("embedRuntime", "false")
binaryOption("externalKlibs", "com.example.kmp.foundation")
val suffix = if (buildType == NativeBuildType.DEBUG) "debug" else "release"
linkerOpts(
"-framework", "foundationKit",
// foundationKit.framework is produced by foundationCommon (not KMPFoundation).
"-F", "${projectDir}/../../foundation/foundationCommon/build/bin/${target.targetName}/${suffix}Framework"
)
}
}| internal var _authDelegate: KMPAuthDelegateProtocol? = null | ||
| internal var _networkDelegate: KMPNetworkDelegateProtocol? = null |
There was a problem hiding this comment.
The bridge delegates _authDelegate and _networkDelegate are stored in mutable Kotlin internal vars. This pattern is less robust than the one used in the foundation module, which uses C functions and static Obj-C variables for storage. The foundation module's approach is safer against cross-dylib linking issues and provides better encapsulation. For consistency and future-proofing, the business module should adopt the same pattern.
| iosArm64 { | ||
| compilations["main"].cinterops.create("foundationBridge") { | ||
| defFile(project.file("../foundation-bridge/foundationBridge.def")) | ||
| includeDirs(project.file("../foundation-bridge/headers")) | ||
| } | ||
| binaries.framework { | ||
| baseName = xcfName | ||
| xcf.add(this) | ||
| // Split-framework: foundationKit embeds the runtime so businessKit doesn't have to. | ||
| binaryOption("exportRuntimeSymbols", "true") | ||
| binaryOption("exportKlibSymbols", "com.example.kmp.foundation") | ||
| } | ||
| } | ||
|
|
||
| iosSimulatorArm64 { | ||
| compilations["main"].cinterops.create("foundationBridge") { | ||
| defFile(project.file("../foundation-bridge/foundationBridge.def")) | ||
| includeDirs(project.file("../foundation-bridge/headers")) | ||
| } | ||
| binaries.framework { | ||
| baseName = xcfName | ||
| xcf.add(this) | ||
| binaryOption("exportRuntimeSymbols", "true") | ||
| binaryOption("exportKlibSymbols", "com.example.kmp.foundation") | ||
| } | ||
| } |
There was a problem hiding this comment.
The configurations for iosArm64 and iosSimulatorArm64 are nearly identical. This duplication can be reduced by iterating over a list of targets, making the script more concise and easier to maintain.
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { target ->
target.compilations["main"].cinterops.create("foundationBridge") {
defFile(project.file("../foundation-bridge/foundationBridge.def"))
includeDirs(project.file("../foundation-bridge/headers"))
}
target.binaries.framework {
baseName = xcfName
xcf.add(this)
// Split-framework: foundationKit embeds the runtime so businessKit doesn't have to.
binaryOption("exportRuntimeSymbols", "true")
binaryOption("exportKlibSymbols", "com.example.kmp.foundation")
}
}| # KMT-2364 Runtime Verification Script | ||
| # Run this from a NEW terminal (not from Claude Code session) | ||
|
|
||
| DEVICE=93C1CE99-BB65-4FB8-874C-48BD1056E350 |
There was a problem hiding this comment.
The script hardcodes a specific simulator device UDID. This makes it brittle and not portable to other development environments. Consider making the device UDID configurable via an argument or automatically detecting a booted simulator.
| DEVICE=93C1CE99-BB65-4FB8-874C-48BD1056E350 | |
| DEVICE=${1:-$(xcrun simctl list devices booted | grep -o '[A-F0-9]\\{8\\}-[A-F0-9]\\{4\\}-[A-F0-9]\\{4\\}-[A-F0-9]\\{4\\}-[A-F0-9]\\{12\\}' | head -n 1)} |
| try: | ||
| return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True) | ||
| except subprocess.CalledProcessError: | ||
| return "" | ||
|
|
There was a problem hiding this comment.
The _run helper function suppresses errors from subprocess calls by returning an empty string. This can hide important issues during analysis, making debugging difficult. It would be more robust to log the error to stderr before returning.
| try: | |
| return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True) | |
| except subprocess.CalledProcessError: | |
| return "" | |
| def _run(self, cmd: List[str]) -> str: | |
| try: | |
| return subprocess.check_output(cmd, stderr=subprocess.PIPE, text=True) | |
| except subprocess.CalledProcessError as e: | |
| print(f"Warning: Command '{' '.join(cmd)}' failed with exit code {e.returncode}:\n{e.stderr}", file=sys.stderr) | |
| return "" |
No description provided.