From a589ac953f779c6b93f27db6ab28663f10c5e60e Mon Sep 17 00:00:00 2001 From: Edison Jwa Date: Sun, 7 Jun 2026 07:50:30 +0900 Subject: [PATCH] fix(ios,macos): preserve Silero @_cdecl exports across Xcode Archive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Apple CoreML Silero VAD backend resolves six @_cdecl Swift symbols via dlsym(RTLD_DEFAULT) at runtime in the Rust audio crate. Local flutter build paths preserved those symbols, but Xcode Archive (the path used for TestFlight and App Store uploads) silently stripped them through two independent mechanisms, causing Rust to fall back to WebRTC VAD on every shipped build. Both stripping mechanisms are now neutralised: * ld dead-strip: OTHER_LDFLAGS now whitelists each of the six chanora_silero_vad_* symbols via repeated `-Xlinker -exported_symbol` pairs in ios/Flutter/{Release,Debug}.xcconfig and macos/Flutter/Flutter-{Release,Debug}.xcconfig. * install-time strip: STRIP_STYLE is set to `non-global` in the same four xcconfigs so the post-link strip phase no longer drops exported global text symbols from the Archive product. Cost: ~264 bytes per binary; verified `xcrun strip` vs `xcrun strip -x` behaviour. Self-test wired into both AppDelegates: at launch on a utility queue, ChanoraSileroSelfTest resolves all six symbols through dlsym (the same path the Rust runtime uses, not a direct call that would mask the bug class) and exercises create → reset → process → destroy. Result is logged via NSLog and surfaces in Console.app / idevicesyslog. A post-link verify_silero_exports.sh build phase runs nm -gU on the final Archive binary and fails the build if any of the six symbols are missing. Empirically caught the original Archive regression that flutter build --no-codesign did not. CocoaPods bridge podspecs now emit a proper .dSYM via dsymutil so TestFlight crash reports are symbolicated; Cargo.toml release profile sets `debug = true` because dsymutil needs DWARF in the input dylib. macOS chanora_bridge.podspec PATH inserts /opt/homebrew/opt/rustup/bin ahead of /opt/homebrew/bin so rustup's cargo (which has the x86_64-apple-darwin target installed) wins over the homebrew rust formula that is aarch64-only. iOS Podfile target renamed from `Runner` to `Chanora` to match the Xcode target name shipped in the project (the workspace and scheme already referenced Chanora; the Podfile mismatch produced lint warnings during `pod install`). ITSAppUsesNonExemptEncryption=false declared in both Info.plist files so TestFlight and App Store Connect uploads skip the export-compliance prompt; Chanora uses only platform-provided TLS. .gitignore now covers Xcode archive bundles, IPA exports, dSYM directories, the local macOS release zip, and agent/tooling state directories so generated TestFlight artifacts no longer appear in git status. End-to-end verified by headless archive: xcodebuild -workspace Runner.xcworkspace -scheme Runner \ -configuration Release -destination 'generic/platform=iOS' \ -archivePath /tmp/chanora.xcarchive archive CODE_SIGNING_ALLOWED=NO nm -gU on the resulting .app/Chanora binary shows all six chanora_silero_vad_* symbols present. --- .gitignore | 13 ++ Cargo.toml | 24 ++++ .../ios/ExportOptions/AdHoc.plist | 25 ++++ .../ios/Flutter/Debug.xcconfig | 6 +- .../ios/Flutter/Release.xcconfig | 14 +- apps/chanora_flutter/ios/Podfile | 2 +- apps/chanora_flutter/ios/Podfile.lock | 4 +- .../ios/Runner.xcodeproj/project.pbxproj | 127 +++++++++++------- .../xcshareddata/xcschemes/Runner.xcscheme | 22 +-- .../ios/Runner/AppDelegate.swift | 4 + apps/chanora_flutter/ios/Runner/Info.plist | 19 ++- .../ios/Runner/SileroCoreMLBridge.swift | 95 +++++++++++++ .../ios/chanora_bridge.podspec | 69 ++++++++-- .../macos/Flutter/Flutter-Debug.xcconfig | 4 + .../macos/Flutter/Flutter-Release.xcconfig | 9 ++ apps/chanora_flutter/macos/Podfile.lock | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 16 +++ .../macos/Runner/AppDelegate.swift | 4 + apps/chanora_flutter/macos/Runner/Info.plist | 2 + .../macos/Runner/SileroCoreMLBridge.swift | 95 +++++++++++++ .../macos/chanora_bridge.podspec | 71 +++++++--- .../scripts/verify_silero_exports.sh | 56 ++++++++ 22 files changed, 582 insertions(+), 101 deletions(-) create mode 100644 apps/chanora_flutter/ios/ExportOptions/AdHoc.plist create mode 100755 apps/chanora_flutter/scripts/verify_silero_exports.sh diff --git a/.gitignore b/.gitignore index 468c51f..0bb7142 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,16 @@ opencode.json .opencode/ AGENTS.md Screenshot 2026-05-17 at 22.23.07.png + +# Xcode archive / export bundles (generated by Product > Archive > Distribute) +**/Chanora */ +**/*.xcarchive/ +**/*.ipa +**/*.dSYM/ + +# macOS release zip bundles produced by local release scripts +/chanora-v*.zip + +# Local agent / tooling state +.omo/ +.playwright-mcp/ diff --git a/Cargo.toml b/Cargo.toml index 3d3a391..e74e83a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,3 +82,27 @@ tsproto-types = { git = "https://github.com/EdisonJwa/tsclientlib.git", branch = [patch.crates-io] cmake = { git = "https://github.com/pr2502/cmake-rs", rev = "bdad5edc569d82151922c5c6c4685b1563f12aa1" } + +# Emit DWARF debug info in release builds. +# +# Apple's archive validator (and App Store Connect upload) require a +# matching `.dSYM` for every embedded framework, keyed by the framework +# binary's LC_UUID. The CocoaPods bridge podspecs copy the cargo-built +# `libchanora_bridge.dylib` into `chanora_bridge.framework/chanora_bridge` +# and then run `dsymutil` on it (see apps/chanora_flutter/ios and +# apps/chanora_flutter/macos podspecs). `dsymutil` needs DWARF in the +# input binary to produce a useful dSYM with symbol records; without +# `debug = true` here we'd ship a stripped binary and an empty/unusable +# dSYM and TestFlight crash reports would be unsymbolicated. +# +# `split-debuginfo = "off"` keeps DWARF inside the dylib so install_name_tool +# rewriting and the dsymutil pass both work without chasing per-object +# `.o` files cargo would otherwise scatter under target//release/deps. +# +# `strip = false` ensures cargo does not pre-strip the dylib before +# dsymutil sees it. We strip the in-framework binary ourselves after the +# dSYM is produced (handled in the podspec script phases). +[profile.release] +debug = true +split-debuginfo = "off" +strip = false diff --git a/apps/chanora_flutter/ios/ExportOptions/AdHoc.plist b/apps/chanora_flutter/ios/ExportOptions/AdHoc.plist new file mode 100644 index 0000000..6e56b95 --- /dev/null +++ b/apps/chanora_flutter/ios/ExportOptions/AdHoc.plist @@ -0,0 +1,25 @@ + + + + + method + ad-hoc + destination + export + signingStyle + manual + stripSwiftSymbols + + uploadBitcode + + uploadSymbols + + teamID + ZNVDEVDRX3 + provisioningProfiles + + app.teamspeak.chanora + Chanora_Ad_Hoc + + + \ No newline at end of file diff --git a/apps/chanora_flutter/ios/Flutter/Debug.xcconfig b/apps/chanora_flutter/ios/Flutter/Debug.xcconfig index ec97fc6..6111d9e 100644 --- a/apps/chanora_flutter/ios/Flutter/Debug.xcconfig +++ b/apps/chanora_flutter/ios/Flutter/Debug.xcconfig @@ -1,2 +1,6 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Chanora/Pods-Chanora.debug.xcconfig" #include "Generated.xcconfig" + +// Mirror Release.xcconfig (see explanation there). +OTHER_LDFLAGS = $(inherited) -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string +STRIP_STYLE = non-global diff --git a/apps/chanora_flutter/ios/Flutter/Release.xcconfig b/apps/chanora_flutter/ios/Flutter/Release.xcconfig index c4855bf..f752e6a 100644 --- a/apps/chanora_flutter/ios/Flutter/Release.xcconfig +++ b/apps/chanora_flutter/ios/Flutter/Release.xcconfig @@ -1,2 +1,14 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Chanora/Pods-Chanora.release.xcconfig" #include "Generated.xcconfig" + +// Force the linker to retain Swift @_cdecl symbols that the chanora_bridge +// Rust framework resolves at runtime via dlsym(RTLD_DEFAULT). Without these +// flags, Xcode Archive's -dead_strip removes them because no Swift caller +// exists, and CoreML VAD silently falls back to WebRTC on TestFlight/App Store. +OTHER_LDFLAGS = $(inherited) -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string + +// `STRIP_STYLE = all` (Xcode default for archive installs) runs `strip` without +// `-x`, which removes even the global @_cdecl symbols the linker exported above +// via -exported_symbol. `non-global` runs `strip -x`, preserving globals so the +// Rust framework's dlsym(RTLD_DEFAULT) can find them. 264-byte cost in the app. +STRIP_STYLE = non-global diff --git a/apps/chanora_flutter/ios/Podfile b/apps/chanora_flutter/ios/Podfile index e44216a..a7dfa42 100644 --- a/apps/chanora_flutter/ios/Podfile +++ b/apps/chanora_flutter/ios/Podfile @@ -27,7 +27,7 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup -target 'Runner' do +target 'Chanora' do use_frameworks! # Chanora Rust bridge as a vendored framework. The podspec runs diff --git a/apps/chanora_flutter/ios/Podfile.lock b/apps/chanora_flutter/ios/Podfile.lock index 2e5d79c..42af664 100644 --- a/apps/chanora_flutter/ios/Podfile.lock +++ b/apps/chanora_flutter/ios/Podfile.lock @@ -23,11 +23,11 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/haptic_kit/ios" SPEC CHECKSUMS: - chanora_bridge: 2ed7c2ba427fab135dd9eab66c507b09cfee113a + chanora_bridge: 26252acdf9ca660ce9c132ad25cd5ad5af467b16 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 haptic_kit: b22c4fbb2aa7b0d66f2891f81a9e950ad2de5758 -PODFILE CHECKSUM: e2123068539aeb66d53dc1612b383d13f489ede2 +PODFILE CHECKSUM: 85b93b53f958f1ff700a147e9da4374c8b1c6970 COCOAPODS: 1.16.2 diff --git a/apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj b/apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj index 4d081eb..7191472 100644 --- a/apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/apps/chanora_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -8,19 +8,19 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B4754099284EEDCD859A973 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3EF79A791760D95CE0F41CFF /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63497078A621E2A73B102C46 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 8C5000012DD0000000000001 /* SileroCoreMLBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */; }; 8C5000042DD0000000000001 /* SileroCoreML in Frameworks */ = {isa = PBXBuildFile; productRef = 8C5000032DD0000000000001 /* SileroCoreML */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + C8BACE02E6EE5F840EE3F174 /* Pods_Chanora.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC4F9695FDCB1E0D04E08974 /* Pods_Chanora.framework */; }; FD3C80659716BF7A0C95C7AF /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */; }; - 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -48,10 +48,10 @@ /* Begin PBXFileReference section */ 076D9E9796600FBC91FD7714 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 0B4754099284EEDCD859A973 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 1937FD83C5CC909094CDC137 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 2EA1142FBFD36E2ED564A5AA /* Pods-Chanora.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chanora.release.xcconfig"; path = "Target Support Files/Pods-Chanora/Pods-Chanora.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; @@ -60,20 +60,23 @@ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SileroCoreMLBridge.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7E043103010958FC2C6CA47F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 89E01DD0E6B92DA93A02E9D6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 8C5000002DD0000000000001 /* SileroCoreMLBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SileroCoreMLBridge.swift; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146EE1CF9000F007C117D /* Chanora.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Chanora.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A23C02505CD7E5092CA7958C /* Pods-Chanora.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chanora.profile.xcconfig"; path = "Target Support Files/Pods-Chanora/Pods-Chanora.profile.xcconfig"; sourceTree = ""; }; C10A61C706CAF223682AC397 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + DC4F9695FDCB1E0D04E08974 /* Pods_Chanora.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Chanora.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + EAE6402BFC041304D1D0896D /* Pods-Chanora.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Chanora.debug.xcconfig"; path = "Target Support Files/Pods-Chanora/Pods-Chanora.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,7 +86,7 @@ files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, 8C5000042DD0000000000001 /* SileroCoreML in Frameworks */, - 1E3B5BCCA481234F14E64D44 /* Pods_Runner.framework in Frameworks */, + C8BACE02E6EE5F840EE3F174 /* Pods_Chanora.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -109,8 +112,8 @@ 4351F25046559EFA4C03047A /* Frameworks */ = { isa = PBXGroup; children = ( - 0B4754099284EEDCD859A973 /* Pods_Runner.framework */, 63497078A621E2A73B102C46 /* Pods_RunnerTests.framework */, + DC4F9695FDCB1E0D04E08974 /* Pods_Chanora.framework */, ); name = Frameworks; sourceTree = ""; @@ -124,6 +127,9 @@ 73171B86DD76CC3E5A58E160 /* Pods-RunnerTests.debug.xcconfig */, E469085D9AE850FF6BD35704 /* Pods-RunnerTests.release.xcconfig */, 076D9E9796600FBC91FD7714 /* Pods-RunnerTests.profile.xcconfig */, + EAE6402BFC041304D1D0896D /* Pods-Chanora.debug.xcconfig */, + 2EA1142FBFD36E2ED564A5AA /* Pods-Chanora.release.xcconfig */, + A23C02505CD7E5092CA7958C /* Pods-Chanora.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -155,7 +161,7 @@ 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, + 97C146EE1CF9000F007C117D /* Chanora.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -201,14 +207,15 @@ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 97C146ED1CF9000F007C117D /* Runner */ = { + 97C146ED1CF9000F007C117D /* Chanora */ = { isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Chanora" */; buildPhases = ( 7FE733EE83086540AF5D21CB /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, + CA110001000000000000A100 /* Verify Silero Exports */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, @@ -218,13 +225,13 @@ ); dependencies = ( ); - name = Runner; + name = Chanora; packageProductDependencies = ( 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, 8C5000032DD0000000000001 /* SileroCoreML */, ); productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productReference = 97C146EE1CF9000F007C117D /* Chanora.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -257,14 +264,14 @@ ); mainGroup = 97C146E51CF9000F007C117D; packageReferences = ( - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */, 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */, ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 97C146ED1CF9000F007C117D /* Runner */, + 97C146ED1CF9000F007C117D /* Chanora */, 331C8080294A63A400263BE5 /* RunnerTests */, ); }; @@ -337,15 +344,15 @@ files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Chanora/Pods-Chanora-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + "${PODS_ROOT}/Target Support Files/Pods-Chanora/Pods-Chanora-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Chanora/Pods-Chanora-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 7FE733EE83086540AF5D21CB /* [CP] Check Pods Manifest.lock */ = { @@ -363,7 +370,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Chanora-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -385,6 +392,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + CA110001000000000000A100 /* Verify Silero Exports */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Verify Silero Exports"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../scripts/verify_silero_exports.sh\"\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -412,7 +434,7 @@ /* Begin PBXTargetDependency section */ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; + target = 97C146ED1CF9000F007C117D /* Chanora */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -496,18 +518,23 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 349G7M4TQQ; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 101; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = ZNVDEVDRX3; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Chanora; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter; + PRODUCT_BUNDLE_IDENTIFIER = app.teamspeak.chanora; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "Chanora_iOS_Ad Hoc"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Chanora_iOS_Ad Hoc"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -682,18 +709,23 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 349G7M4TQQ; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 101; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = ZNVDEVDRX3; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Chanora; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter; + PRODUCT_BUNDLE_IDENTIFIER = app.teamspeak.chanora; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = Chanora_ios_Development; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Chanora_ios_Development; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -708,18 +740,23 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 349G7M4TQQ; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 101; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = ZNVDEVDRX3; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Chanora; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = app.chanora.chanoraFlutter; + PRODUCT_BUNDLE_IDENTIFIER = app.teamspeak.chanora; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = "Chanora_App Store"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Chanora_App Store"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -749,7 +786,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Chanora" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, @@ -762,26 +799,26 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../../../silero-coreml; - }; - 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = { isa = XCLocalSwiftPackageReference; relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; }; + 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../silero-coreml"; + }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; 8C5000032DD0000000000001 /* SileroCoreML */ = { isa = XCSwiftPackageProductDependency; package = 8C5000022DD0000000000001 /* XCLocalSwiftPackageReference "silero-coreml" */; productName = SileroCoreML; }; - 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { - isa = XCSwiftPackageProductDependency; - productName = FlutterGeneratedPluginSwiftPackage; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/apps/chanora_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/apps/chanora_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c3fedb2..00b5309 100644 --- a/apps/chanora_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/apps/chanora_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -15,8 +15,8 @@ @@ -33,8 +33,8 @@ @@ -50,8 +50,8 @@ @@ -86,8 +86,8 @@ @@ -103,8 +103,8 @@ diff --git a/apps/chanora_flutter/ios/Runner/AppDelegate.swift b/apps/chanora_flutter/ios/Runner/AppDelegate.swift index 4d851b9..cd738dd 100644 --- a/apps/chanora_flutter/ios/Runner/AppDelegate.swift +++ b/apps/chanora_flutter/ios/Runner/AppDelegate.swift @@ -11,6 +11,10 @@ import AVFoundation _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + DispatchQueue.global(qos: .utility).async { + ChanoraSileroSelfTest.run() + } + // Configure the iOS AVAudioSession **category + mode** at // app-launch time, but DEFER setActive(true) until the scene // is foregrounded. Calling setActive in didFinishLaunching is diff --git a/apps/chanora_flutter/ios/Runner/Info.plist b/apps/chanora_flutter/ios/Runner/Info.plist index 4e60740..057971e 100644 --- a/apps/chanora_flutter/ios/Runner/Info.plist +++ b/apps/chanora_flutter/ios/Runner/Info.plist @@ -4,9 +4,6 @@ CADisableMinimumFrameDurationOnPhone - CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -27,10 +24,16 @@ ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + + NSLocalNetworkUsageDescription + Chanora needs local network access to connect to your voice servers. NSMicrophoneUsageDescription - Chanora needs microphone access so you can talk on your TeamSpeak-compatible voice server. + Chanora needs microphone access so you can talk on your voice server. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -58,8 +61,8 @@ audio - NSLocalNetworkUsageDescription - Chanora needs local network access to connect to TeamSpeak-compatible voice servers. + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -77,9 +80,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIFileSharingEnabled - - LSSupportsOpeningDocumentsInPlace - diff --git a/apps/chanora_flutter/ios/Runner/SileroCoreMLBridge.swift b/apps/chanora_flutter/ios/Runner/SileroCoreMLBridge.swift index 31e1114..3e2823c 100644 --- a/apps/chanora_flutter/ios/Runner/SileroCoreMLBridge.swift +++ b/apps/chanora_flutter/ios/Runner/SileroCoreMLBridge.swift @@ -1,4 +1,5 @@ import CoreML +import Darwin import Foundation import SileroCoreML @@ -96,3 +97,97 @@ public func chanoraSileroVadFreeString(_ string: UnsafeMutablePointer?) { guard let string else { return } free(string) } + +@objc public final class ChanoraSileroSelfTest: NSObject { + // Validates the same code path the Rust framework uses: dlsym(RTLD_DEFAULT) for all + // six @_cdecl symbols, then exercises create -> reset -> process -> destroy. Catches + // the dead-strip / linker-export class of bug that broke TestFlight; calling the Swift + // functions directly would mask it because direct calls bypass the dynamic symbol table. + @objc public static func run() { + let started = DispatchTime.now() + + typealias CreateFn = @convention(c) () -> UnsafeMutableRawPointer? + typealias DestroyFn = @convention(c) (UnsafeMutableRawPointer?) -> Void + typealias ResetFn = @convention(c) (UnsafeMutableRawPointer?) -> Int32 + typealias ProcessFn = @convention(c) ( + UnsafeMutableRawPointer?, UnsafePointer?, Int, UnsafeMutablePointer? + ) -> Int32 + typealias LastErrorFn = @convention(c) () -> UnsafeMutablePointer? + typealias FreeStringFn = @convention(c) (UnsafeMutablePointer?) -> Void + + func resolve(_ name: String, as type: T.Type) -> T? { + guard let raw = dlsym(UnsafeMutableRawPointer(bitPattern: -2), name) else { + return nil + } + return unsafeBitCast(raw, to: type) + } + + let names = [ + "chanora_silero_vad_create", + "chanora_silero_vad_destroy", + "chanora_silero_vad_reset", + "chanora_silero_vad_process", + "chanora_silero_vad_last_error", + "chanora_silero_vad_free_string", + ] + let missing = names.filter { dlsym(UnsafeMutableRawPointer(bitPattern: -2), $0) == nil } + if !missing.isEmpty { + NSLog("chanora_flutter: SileroCoreML self-test FAILED dlsym missing=\(missing.joined(separator: ","))") + return + } + + guard + let create = resolve("chanora_silero_vad_create", as: CreateFn.self), + let destroy = resolve("chanora_silero_vad_destroy", as: DestroyFn.self), + let reset = resolve("chanora_silero_vad_reset", as: ResetFn.self), + let process = resolve("chanora_silero_vad_process", as: ProcessFn.self), + let lastError = resolve("chanora_silero_vad_last_error", as: LastErrorFn.self), + let freeString = resolve("chanora_silero_vad_free_string", as: FreeStringFn.self) + else { + NSLog("chanora_flutter: SileroCoreML self-test FAILED unsafeBitCast resolution") + return + } + + func readError() -> String { + guard let ptr = lastError() else { return "unknown" } + let msg = String(cString: ptr) + freeString(ptr) + return msg + } + + guard let handle = create() else { + let elapsedMs = elapsedMs(since: started) + NSLog("chanora_flutter: SileroCoreML self-test FAILED at create err=\(readError()) elapsed_ms=\(elapsedMs)") + return + } + + let resetRc = reset(handle) + if resetRc != 0 { + destroy(handle) + let elapsedMs = elapsedMs(since: started) + NSLog("chanora_flutter: SileroCoreML self-test FAILED at reset rc=\(resetRc) err=\(readError()) elapsed_ms=\(elapsedMs)") + return + } + + let chunkSize = SileroVADRunner.chunkSize + var probability: Float = 0 + let samples = [Float](repeating: 0, count: chunkSize) + let processRc = samples.withUnsafeBufferPointer { buf -> Int32 in + process(handle, buf.baseAddress, chunkSize, &probability) + } + + destroy(handle) + + let elapsedMs = elapsedMs(since: started) + if processRc == 0 { + NSLog("chanora_flutter: SileroCoreML self-test OK probability=\(probability) elapsed_ms=\(elapsedMs)") + } else { + NSLog("chanora_flutter: SileroCoreML self-test FAILED at process rc=\(processRc) err=\(readError()) elapsed_ms=\(elapsedMs)") + } + } + + private static func elapsedMs(since start: DispatchTime) -> String { + let ns = DispatchTime.now().uptimeNanoseconds &- start.uptimeNanoseconds + return String(format: "%.1f", Double(ns) / 1_000_000.0) + } +} diff --git a/apps/chanora_flutter/ios/chanora_bridge.podspec b/apps/chanora_flutter/ios/chanora_bridge.podspec index 93ca741..fe63f46 100644 --- a/apps/chanora_flutter/ios/chanora_bridge.podspec +++ b/apps/chanora_flutter/ios/chanora_bridge.podspec @@ -85,6 +85,13 @@ Pod::Spec.new do |s| } CARGO_BIN="$(find_cargo)" RUSTC_BIN="$(find_rustc)" + + # Prepend Homebrew's bin dir to PATH so `cmake` (used by + # audiopus_sys's libopus source build) is found. Xcode's + # script_phase PATH sanitisation strips /opt/homebrew/bin, + # which on Apple Silicon hosts is where Homebrew tools live. + export PATH="/opt/homebrew/bin:$PATH" + echo "[chanora_bridge.podspec] cargo build aarch64-apple-ios" cd "$REPO_ROOT" HOME="$USER_HOME" \\ @@ -129,7 +136,21 @@ PLIST install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\ "$FW/chanora_bridge" - echo "[chanora_bridge.podspec] framework ready at $FW" + + # Generate the framework's dSYM bundle. Apple's archive validator + # rejects uploads when an embedded framework has no matching dSYM + # (UUID lookup miss in the archive's dSYMs/ folder), which is the + # failure mode that produced this prepare_command in the first + # place. dsymutil reads the DWARF that cargo emitted (enabled by + # [profile.release] debug = true at the workspace root) and writes + # chanora_bridge.framework.dSYM next to the framework. We then + # strip the in-framework binary so the shipped app stays slim — + # the symbols live exclusively in the dSYM bundle, which is the + # layout xcodebuild -exportArchive and App Store Connect expect. + rm -rf "$FW.dSYM" + xcrun dsymutil "$FW/chanora_bridge" -o "$FW.dSYM" + xcrun strip -S -x "$FW/chanora_bridge" + echo "[chanora_bridge.podspec] framework + dSYM ready at $FW" SCRIPT # Pod CocoaPods picks this up; the framework gets embedded into @@ -185,6 +206,13 @@ PLIST } CARGO_BIN="$(find_cargo)" RUSTC_BIN="$(find_rustc)" + + # Prepend Homebrew's bin dir to PATH so `cmake` (used by + # audiopus_sys's libopus source build) is found. Xcode's + # script_phase PATH sanitisation strips /opt/homebrew/bin, + # which on Apple Silicon hosts is where Homebrew tools live. + export PATH="/opt/homebrew/bin:$PATH" + if [ "${PLATFORM_NAME:-iphoneos}" = "iphonesimulator" ]; then RUST_TARGET="aarch64-apple-ios-sim" SUPPORTED_PLATFORM="iPhoneSimulator" @@ -210,15 +238,19 @@ PLIST # Skip the wrap step if the framework's binary is already # up-to-date with the cargo output (fast no-op on incremental - # builds where Rust didn't change). + # builds where Rust didn't change). We still publish the dSYM + # into DWARF_DSYM_FOLDER_PATH below so archive builds always + # have the symbols, even when the framework itself is cached. + FW_UP_TO_DATE=0 if [ -f "$FW/chanora_bridge" ] && [ "$FW/chanora_bridge" -nt "$BRIDGE" ]; then echo "[chanora_bridge script_phase] framework already up-to-date" - exit 0 + FW_UP_TO_DATE=1 fi - mkdir -p "$FW" - cp "$BRIDGE" "$FW/chanora_bridge" - cat > "$FW/Info.plist" < "$FW/Info.plist" < @@ -234,9 +266,28 @@ PLIST PLIST - install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\ - "$FW/chanora_bridge" - echo "[chanora_bridge script_phase] framework refreshed" + install_name_tool -id "@rpath/chanora_bridge.framework/chanora_bridge" \\ + "$FW/chanora_bridge" + rm -rf "$FW.dSYM" + xcrun dsymutil "$FW/chanora_bridge" -o "$FW.dSYM" + xcrun strip -S -x "$FW/chanora_bridge" + echo "[chanora_bridge script_phase] framework refreshed (with dSYM)" + fi + + # Publish the dSYM into Xcode's archive dSYM folder on every + # build (cached or not). Without this the archive validator + # fails with "archive did not include a dSYM for the + # chanora_bridge.framework with the UUIDs []" and the + # IPA cannot be uploaded to App Store Connect / TestFlight. + # ${DWARF_DSYM_FOLDER_PATH} resolves to /dSYMs for + # archive builds and otherwise; both paths + # are the ones xcodebuild scans when collecting symbols. + if [ -n "${DWARF_DSYM_FOLDER_PATH:-}" ] && [ -d "$FW.dSYM" ]; then + mkdir -p "$DWARF_DSYM_FOLDER_PATH" + rm -rf "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM" + cp -R "$FW.dSYM" "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM" + echo "[chanora_bridge script_phase] dSYM published to $DWARF_DSYM_FOLDER_PATH" + fi SCRIPT :execution_position => :before_compile, } diff --git a/apps/chanora_flutter/macos/Flutter/Flutter-Debug.xcconfig b/apps/chanora_flutter/macos/Flutter/Flutter-Debug.xcconfig index 4b81f9b..0f02383 100644 --- a/apps/chanora_flutter/macos/Flutter/Flutter-Debug.xcconfig +++ b/apps/chanora_flutter/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1,6 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" + +// Mirror Flutter-Release.xcconfig (see explanation there). +OTHER_LDFLAGS = $(inherited) -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string +STRIP_STYLE = non-global diff --git a/apps/chanora_flutter/macos/Flutter/Flutter-Release.xcconfig b/apps/chanora_flutter/macos/Flutter/Flutter-Release.xcconfig index 5caa9d1..856b483 100644 --- a/apps/chanora_flutter/macos/Flutter/Flutter-Release.xcconfig +++ b/apps/chanora_flutter/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1,11 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" + +// macOS Release defaults to DEAD_CODE_STRIPPING = YES. Without these flags the +// Swift @_cdecl symbols below would be stripped because no Swift caller exists; +// the chanora_bridge Rust framework resolves them at runtime via +// dlsym(RTLD_DEFAULT) and would silently fall back to WebRTC VAD. +OTHER_LDFLAGS = $(inherited) -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_create -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_destroy -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_reset -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_process -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_last_error -Xlinker -exported_symbol -Xlinker _chanora_silero_vad_free_string + +// See ios/Flutter/Release.xcconfig for the STRIP_STYLE rationale. +STRIP_STYLE = non-global diff --git a/apps/chanora_flutter/macos/Podfile.lock b/apps/chanora_flutter/macos/Podfile.lock index 8cc430c..6c515d4 100644 --- a/apps/chanora_flutter/macos/Podfile.lock +++ b/apps/chanora_flutter/macos/Podfile.lock @@ -13,7 +13,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral SPEC CHECKSUMS: - chanora_bridge: 4105993843b5421ee4ce72220a74c63f6fd99103 + chanora_bridge: 9d1469952801a1caa3bb56d5d3bce91df8dca4ad FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 PODFILE CHECKSUM: 99f0d126cab50f07c488b8550ebf033d2e8bcaeb diff --git a/apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj b/apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj index b6d02ea..a73e526 100644 --- a/apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/apps/chanora_flutter/macos/Runner.xcodeproj/project.pbxproj @@ -248,6 +248,7 @@ 4287874B577AE59BBE39386D /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, + CA110002000000000000A200 /* Verify Silero Exports */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, @@ -441,6 +442,21 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + CA110002000000000000A200 /* Verify Silero Exports */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Verify Silero Exports"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/../scripts/verify_silero_exports.sh\"\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/apps/chanora_flutter/macos/Runner/AppDelegate.swift b/apps/chanora_flutter/macos/Runner/AppDelegate.swift index a9b7950..4019395 100644 --- a/apps/chanora_flutter/macos/Runner/AppDelegate.swift +++ b/apps/chanora_flutter/macos/Runner/AppDelegate.swift @@ -7,6 +7,10 @@ class AppDelegate: FlutterAppDelegate { override func applicationDidFinishLaunching(_ notification: Notification) { super.applicationDidFinishLaunching(notification) + DispatchQueue.global(qos: .utility).async { + ChanoraSileroSelfTest.run() + } + // Ask for microphone access on launch rather than on first // voice-channel join. Matches user expectations for a voice // chat client and saves the user from a surprising prompt diff --git a/apps/chanora_flutter/macos/Runner/Info.plist b/apps/chanora_flutter/macos/Runner/Info.plist index 1806c0c..00d425c 100644 --- a/apps/chanora_flutter/macos/Runner/Info.plist +++ b/apps/chanora_flutter/macos/Runner/Info.plist @@ -22,6 +22,8 @@ $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright diff --git a/apps/chanora_flutter/macos/Runner/SileroCoreMLBridge.swift b/apps/chanora_flutter/macos/Runner/SileroCoreMLBridge.swift index 31e1114..3e2823c 100644 --- a/apps/chanora_flutter/macos/Runner/SileroCoreMLBridge.swift +++ b/apps/chanora_flutter/macos/Runner/SileroCoreMLBridge.swift @@ -1,4 +1,5 @@ import CoreML +import Darwin import Foundation import SileroCoreML @@ -96,3 +97,97 @@ public func chanoraSileroVadFreeString(_ string: UnsafeMutablePointer?) { guard let string else { return } free(string) } + +@objc public final class ChanoraSileroSelfTest: NSObject { + // Validates the same code path the Rust framework uses: dlsym(RTLD_DEFAULT) for all + // six @_cdecl symbols, then exercises create -> reset -> process -> destroy. Catches + // the dead-strip / linker-export class of bug that broke TestFlight; calling the Swift + // functions directly would mask it because direct calls bypass the dynamic symbol table. + @objc public static func run() { + let started = DispatchTime.now() + + typealias CreateFn = @convention(c) () -> UnsafeMutableRawPointer? + typealias DestroyFn = @convention(c) (UnsafeMutableRawPointer?) -> Void + typealias ResetFn = @convention(c) (UnsafeMutableRawPointer?) -> Int32 + typealias ProcessFn = @convention(c) ( + UnsafeMutableRawPointer?, UnsafePointer?, Int, UnsafeMutablePointer? + ) -> Int32 + typealias LastErrorFn = @convention(c) () -> UnsafeMutablePointer? + typealias FreeStringFn = @convention(c) (UnsafeMutablePointer?) -> Void + + func resolve(_ name: String, as type: T.Type) -> T? { + guard let raw = dlsym(UnsafeMutableRawPointer(bitPattern: -2), name) else { + return nil + } + return unsafeBitCast(raw, to: type) + } + + let names = [ + "chanora_silero_vad_create", + "chanora_silero_vad_destroy", + "chanora_silero_vad_reset", + "chanora_silero_vad_process", + "chanora_silero_vad_last_error", + "chanora_silero_vad_free_string", + ] + let missing = names.filter { dlsym(UnsafeMutableRawPointer(bitPattern: -2), $0) == nil } + if !missing.isEmpty { + NSLog("chanora_flutter: SileroCoreML self-test FAILED dlsym missing=\(missing.joined(separator: ","))") + return + } + + guard + let create = resolve("chanora_silero_vad_create", as: CreateFn.self), + let destroy = resolve("chanora_silero_vad_destroy", as: DestroyFn.self), + let reset = resolve("chanora_silero_vad_reset", as: ResetFn.self), + let process = resolve("chanora_silero_vad_process", as: ProcessFn.self), + let lastError = resolve("chanora_silero_vad_last_error", as: LastErrorFn.self), + let freeString = resolve("chanora_silero_vad_free_string", as: FreeStringFn.self) + else { + NSLog("chanora_flutter: SileroCoreML self-test FAILED unsafeBitCast resolution") + return + } + + func readError() -> String { + guard let ptr = lastError() else { return "unknown" } + let msg = String(cString: ptr) + freeString(ptr) + return msg + } + + guard let handle = create() else { + let elapsedMs = elapsedMs(since: started) + NSLog("chanora_flutter: SileroCoreML self-test FAILED at create err=\(readError()) elapsed_ms=\(elapsedMs)") + return + } + + let resetRc = reset(handle) + if resetRc != 0 { + destroy(handle) + let elapsedMs = elapsedMs(since: started) + NSLog("chanora_flutter: SileroCoreML self-test FAILED at reset rc=\(resetRc) err=\(readError()) elapsed_ms=\(elapsedMs)") + return + } + + let chunkSize = SileroVADRunner.chunkSize + var probability: Float = 0 + let samples = [Float](repeating: 0, count: chunkSize) + let processRc = samples.withUnsafeBufferPointer { buf -> Int32 in + process(handle, buf.baseAddress, chunkSize, &probability) + } + + destroy(handle) + + let elapsedMs = elapsedMs(since: started) + if processRc == 0 { + NSLog("chanora_flutter: SileroCoreML self-test OK probability=\(probability) elapsed_ms=\(elapsedMs)") + } else { + NSLog("chanora_flutter: SileroCoreML self-test FAILED at process rc=\(processRc) err=\(readError()) elapsed_ms=\(elapsedMs)") + } + } + + private static func elapsedMs(since start: DispatchTime) -> String { + let ns = DispatchTime.now().uptimeNanoseconds &- start.uptimeNanoseconds + return String(format: "%.1f", Double(ns) / 1_000_000.0) + } +} diff --git a/apps/chanora_flutter/macos/chanora_bridge.podspec b/apps/chanora_flutter/macos/chanora_bridge.podspec index 9672329..3443252 100644 --- a/apps/chanora_flutter/macos/chanora_bridge.podspec +++ b/apps/chanora_flutter/macos/chanora_bridge.podspec @@ -52,7 +52,7 @@ Pod::Spec.new do |s| echo "[chanora_bridge.podspec] cargo build aarch64-apple-darwin" cd "$REPO_ROOT" - PATH="$HOME/.cargo/bin:$PATH" \\ + PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \\ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \\ CMAKE_POLICY_VERSION_MINIMUM=3.5 \\ LIBOPUS_STATIC=1 \\ @@ -60,7 +60,7 @@ Pod::Spec.new do |s| cargo build --release --target aarch64-apple-darwin -p chanora_bridge echo "[chanora_bridge.podspec] cargo build x86_64-apple-darwin" - PATH="$HOME/.cargo/bin:$PATH" \\ + PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \\ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \\ CMAKE_POLICY_VERSION_MINIMUM=3.5 \\ LIBOPUS_STATIC=1 \\ @@ -112,7 +112,19 @@ PLIST install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \\ "$FW/Versions/A/chanora_bridge" - echo "[chanora_bridge.podspec] framework ready at $FW" + + # Generate the framework's dSYM bundle. Apple's archive validator + # rejects uploads when an embedded framework has no matching dSYM + # (UUID lookup miss in the archive's dSYMs/ folder). dsymutil reads + # the DWARF that cargo emitted (enabled by [profile.release] + # debug = true at the workspace root) and writes the bundle next + # to the framework. We then strip the in-framework binary so the + # shipped app stays slim; symbols live in the dSYM bundle, which + # is the layout xcodebuild -exportArchive and notarisation expect. + rm -rf "$FW.dSYM" + xcrun dsymutil "$FW/Versions/A/chanora_bridge" -o "$FW.dSYM" + xcrun strip -S -x "$FW/Versions/A/chanora_bridge" + echo "[chanora_bridge.podspec] framework + dSYM ready at $FW" SCRIPT # Pod CocoaPods picks this up; the framework gets embedded into @@ -141,7 +153,7 @@ PLIST echo "[chanora_bridge script_phase] cargo build aarch64-apple-darwin" cd "$REPO_ROOT" - PATH="$HOME/.cargo/bin:$PATH" \ + PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \ CMAKE_POLICY_VERSION_MINIMUM=3.5 \ LIBOPUS_STATIC=1 \ @@ -149,7 +161,7 @@ PLIST cargo build --release --target aarch64-apple-darwin -p chanora_bridge echo "[chanora_bridge script_phase] cargo build x86_64-apple-darwin" - PATH="$HOME/.cargo/bin:$PATH" \ + PATH="$HOME/.cargo/bin:/opt/homebrew/opt/rustup/bin:/opt/homebrew/bin:$PATH" \ MACOSX_DEPLOYMENT_TARGET=#{MACOS_BRIDGE_DEPLOYMENT_TARGET} \ CMAKE_POLICY_VERSION_MINIMUM=3.5 \ LIBOPUS_STATIC=1 \ @@ -158,27 +170,31 @@ PLIST cd "$REPO_ROOT/apps/chanora_flutter/macos" FW=Frameworks/chanora_bridge.framework + FW_UP_TO_DATE=0 # Skip the wrap step if the framework's binary is already # up-to-date with the cargo output (fast no-op on incremental - # builds where Rust didn't change). + # builds where Rust didn't change). We still publish the dSYM + # into DWARF_DSYM_FOLDER_PATH below so archive builds always + # have the symbols, even when the framework itself is cached. if [ -f "$FW/Versions/A/chanora_bridge" ] && [ "$FW/Versions/A/chanora_bridge" -nt "$BRIDGE_ARM64" ] && [ "$FW/Versions/A/chanora_bridge" -nt "$BRIDGE_X86_64" ]; then echo "[chanora_bridge script_phase] framework already up-to-date" - exit 0 + FW_UP_TO_DATE=1 fi - # Create universal binary with lipo. - mkdir -p "$(dirname "$UNIVERSAL")" - lipo -create "$BRIDGE_ARM64" "$BRIDGE_X86_64" -output "$UNIVERSAL" + if [ "$FW_UP_TO_DATE" = 0 ]; then + # Create universal binary with lipo. + mkdir -p "$(dirname "$UNIVERSAL")" + lipo -create "$BRIDGE_ARM64" "$BRIDGE_X86_64" -output "$UNIVERSAL" - rm -rf "$FW" - mkdir -p "$FW/Versions/A/Resources" - ln -sfh A "$FW/Versions/Current" - ln -sfh Versions/Current/Resources "$FW/Resources" - cp "$UNIVERSAL" "$FW/Versions/A/chanora_bridge" - ln -sfh Versions/Current/chanora_bridge "$FW/chanora_bridge" + rm -rf "$FW" + mkdir -p "$FW/Versions/A/Resources" + ln -sfh A "$FW/Versions/Current" + ln -sfh Versions/Current/Resources "$FW/Resources" + cp "$UNIVERSAL" "$FW/Versions/A/chanora_bridge" + ln -sfh Versions/Current/chanora_bridge "$FW/chanora_bridge" - cat > "$FW/Versions/A/Resources/Info.plist" < "$FW/Versions/A/Resources/Info.plist" < @@ -195,9 +211,24 @@ PLIST PLIST - install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \ - "$FW/Versions/A/chanora_bridge" - echo "[chanora_bridge script_phase] framework refreshed" + install_name_tool -id "@rpath/chanora_bridge.framework/Versions/A/chanora_bridge" \ + "$FW/Versions/A/chanora_bridge" + rm -rf "$FW.dSYM" + xcrun dsymutil "$FW/Versions/A/chanora_bridge" -o "$FW.dSYM" + xcrun strip -S -x "$FW/Versions/A/chanora_bridge" + echo "[chanora_bridge script_phase] framework refreshed (with dSYM)" + fi + + # Publish the dSYM into Xcode's archive dSYM folder on every + # build (cached or not). See the iOS podspec for the full + # rationale — same constraint applies to macOS notarisation + # and archive-based distribution. + if [ -n "${DWARF_DSYM_FOLDER_PATH:-}" ] && [ -d "$FW.dSYM" ]; then + mkdir -p "$DWARF_DSYM_FOLDER_PATH" + rm -rf "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM" + cp -R "$FW.dSYM" "$DWARF_DSYM_FOLDER_PATH/chanora_bridge.framework.dSYM" + echo "[chanora_bridge script_phase] dSYM published to $DWARF_DSYM_FOLDER_PATH" + fi SCRIPT :execution_position => :before_compile, } diff --git a/apps/chanora_flutter/scripts/verify_silero_exports.sh b/apps/chanora_flutter/scripts/verify_silero_exports.sh new file mode 100755 index 0000000..f205536 --- /dev/null +++ b/apps/chanora_flutter/scripts/verify_silero_exports.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# verify_silero_exports.sh +# +# Asserts that all six chanora_silero_vad_* C symbols that the Rust +# chanora_bridge framework resolves via dlsym(RTLD_DEFAULT) are +# present in the linked app binary's dynamic export table. +# +# Without this check, Xcode Archive's -dead_strip can silently +# remove these Swift @_cdecl symbols because no Swift caller exists, +# and CoreML VAD falls back to WebRTC on TestFlight/App Store with +# no compile-time, link-time, or runtime warning. We hit that bug +# once; this script ensures we never ship it again. +# +# Runs as an Xcode build phase after Link Binary, on iOS and macOS. + +set -e + +# Xcode passes these. Outside Xcode, the caller must export them. +if [ -z "${TARGET_BUILD_DIR}" ] || [ -z "${EXECUTABLE_PATH}" ]; then + echo "error: verify_silero_exports.sh requires TARGET_BUILD_DIR and EXECUTABLE_PATH (run from Xcode build phase)" >&2 + exit 1 +fi + +BINARY="${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}" + +if [ ! -f "${BINARY}" ]; then + echo "error: app binary not found at ${BINARY}" >&2 + exit 1 +fi + +REQUIRED_SYMBOLS=" +_chanora_silero_vad_create +_chanora_silero_vad_destroy +_chanora_silero_vad_reset +_chanora_silero_vad_process +_chanora_silero_vad_last_error +_chanora_silero_vad_free_string +" + +EXPORTED=$(xcrun nm -gU "${BINARY}" 2>/dev/null | awk '{print $NF}') + +MISSING="" +for sym in ${REQUIRED_SYMBOLS}; do + if ! echo "${EXPORTED}" | grep -qx "${sym}"; then + MISSING="${MISSING} ${sym}" + fi +done + +if [ -n "${MISSING}" ]; then + echo "error: chanora_silero_vad exports missing from $(basename "${BINARY}"):${MISSING}" >&2 + echo "error: Rust dlsym(RTLD_DEFAULT) resolution will fail and CoreML VAD will fall back to WebRTC." >&2 + echo "error: Check OTHER_LDFLAGS -exported_symbol entries in Flutter/*.xcconfig and the @_cdecl exports in Runner/SileroCoreMLBridge.swift." >&2 + exit 1 +fi + +echo "verify_silero_exports: all 6 chanora_silero_vad_* symbols present in $(basename "${BINARY}")"