From c86dcd1bbfc923a515bd1bfc4c3ee7c4c5ea4e81 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Thu, 28 May 2026 18:56:54 -0400 Subject: [PATCH 1/8] ci(ios): package .app as .ipa before artifact upload GitHub Actions upload-artifact wraps directory inputs in a zip, so uploading QGroundControl.app directly produces a .zip download. Package the .app into a proper IPA (Payload/App.app zipped) first so the artifact is a single file with the correct iOS distribution format. --- .github/workflows/ios.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index cd86744da7a8..c10bfd90948f 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -82,12 +82,22 @@ jobs: build-dir: ${{ runner.temp }}/build build-type: ${{ matrix.build_type }} + - name: Package IPA + if: matrix.build_type == 'Release' + run: | + APP_DIR="${{ runner.temp }}/build/${{ matrix.build_type }}" + cd "$APP_DIR" + mkdir -p Payload + cp -r "${{ env.PACKAGE }}.app" Payload/ + zip -r "${{ env.PACKAGE }}.ipa" Payload/ + rm -rf Payload + - name: Attest and Upload if: matrix.build_type == 'Release' uses: ./.github/actions/attest-and-upload with: - artifact-name: ${{ env.PACKAGE }}.app - # iOS uses Ninja Multi-Config; the .app lands in build//, not build/. - artifact-source-path: ${{ runner.temp }}/build/${{ matrix.build_type }}/${{ env.PACKAGE }}.app + artifact-name: ${{ env.PACKAGE }}.ipa + # iOS uses Ninja Multi-Config; the .ipa lands in build//, not build/. + artifact-source-path: ${{ runner.temp }}/build/${{ matrix.build_type }}/${{ env.PACKAGE }}.ipa package-name: ${{ env.PACKAGE }} upload-aws: 'false' From ac0211d0d3ebdcb0178ae58fccca486fd049c523 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Thu, 28 May 2026 19:38:44 -0400 Subject: [PATCH 2/8] cmake(ios): use iOS Info.plist for Ninja generator builds MACOSX_BUNDLE_INFO_PLIST was unconditionally set to the macOS plist. The iOS override only set XCODE_ATTRIBUTE_INFOPLIST_FILE, which is Xcode-generator-only. With Ninja, the bundle got the macOS Info.plist (LSMinimumSystemVersion instead of LSRequiresIPhoneOS), causing sideloading tools to reject it as an invalid iOS app. --- cmake/platform/Apple.cmake | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmake/platform/Apple.cmake b/cmake/platform/Apple.cmake index 4abb2bb7d5a5..4b521abe62ab 100644 --- a/cmake/platform/Apple.cmake +++ b/cmake/platform/Apple.cmake @@ -29,9 +29,15 @@ endif() # ---------------------------------------------------------------------------- cmake_path(GET QGC_MACOS_ICON_PATH FILENAME MACOSX_BUNDLE_ICON_FILE) +if(IOS) + set(_qgc_bundle_plist "${CMAKE_SOURCE_DIR}/deploy/ios/iOS-Info.plist") +else() + set(_qgc_bundle_plist "${QGC_MACOS_PLIST_PATH}") +endif() + set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES - MACOSX_BUNDLE_INFO_PLIST "${QGC_MACOS_PLIST_PATH}" + MACOSX_BUNDLE_INFO_PLIST "${_qgc_bundle_plist}" MACOSX_BUNDLE_BUNDLE_NAME "${CMAKE_PROJECT_NAME}" MACOSX_BUNDLE_BUNDLE_VERSION "${CMAKE_PROJECT_VERSION}" MACOSX_BUNDLE_COPYRIGHT "${QGC_APP_COPYRIGHT}" From 5bc020ddffd8d1ff031d14c4e69a7b0e8da99340 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Thu, 28 May 2026 20:12:52 -0400 Subject: [PATCH 3/8] cmake(ios): add CMake-variable plist template for Ninja generator iOS-Info.plist uses Xcode build variables (e.g. $(EXECUTABLE_NAME)) that Xcode substitutes at build time but Ninja leaves as literal strings, causing sideloading tools to fail finding the executable. Add iOSBundleInfo.plist.in using CMake-style ${VAR} substitution for Ninja builds. Xcode generator continues using iOS-Info.plist via XCODE_ATTRIBUTE_INFOPLIST_FILE unchanged. --- cmake/platform/Apple.cmake | 2 +- deploy/ios/iOSBundleInfo.plist.in | 67 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 deploy/ios/iOSBundleInfo.plist.in diff --git a/cmake/platform/Apple.cmake b/cmake/platform/Apple.cmake index 4b521abe62ab..f5825cb41154 100644 --- a/cmake/platform/Apple.cmake +++ b/cmake/platform/Apple.cmake @@ -30,7 +30,7 @@ endif() cmake_path(GET QGC_MACOS_ICON_PATH FILENAME MACOSX_BUNDLE_ICON_FILE) if(IOS) - set(_qgc_bundle_plist "${CMAKE_SOURCE_DIR}/deploy/ios/iOS-Info.plist") + set(_qgc_bundle_plist "${CMAKE_SOURCE_DIR}/deploy/ios/iOSBundleInfo.plist.in") else() set(_qgc_bundle_plist "${QGC_MACOS_PLIST_PATH}") endif() diff --git a/deploy/ios/iOSBundleInfo.plist.in b/deploy/ios/iOSBundleInfo.plist.in new file mode 100644 index 000000000000..efb725882832 --- /dev/null +++ b/deploy/ios/iOSBundleInfo.plist.in @@ -0,0 +1,67 @@ + + + + + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleDisplayName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + CFBundleDevelopmentRegion + en + + LSRequiresIPhoneOS + + MinimumOSVersion + ${QGC_IOS_DEPLOYMENT_TARGET} + + UIRequiresFullScreen + + UILaunchStoryboardName + QGCLaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + UIFileSharingEnabled + + LSSupportsOpeningDocumentsInPlace + + + NSCameraUsageDescription + QGC uses UVC devices for video streaming. + NSMicrophoneUsageDescription + Qt Multimedia for iOS uses the camera and microphone. + NSLocationUsageDescription + Ground Station Location + NSLocationWhenInUseUsageDescription + Ground Station Location + NSBluetoothAlwaysUsageDescription + Bluetooth is used to connect to vehicles + + From d5cb6548a4f929a34f99d01cb3b92b1d010c6e28 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Thu, 28 May 2026 21:15:12 -0400 Subject: [PATCH 4/8] cmake(ios): embed FFmpeg xcframeworks for Ninja generator builds With Ninja generator, Xcode's automatic 'Embed Frameworks' build phase doesn't run, so FFmpeg xcframeworks are not copied into the app bundle. The binary's rpath contains the CI runner's absolute Qt path which doesn't exist on device, causing a DYLD crash at launch. Add a POST_BUILD command to copy xcframeworks from Qt's ffmpeg directory into the app bundle's Frameworks/ directory, and set BUILD_RPATH to @executable_path/Frameworks so dyld resolves them correctly on device. --- cmake/platform/Apple.cmake | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cmake/platform/Apple.cmake b/cmake/platform/Apple.cmake index f5825cb41154..16c942a555ae 100644 --- a/cmake/platform/Apple.cmake +++ b/cmake/platform/Apple.cmake @@ -96,5 +96,41 @@ elseif(IOS) qt_add_ios_ffmpeg_libraries(${CMAKE_PROJECT_NAME}) endif() + # With Ninja generator, Xcode's "Embed Frameworks" build phase doesn't run. + # Manually copy FFmpeg xcframeworks into the bundle and set rpath so dyld + # finds them on device instead of the CI runner's absolute Qt path. + if(NOT CMAKE_GENERATOR MATCHES "Xcode") + cmake_path(GET Qt6_DIR PARENT_PATH _qt_cmake_dir) + cmake_path(GET _qt_cmake_dir PARENT_PATH _qt_lib_dir) + set(_ffmpeg_xcfw_dir "${_qt_lib_dir}/ffmpeg") + + if(EXISTS "${_ffmpeg_xcfw_dir}") + file(GLOB _xcframeworks LIST_DIRECTORIES true "${_ffmpeg_xcfw_dir}/*.xcframework") + foreach(_xcfw IN LISTS _xcframeworks) + cmake_path(GET _xcfw STEM _fw_name) + foreach(_slice ios-arm64 ios-arm64_arm64e) + set(_fw_src "${_xcfw}/${_slice}/${_fw_name}.framework") + if(EXISTS "${_fw_src}") + add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${_fw_src}" + "$/Frameworks/${_fw_name}.framework" + COMMENT "Embedding ${_fw_name}.framework" + VERBATIM + ) + break() + endif() + endforeach() + endforeach() + message(STATUS "QGC: FFmpeg xcframeworks will be embedded at build time (Ninja)") + else() + message(STATUS "QGC: No FFmpeg xcframeworks found at ${_ffmpeg_xcfw_dir}") + endif() + + set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES + BUILD_RPATH "@executable_path/Frameworks" + ) + endif() + message(STATUS "QGC: iOS platform configuration applied") endif() From 4de2b0691241f38b3b7b7c5afe84fc26f07cb4da Mon Sep 17 00:00:00 2001 From: FARHANG Date: Thu, 28 May 2026 21:42:52 -0400 Subject: [PATCH 5/8] cmake(ios): embed GStreamerMobile framework for Ninja generator builds GStreamerMobile is a project-built SHARED framework. With Ninja generator, it is not automatically copied into the app bundle. Add a POST_BUILD command in FindGStreamerMobile to copy it into QGroundControl.app/Frameworks/ on iOS non-Xcode builds. --- cmake/find-modules/FindGStreamerMobile.cmake | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmake/find-modules/FindGStreamerMobile.cmake b/cmake/find-modules/FindGStreamerMobile.cmake index 98944d46aed9..344b36858202 100644 --- a/cmake/find-modules/FindGStreamerMobile.cmake +++ b/cmake/find-modules/FindGStreamerMobile.cmake @@ -238,6 +238,17 @@ if (GSTREAMER_IS_MOBILE AND (NOT TARGET GStreamer::mobile)) FRAMEWORK_VERSION A MACOSX_FRAMEWORK_IDENTIFIER org.gstreamer.GStreamerMobile ) + # With Ninja generator, Xcode's "Embed Frameworks" build phase doesn't run. + # Copy the built framework into the app bundle so dyld finds it on device. + if(IOS AND NOT CMAKE_GENERATOR MATCHES "Xcode") + add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "$" + "$/Frameworks/${GStreamer_Mobile_MODULE_NAME}.framework" + COMMENT "Embedding ${GStreamer_Mobile_MODULE_NAME}.framework" + VERBATIM + ) + endif() else() set_target_properties( GStreamerMobile From 4c9370ec596e47229dcd3d321fc637388829acf2 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Thu, 28 May 2026 22:24:42 -0400 Subject: [PATCH 6/8] cmake(ios): defer GStreamerMobile framework embedding for Ninja builds The previous attempt added add_custom_command in FindGStreamerMobile.cmake, but that runs before add_subdirectory(src) creates the GStreamerMobile target so CMake silently ignored it. Use cmake_language(DEFER DIRECTORY root ...) in Apple.cmake so the post-build copy is registered after all subdirectories are processed and the GStreamerMobile target exists. Use TARGET_FILE_DIR instead of TARGET_BUNDLE_DIR for the framework source path as it reliably resolves to the .framework directory on iOS. --- cmake/find-modules/FindGStreamerMobile.cmake | 11 ----------- cmake/platform/Apple.cmake | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/cmake/find-modules/FindGStreamerMobile.cmake b/cmake/find-modules/FindGStreamerMobile.cmake index 344b36858202..98944d46aed9 100644 --- a/cmake/find-modules/FindGStreamerMobile.cmake +++ b/cmake/find-modules/FindGStreamerMobile.cmake @@ -238,17 +238,6 @@ if (GSTREAMER_IS_MOBILE AND (NOT TARGET GStreamer::mobile)) FRAMEWORK_VERSION A MACOSX_FRAMEWORK_IDENTIFIER org.gstreamer.GStreamerMobile ) - # With Ninja generator, Xcode's "Embed Frameworks" build phase doesn't run. - # Copy the built framework into the app bundle so dyld finds it on device. - if(IOS AND NOT CMAKE_GENERATOR MATCHES "Xcode") - add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "$" - "$/Frameworks/${GStreamer_Mobile_MODULE_NAME}.framework" - COMMENT "Embedding ${GStreamer_Mobile_MODULE_NAME}.framework" - VERBATIM - ) - endif() else() set_target_properties( GStreamerMobile diff --git a/cmake/platform/Apple.cmake b/cmake/platform/Apple.cmake index 16c942a555ae..e278c5c665bd 100644 --- a/cmake/platform/Apple.cmake +++ b/cmake/platform/Apple.cmake @@ -67,6 +67,19 @@ if(MACOS) message(STATUS "QGC: macOS platform configuration applied") elseif(IOS) + function(_qgc_ios_embed_gstreamer_mobile target) + if(NOT TARGET GStreamerMobile) + return() + endif() + add_custom_command(TARGET "${target}" POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "$" + "$/Frameworks/gstreamer_mobile.framework" + COMMENT "Embedding gstreamer_mobile.framework" + VERBATIM + ) + message(STATUS "QGC: GStreamerMobile will be embedded at build time (Ninja)") + endfunction() # iOS-specific configuration # set(CMAKE_XCODE_ATTRIBUTE_ARCHS @@ -130,6 +143,13 @@ elseif(IOS) set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES BUILD_RPATH "@executable_path/Frameworks" ) + + # GStreamerMobile is created by find_package(GStreamerMobile) inside + # add_subdirectory(src), which hasn't run yet. Defer the post-build + # copy until after all subdirectories are processed. + set(_qgc_target "${CMAKE_PROJECT_NAME}") + cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" + CALL _qgc_ios_embed_gstreamer_mobile "${_qgc_target}") endif() message(STATUS "QGC: iOS platform configuration applied") From 3868fdf9925ec0ac5519dd018fd710cea8c609d2 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Fri, 29 May 2026 13:47:30 -0400 Subject: [PATCH 7/8] gstreamer(ios): disable libav plugin on iOS gst_ffmpeg_cfg_init hits a g_assert failure when the GStreamer libav plugin initializes on iOS, aborting the app at launch. iOS has hardware video decoding via applemedia, so libav/FFmpeg software decoding via GStreamer is not needed. Guard GST_PLUGIN_STATIC_DECLARE/REGISTER(libav) with Q_OS_IOS in GStreamer.cc, and remove libav from the iOS GStreamer plugin list in FindQGCGStreamer.cmake so it is not linked into gstreamer_mobile. --- cmake/find-modules/FindQGCGStreamer.cmake | 3 +++ src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/cmake/find-modules/FindQGCGStreamer.cmake b/cmake/find-modules/FindQGCGStreamer.cmake index e3a75afc7fbc..5cd2669942ba 100644 --- a/cmake/find-modules/FindQGCGStreamer.cmake +++ b/cmake/find-modules/FindQGCGStreamer.cmake @@ -808,6 +808,9 @@ if(NOT DEFINED GSTREAMER_PLUGINS) # Deferred for all platforms — GStreamer_VERSION is populated after find_package(GStreamer) below. if(ANDROID) list(APPEND GSTREAMER_PLUGINS androidmedia dav1d) + elseif(IOS) + list(REMOVE_ITEM GSTREAMER_PLUGINS libav) + list(APPEND GSTREAMER_PLUGINS applemedia) elseif(APPLE) list(APPEND GSTREAMER_PLUGINS applemedia dav1d vulkan) elseif(WIN32) diff --git a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc index a6d9ce2b407b..aa4ff3dd0753 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GStreamer.cc @@ -48,7 +48,9 @@ G_BEGIN_DECLS GST_PLUGIN_STATIC_DECLARE(app); GST_PLUGIN_STATIC_DECLARE(coreelements); GST_PLUGIN_STATIC_DECLARE(isomp4); +#ifndef Q_OS_IOS GST_PLUGIN_STATIC_DECLARE(libav); +#endif GST_PLUGIN_STATIC_DECLARE(matroska); GST_PLUGIN_STATIC_DECLARE(mpegtsdemux); GST_PLUGIN_STATIC_DECLARE(openh264); @@ -128,7 +130,9 @@ void _registerPlugins() GST_PLUGIN_STATIC_REGISTER(app); GST_PLUGIN_STATIC_REGISTER(coreelements); GST_PLUGIN_STATIC_REGISTER(isomp4); +#ifndef Q_OS_IOS GST_PLUGIN_STATIC_REGISTER(libav); +#endif GST_PLUGIN_STATIC_REGISTER(matroska); GST_PLUGIN_STATIC_REGISTER(mpegtsdemux); GST_PLUGIN_STATIC_REGISTER(openh264); From 24f1f16754e5afd4c251ff31dfb7d4b35d37ff54 Mon Sep 17 00:00:00 2001 From: FARHANG Date: Fri, 29 May 2026 14:25:31 -0400 Subject: [PATCH 8/8] gstreamer(ios): remove applemedia plugin on iOS applemedia directly interfaces with Apple media APIs that change between iOS versions. On iOS 26 it appears to corrupt the GStreamer element factory registry, causing a SIGSEGV in analyze_new_pad via gst_element_factory_get_metadata with a bad pointer. Remove it to use software decoding path instead. --- cmake/find-modules/FindQGCGStreamer.cmake | 1 - 1 file changed, 1 deletion(-) diff --git a/cmake/find-modules/FindQGCGStreamer.cmake b/cmake/find-modules/FindQGCGStreamer.cmake index 5cd2669942ba..0739109c0875 100644 --- a/cmake/find-modules/FindQGCGStreamer.cmake +++ b/cmake/find-modules/FindQGCGStreamer.cmake @@ -810,7 +810,6 @@ if(NOT DEFINED GSTREAMER_PLUGINS) list(APPEND GSTREAMER_PLUGINS androidmedia dav1d) elseif(IOS) list(REMOVE_ITEM GSTREAMER_PLUGINS libav) - list(APPEND GSTREAMER_PLUGINS applemedia) elseif(APPLE) list(APPEND GSTREAMER_PLUGINS applemedia dav1d vulkan) elseif(WIN32)