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