Skip to content

Bridge pod pattern and kn gc fix#52

Open
jinweihan93-ops wants to merge 28 commits intoandroid:mainfrom
jinweihan93-ops:bridge-pod-pattern-and-kn-gc-fix
Open

Bridge pod pattern and kn gc fix#52
jinweihan93-ops wants to merge 28 commits intoandroid:mainfrom
jinweihan93-ops:bridge-pod-pattern-and-kn-gc-fix

Conversation

@jinweihan93-ops
Copy link
Copy Markdown

No description provided.

jinwei.han and others added 28 commits March 30, 2026 20:49
…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>
@google-cla
Copy link
Copy Markdown

google-cla bot commented Apr 3, 2026

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.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +7
DEVICE=93C1CE99-BB65-4FB8-874C-48BD1056E350
BUNDLE=com.exampe.kmp.getstarted.KMPGetStartedCodelab
WORKSPACE=/Users/bytedance/codelab-android-kmp/get-started/iosApp/KMPGetStartedCodelab.xcworkspace
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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"

Comment on lines +54 to +57
"\"$(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\"",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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`*
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
*项目路径:`/Users/bytedance/codelab-android-kmp/get-started`*
*项目路径:`./get-started` (项目根目录)*

Comment on lines +28 to +66
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"
)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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"
            )
        }
    }

Comment on lines +26 to +27
internal var _authDelegate: KMPAuthDelegateProtocol? = null
internal var _networkDelegate: KMPNetworkDelegateProtocol? = null
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment on lines +37 to +62
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")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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)}

Comment on lines +158 to +162
try:
return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True)
except subprocess.CalledProcessError:
return ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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 ""

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant