diff --git a/.github/actions/build-setup/action.yml b/.github/actions/build-setup/action.yml index d1e60e63bf6b..0aa842af9c53 100644 --- a/.github/actions/build-setup/action.yml +++ b/.github/actions/build-setup/action.yml @@ -217,6 +217,7 @@ runs: android-cmdline-tools: ${{ steps.prereq.outputs.android_cmdline_tools }} android-build-tools: ${{ steps.prereq.outputs.android_build_tools }} save-cache: ${{ inputs.save-cache }} + aqt-source: ${{ inputs.aqt-source }} - name: Install Qt for iOS if: inputs.mode == 'ios' diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml index 77fc5927620b..94a050ff7083 100644 --- a/.github/actions/install-dependencies/action.yml +++ b/.github/actions/install-dependencies/action.yml @@ -90,10 +90,13 @@ runs: timeout_minutes: 20 max_attempts: 3 shell: bash + # Acquire::*::Timeout aborts a stalled mirror connection so retries can fire. command: | - sudo apt-get -o Acquire::Retries=3 update -qq + APT_OPTS="-o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30" # shellcheck disable=SC2086 - sudo apt-get -o Acquire::Retries=3 install -y --no-install-recommends $APT_PACKAGES + sudo apt-get $APT_OPTS update -qq + # shellcheck disable=SC2086 + sudo apt-get $APT_OPTS install -y --no-install-recommends $APT_PACKAGES - name: Fix apt alternatives (Linux) if: runner.os == 'Linux' diff --git a/.github/actions/qt-android/action.yml b/.github/actions/qt-android/action.yml index 5f7f32108383..8b433d7f4f35 100644 --- a/.github/actions/qt-android/action.yml +++ b/.github/actions/qt-android/action.yml @@ -50,6 +50,10 @@ inputs: description: "Whether to save cache (auto = save for pushes and same-repo PRs, skip for fork PRs)" required: false default: 'auto' + aqt-source: + description: Optional pip spec used to install aqtinstall (e.g. git+https://github.com/miurahr/aqtinstall.git@) + required: false + default: '' runs: using: composite steps: @@ -93,6 +97,7 @@ runs: arch: ${{ inputs.arch }} dir: ${{ runner.temp }} modules: ${{ inputs.modules }} + aqt-source: ${{ inputs.aqt-source }} - name: Install Qt for Android (arm64_v8a) if: contains(format(';{0};', inputs.abis), ';arm64-v8a;') @@ -106,6 +111,7 @@ runs: dir: ${{ runner.temp }} modules: ${{ inputs.modules }} export-env: 'false' + aqt-source: ${{ inputs.aqt-source }} - name: Install Qt for Android (armv7) if: contains(format(';{0};', inputs.abis), ';armeabi-v7a;') @@ -119,6 +125,7 @@ runs: dir: ${{ runner.temp }} modules: ${{ inputs.modules }} export-env: 'false' + aqt-source: ${{ inputs.aqt-source }} - name: Install Qt for Android (x86_64) if: contains(format(';{0};', inputs.abis), ';x86_64;') @@ -132,6 +139,7 @@ runs: dir: ${{ runner.temp }} modules: ${{ inputs.modules }} export-env: 'false' + aqt-source: ${{ inputs.aqt-source }} - name: Install Qt for Android (x86) if: contains(format(';{0};', inputs.abis), ';x86;') @@ -145,6 +153,7 @@ runs: dir: ${{ runner.temp }} modules: ${{ inputs.modules }} export-env: 'false' + aqt-source: ${{ inputs.aqt-source }} - name: Resolve primary Android Qt root id: qt-target diff --git a/.github/build-config.json b/.github/build-config.json index 09a1fa8c8f5d..7e776cdbf4e6 100644 --- a/.github/build-config.json +++ b/.github/build-config.json @@ -3,9 +3,9 @@ "android_build_tools": "36.0.0", "ccache_version": "4.13.6", "mold_version": "2.41.0", - "android_cmdline_tools": "13114758", + "android_cmdline_tools": "14742923", "android_min_sdk": "28", - "android_platform": "35", + "android_platform": "36", "cmake_minimum_version": "3.25", "gstreamer_android_version": "1.28.1", "gstreamer_ios_version": "1.28.1", @@ -13,15 +13,15 @@ "gstreamer_minimum_version": "1.20.0", "gstreamer_default_version": "1.28.1", "gstreamer_windows_version": "1.28.1", - "ios_deployment_target": "14.0", - "java_version": "17", + "ios_deployment_target": "17.0", + "java_version": "21", "macos_deployment_target": "13.0", "ndk_full_version": "27.2.12479018", "platform_workflows": "Linux,Windows,MacOS,Android", "ndk_version": "r27c", - "qt_minimum_version": "6.10.0", + "qt_minimum_version": "6.11.0", "qt_modules": "qtgraphs qtlocation qtpositioning qtspeech qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml qtwebsockets qthttpserver", - "qt_version": "6.10.3", + "qt_version": "6.11.1", "vulkan_sdk_version": "1.4.304.1", "xcode_ios_version": "latest-stable", "xcode_version": "16.x", diff --git a/.github/scripts/cmake_helper.py b/.github/scripts/cmake_helper.py index 51ca7d355aa2..321e88fc65f3 100644 --- a/.github/scripts/cmake_helper.py +++ b/.github/scripts/cmake_helper.py @@ -144,6 +144,18 @@ def cmd_configure(args: argparse.Namespace) -> None: sys.exit(result.returncode) +def _maybe_wrap_xvfb(cmd: list[str]) -> list[str]: + """Prefix `xvfb-run` on headless Linux so the offscreen plugin's GLX path + gets a display and QRhiGles2 can create a software GL context.""" + if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): + xvfb = shutil.which("xvfb-run") + if xvfb: + print("::notice::No DISPLAY; running CTest under xvfb-run") + return [xvfb, "-a", *cmd] + print("::warning::xvfb-run not found; tests run without a GL context") + return cmd + + def cmd_ctest(args: argparse.Namespace) -> None: """Run CTest with standardized arguments and timing.""" cmd = [ @@ -166,7 +178,7 @@ def cmd_ctest(args: argparse.Namespace) -> None: cmd += ["-I", f"{start_idx},0,{args.shard_count}"] start = time.monotonic() - exit_code = _run_with_tee(cmd, args.ctest_output) + exit_code = _run_with_tee(_maybe_wrap_xvfb(cmd), args.ctest_output) duration = int(time.monotonic() - start) gh_notice(f"Tests completed in {duration}s") sys.exit(exit_code) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 32cd7013a7f6..d35788c35b94 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -70,6 +70,7 @@ jobs: shell: pwsh primary: false emulator: false + aqt_source: 'git+https://github.com/miurahr/aqtinstall.git@8d961e61720b3c06583517fbc68d57ec0a4e9f95' - host: linux-emulator qt_host: linux runner: ${{ github.repository_owner == 'mavlink' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('runs-on={0}/runner=linux-x64-emulator', github.run_id) || 'ubuntu-latest' }} @@ -131,6 +132,7 @@ jobs: mode: android qt-host: ${{ matrix.qt_host || matrix.host }} qt-arch: ${{ matrix.arch }} + aqt-source: ${{ matrix.aqt_source || '' }} abis: ${{ env.QT_ANDROID_ABIS }} build-type: ${{ env.BUILD_TYPE }} cpm-modules: ${{ runner.temp }}/build/cpm_modules diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index bf9155346518..d6b353f42846 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -32,6 +32,9 @@ permissions: attestations: write actions: read +env: + AQT_SOURCE: 'git+https://github.com/miurahr/aqtinstall.git@8d961e61720b3c06583517fbc68d57ec0a4e9f95' + jobs: changes: uses: ./.github/workflows/_detect-changes.yml @@ -68,7 +71,6 @@ jobs: host: windows arch: win64_msvc2022_arm64_cross_compiled package: QGroundControl-installer-AMD64-ARM64 - aqt_source: 'git+https://github.com/miurahr/aqtinstall.git@c84e1470349e5bedbeee977bd62ec495d50d65b6' defaults: run: @@ -107,7 +109,7 @@ jobs: with: qt-host: ${{ matrix.host }} qt-arch: ${{ matrix.arch }} - aqt-source: ${{ matrix.aqt_source || '' }} + aqt-source: ${{ env.AQT_SOURCE }} build-type: ${{ matrix.build_type }} - name: Configure diff --git a/.lychee.toml b/.lychee.toml index edf8e6a0b7ec..7b8da877dc46 100644 --- a/.lychee.toml +++ b/.lychee.toml @@ -27,6 +27,9 @@ exclude = [ '^https?://review\.px4\.io', '^https?://discuss\.px4\.io', + # Dynamic build artifact (CDN returns 502 when build is mid-publish) + '^https://d176tv9ibo4jno\.cloudfront\.net/', + # Dead/restructured external sites '^https?://www\.cplusplus\.com/', '^https?://gaming\.logitech\.com/', diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index cc1a861ae551..21837c60fa03 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -81,13 +81,9 @@ - - - - candidates = [] - if (project.hasProperty('QGC_ANDROID_PROPERTIES_FILE')) { - candidates.add(file(project.property('QGC_ANDROID_PROPERTIES_FILE').toString())) - } - final String envPath = System.getenv('QGC_ANDROID_PROPERTIES_FILE') - if (envPath != null && !envPath.isEmpty()) { - candidates.add(file(envPath)) - } - - // Default: look in the parent of the Gradle project dir (CMAKE_BINARY_DIR) - // androiddeployqt copies this build.gradle into /android-build/, - // while CMake generates the properties file in /. - candidates.add(new File(project.projectDir.parentFile, 'qgc-android-config.properties')) - - for (File candidate : candidates) { - if (!candidate.exists()) { - continue - } - - candidate.withInputStream { stream -> props.load(stream) } - println "qgcAndroidConfig: ${candidate.absolutePath}" - final List requiredKeys = [ - 'QGC_ANDROID_COMPILE_SDK_VERSION', - 'QGC_ANDROID_TARGET_SDK_VERSION', - 'QGC_ANDROID_MIN_SDK_VERSION' - ] - - for (String key : requiredKeys) { - final String value = props.getProperty(key) - if (value == null || value.isEmpty()) { - throw new GradleException("qgcAndroidConfig missing required key '${key}' in ${candidate.absolutePath}") - } - } - - return props - } - - throw new GradleException( - "Missing qgc-android-config.properties. Configure with CMake (qt-cmake) " + - "or pass -PQGC_ANDROID_PROPERTIES_FILE=/path/to/qgc-android-config.properties" - ) -} - -final Properties qgcAndroidConfig = loadQgcAndroidConfig() - -def qgcConfigOrDefault = { String key, Object fallback -> - final String value = qgcAndroidConfig.getProperty(key) - if (value != null && !value.isEmpty()) { - return value - } - return fallback -} - def resolveCpmJavaSourceDir = { - final List candidates = [] - final String sharedConfigPath = qgcAndroidConfig.getProperty('QGC_CPM_JAVA_SRC_DIR') - if (sharedConfigPath != null && !sharedConfigPath.isEmpty()) { - candidates.add(file(sharedConfigPath)) - } - if (project.hasProperty('QGC_CPM_JAVA_SRC_DIR')) { - candidates.add(file(project.property('QGC_CPM_JAVA_SRC_DIR').toString())) - } - if (project.hasProperty('cpmJavaSrcDir')) { - candidates.add(file(project.property('cpmJavaSrcDir').toString())) - } - final String envPath = System.getenv('QGC_EXTRA_JAVA_SOURCES_DIR') - if (envPath != null && !envPath.isEmpty()) { - candidates.add(file(envPath)) - } - - for (File candidate : candidates) { - if (candidate.exists()) { - return candidate - } - } - - return null + // CMake copies CPM Java dependencies into /extra_java_sources. + // androiddeployqt copies this build.gradle into /android-build-*/, + // so the CPM sources are the sibling 'extra_java_sources' of the Gradle project dir. + final File cpmDir = new File(project.projectDir.parentFile, 'extra_java_sources') + return cpmDir.exists() ? cpmDir : null } android { @@ -125,20 +49,15 @@ android { * Changing them manually might break the compilation! *******************************************************/ - // namespace = androidPackageName - namespace = "org.mavlink.qgroundcontrol" - compileSdk qgcConfigOrDefault('QGC_ANDROID_COMPILE_SDK_VERSION', androidCompileSdkVersion).toInteger() + namespace androidPackageName + compileSdkVersion androidCompileSdkVersion buildToolsVersion androidBuildToolsVersion - ndkVersion = androidNdkVersion - - // Extract native libraries from the APK - // packagingOptions.jniLibs.useLegacyPackaging = true + ndkVersion androidNdkVersion sourceSets { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'].tap { - // CPM Java dependencies deployed to the build tree by CMake def cpmJava = resolveCpmJavaSourceDir() if (cpmJava != null) { it.add(cpmJava.absolutePath) @@ -147,16 +66,15 @@ android { println "cpmJavaSrcDir: " } } - // Exclude JUnit test sources from the main source set — they are - // picked up via 'src' above but belong to the test configuration. java.filter.exclude 'test/**' + kotlin.srcDirs = [qtAndroidDir + '/src', 'src', 'kotlin'] aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] res.srcDirs = [qtAndroidDir + '/res', 'res'] resources.srcDirs = ['resources'] renderscript.srcDirs = ['src'] assets.srcDirs = ['assets'] jniLibs.srcDirs = ['libs'] - } + } test { java.srcDirs = ['src/test/java'] } @@ -172,50 +90,48 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 } lint { abortOnError = false checkReleaseBuilds = true - // Keep lint strict for project sources but ignore third-party CPM Java - // code checked out under ../extra_java_sources. lintConfig = file("lint.xml") } - // Do not compress Qt binary resources file aaptOptions { + // Do not compress Qt binary resources file noCompress 'rcc' } + packagingOptions { + jniLibs { + useLegacyPackaging = legacyPackaging + } + } + defaultConfig { - minSdk qgcConfigOrDefault('QGC_ANDROID_MIN_SDK_VERSION', qtMinSdkVersion).toInteger() - targetSdk qgcConfigOrDefault('QGC_ANDROID_TARGET_SDK_VERSION', qtTargetSdkVersion).toInteger() + resConfig "en" + minSdkVersion qtMinSdkVersion + targetSdkVersion qtTargetSdkVersion ndk.abiFilters = qtTargetAbiList.split(",") - if (qgcAndroidConfig.getProperty('QGC_ANDROID_VERSION_CODE')) { - versionCode Integer.parseInt(qgcAndroidConfig.getProperty('QGC_ANDROID_VERSION_CODE')) - } - if (qgcAndroidConfig.getProperty('QGC_ANDROID_VERSION_NAME')) { - versionName qgcAndroidConfig.getProperty('QGC_ANDROID_VERSION_NAME') - } - println "timestamp: " + getTimestampStr() - // println "applicationId: ${applicationId}" - println "minSdk: ${minSdk}" - println "targetSdk: ${targetSdk}" + println "applicationId: ${applicationId}" + println "minSdk: ${minSdkVersion}" + println "targetSdk: ${targetSdkVersion}" println "ndk.abiFilters: ${ndk.abiFilters}" - println "versionCode: ${versionCode}" - println "versionName: ${versionName}" } - // println "androidPackageName: ${namespace}" - println "compileSdk: ${compileSdk}" - println "androidBuildToolsVersion: ${buildToolsVersion}" - println "androidNdkVersion: ${ndkVersion}" - println "project.name: ${project.name}" + println "compileSdkVersion: ${compileSdkVersion}" + println "buildToolsVersion: ${buildToolsVersion}" + println "ndkVersion: ${ndkVersion}" + println "project.name: ${rootProject.name}" println "buildDir: ${buildDir}" println "qtAndroidDir: ${qtAndroidDir}" + println "qtGradlePluginType: ${qtGradlePluginType}" + println "legacyPackaging: ${legacyPackaging}" + println "gradleVersion: ${gradle.gradleVersion}" println "Java Version: " + JavaVersion.current() buildTypes { diff --git a/android/gradle.properties b/android/gradle.properties index eff1dc01c5bb..cbbd9d98411b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,7 +4,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx3200m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs=-Xmx3300m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # Enable building projects in parallel org.gradle.parallel=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar index 61285a659d17..b1b8ef56b44f 100644 Binary files a/android/gradle/wrapper/gradle-wrapper.jar and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 5c5334eb2763..a9db11550c62 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip -networkTimeout=120000 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew index 61b77d7f371f..203529cbc6f4 100755 --- a/android/gradlew +++ b/android/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. diff --git a/android/gradlew.bat b/android/gradlew.bat index bd8a8c0556a9..7e60b7292031 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,29 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/cmake/CustomOptions.cmake b/cmake/CustomOptions.cmake index a3c1bcfb1571..90b03974db36 100644 --- a/cmake/CustomOptions.cmake +++ b/cmake/CustomOptions.cmake @@ -237,8 +237,8 @@ unset(_qmlls_ini_default) option(QT_QMLLINT_CONTEXT_PROPERTY_DUMP "Emit qmllint context property data (Qt 6.11+; no-op on older)" ON) option(QT_QML_GENERATE_QMLLINT "Run qmllint at build time" OFF) -set(QGC_QT_DISABLE_DEPRECATED_UP_TO "0x060A00" CACHE STRING "Disable Qt APIs deprecated before this version") -set(QGC_QT_ENABLE_STRICT_MODE_UP_TO "0x060A00" CACHE STRING "Enable strict Qt API mode up to this version") +set(QGC_QT_DISABLE_DEPRECATED_UP_TO "0x060B00" CACHE STRING "Disable Qt APIs deprecated before this version") +set(QGC_QT_ENABLE_STRICT_MODE_UP_TO "0x060B00" CACHE STRING "Enable strict Qt API mode up to this version") # Debug environment variables (uncomment to enable) # set(ENV{QT_DEBUG_PLUGINS} "1") diff --git a/cmake/PrintSummary.cmake b/cmake/PrintSummary.cmake index 07e5c80f9195..1f19569402ff 100644 --- a/cmake/PrintSummary.cmake +++ b/cmake/PrintSummary.cmake @@ -178,6 +178,8 @@ if(ANDROID) message(STATUS "Android Platform:") message(STATUS " Target SDK: ${QGC_QT_ANDROID_TARGET_SDK_VERSION}") message(STATUS " Min SDK: ${QGC_QT_ANDROID_MIN_SDK_VERSION}") + message(STATUS " NDK host: ${ANDROID_NDK_HOST_SYSTEM_NAME}") + message(STATUS " SDK root: ${ANDROID_SDK_ROOT}") message(STATUS " Package: ${QGC_ANDROID_PACKAGE_NAME}") message(STATUS " APK signing: ${QT_ANDROID_SIGN_APK}") message(STATUS " AAB signing: ${QT_ANDROID_SIGN_AAB}") diff --git a/cmake/QGCTest.cmake b/cmake/QGCTest.cmake index 8d6dade7a234..f7eed73d5809 100644 --- a/cmake/QGCTest.cmake +++ b/cmake/QGCTest.cmake @@ -168,9 +168,10 @@ function(add_qgc_test test_name) set(_test_env "QT_LOGGING_RULES=*.debug=false") if(NOT QGC_TEST_ONSCREEN) - # Headless CI has no GL context; force Qt Quick's software backend (matches - # QmlTesting) so UI tests render without RHI/OpenGL and don't fail at teardown. - list(PREPEND _test_env "QT_QPA_PLATFORM=offscreen" "QT_QUICK_BACKEND=software") + # Offscreen plugin + software Quick backend; LIBGL_ALWAYS_SOFTWARE forces the + # GLX path (under xvfb, see cmake_helper.py) onto Mesa llvmpipe, no GPU needed. + list(PREPEND _test_env "QT_QPA_PLATFORM=offscreen" "QT_QUICK_BACKEND=software" + "LIBGL_ALWAYS_SOFTWARE=1") endif() # LSan's tracer process needs ptrace, which Yama (ptrace_scope>=1) blocks on diff --git a/cmake/install/Install.cmake b/cmake/install/Install.cmake index 498fb7cfd555..8a75b25ff3bb 100644 --- a/cmake/install/Install.cmake +++ b/cmake/install/Install.cmake @@ -30,12 +30,17 @@ if(MACOS OR WIN32) if(MACOS) list(APPEND deploy_tool_options_arg "-appstore-compliant") endif() + if(WIN32) + # windeployqt and qt6_deploy_qml_imports both deploy QML plugins into qml/ + # and relock qtquick2plugin.dll; -no-quick-import lets CMake own QML deploy. + list(APPEND deploy_tool_options_arg "-no-quick-import") + endif() endif() if(NOT ANDROID AND NOT IOS) set(deploy_include_plugins INCLUDE_PLUGINS qoffscreen) if(LINUX) - # Qt 6.10+ renamed wayland platform plugin to libqwayland.so + # Wayland platform plugin (libqwayland.so) list(APPEND deploy_include_plugins qwayland) endif() endif() diff --git a/cmake/platform/Android.cmake b/cmake/platform/Android.cmake index 5d302c2d3279..7bd59b51ed7f 100644 --- a/cmake/platform/Android.cmake +++ b/cmake/platform/Android.cmake @@ -79,21 +79,13 @@ endif() set(ANDROID_VERSION_CODE "${ANDROID_BITNESS_CODE}${CMAKE_PROJECT_VERSION_MAJOR}${CMAKE_PROJECT_VERSION_MINOR}${ANDROID_PATCH_VERSION}${ANDROID_DEV_VERSION}") message(STATUS "QGC: Android version code: ${ANDROID_VERSION_CODE}") -set(QGC_ANDROID_PROPERTIES_FILE "${CMAKE_BINARY_DIR}/qgc-android-config.properties") -set(QGC_ANDROID_PROPERTIES_CONTENT - "QGC_ANDROID_COMPILE_SDK_VERSION=${QGC_QT_ANDROID_COMPILE_SDK_VERSION}\n" - "QGC_ANDROID_TARGET_SDK_VERSION=${QGC_QT_ANDROID_TARGET_SDK_VERSION}\n" - "QGC_ANDROID_MIN_SDK_VERSION=${QGC_QT_ANDROID_MIN_SDK_VERSION}\n" - "QGC_ANDROID_VERSION_CODE=${ANDROID_VERSION_CODE}\n" - "QGC_ANDROID_VERSION_NAME=${CMAKE_PROJECT_VERSION}\n" - "QGC_CPM_JAVA_SRC_DIR=${CMAKE_BINARY_DIR}/extra_java_sources\n" -) -string(JOIN "" QGC_ANDROID_PROPERTIES_CONTENT ${QGC_ANDROID_PROPERTIES_CONTENT}) -file(GENERATE - OUTPUT "${QGC_ANDROID_PROPERTIES_FILE}" - CONTENT "${QGC_ANDROID_PROPERTIES_CONTENT}" -) -message(STATUS "QGC: Android shared properties: ${QGC_ANDROID_PROPERTIES_FILE}") +# ---------------------------------------------------------------------------- +# Extra Java Sources (CPM-deployed dependencies) +# ---------------------------------------------------------------------------- +# CPM Java dependencies are copied into this directory by src/Android/CMakeLists.txt +# and picked up by android/build.gradle as a supplementary source set. Gradle +# derives the same location as the sibling 'extra_java_sources' of its project dir. +set(QGC_ANDROID_EXTRA_JAVA_SOURCES_DIR "${CMAKE_BINARY_DIR}/extra_java_sources") set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES @@ -108,18 +100,20 @@ set_target_properties(${CMAKE_PROJECT_NAME} QT_ANDROID_VERSION_CODE ${ANDROID_VERSION_CODE} QT_ANDROID_APP_NAME "${CMAKE_PROJECT_NAME}" QT_ANDROID_APP_ICON "@mipmap/ic_launcher" - # QT_QML_IMPORT_PATH + QT_ANDROID_LEGACY_PACKAGING $ QT_QML_ROOT_PATH "${CMAKE_SOURCE_DIR}" + # QT_QML_IMPORT_PATH # QT_ANDROID_SYSTEM_LIBS_PREFIX ) +# set(QT_ANDROID_POST_BUILD_GRADLE_CLEANUP ON) + # if(CMAKE_BUILD_TYPE STREQUAL "Debug") # set(QT_ANDROID_APPLICATION_ARGUMENTS) # endif() -set(QGC_CPM_JAVA_SRC_DIR "${CMAKE_BINARY_DIR}/extra_java_sources") # Forward Python3_EXECUTABLE so per-ABI sub-configures use the same interpreter (jinja2 lives in workspace .venv, not hostedtoolcache python). -list(APPEND QT_ANDROID_MULTI_ABI_FORWARD_VARS QGC_STABLE_BUILD QT_HOST_PATH QGC_CPM_JAVA_SRC_DIR QGC_ANDROID_PROPERTIES_FILE Python3_EXECUTABLE) +list(APPEND QT_ANDROID_MULTI_ABI_FORWARD_VARS QGC_STABLE_BUILD QT_HOST_PATH Python3_EXECUTABLE) # ---------------------------------------------------------------------------- # Android OpenSSL Libraries diff --git a/cmake/presets/iOS.json b/cmake/presets/iOS.json index 3769f0b80aa8..22318c3abcdf 100644 --- a/cmake/presets/iOS.json +++ b/cmake/presets/iOS.json @@ -12,7 +12,7 @@ "cacheVariables": { "CMAKE_CONFIGURATION_TYPES": "Release;Debug", "CMAKE_OSX_ARCHITECTURES": "arm64", - "CMAKE_OSX_DEPLOYMENT_TARGET": "14.0", + "CMAKE_OSX_DEPLOYMENT_TARGET": "17.0", "QT_HOST_PATH": "$penv{QT_HOST_PATH}" } } diff --git a/custom-example/src/FlyViewToolStripActionList.qml b/custom-example/src/FlyViewToolStripActionList.qml index fad1c3c082e1..b346e31ac87f 100644 --- a/custom-example/src/FlyViewToolStripActionList.qml +++ b/custom-example/src/FlyViewToolStripActionList.qml @@ -2,6 +2,7 @@ import QtQml.Models import QGroundControl import QGroundControl.Controls +import QGroundControl.FlyView ToolStripActionList { id: _root diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile index 8c8551201b2d..fdb01c1789d9 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/Dockerfile @@ -126,8 +126,8 @@ ENV ANDROID_HOME=$ANDROID_SDK_ROOT RUN . /etc/profile.d/qgc.sh && \ mkdir -p $ANDROID_SDK_ROOT/cmdline-tools/latest && \ wget "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDLINE_TOOLS}_latest.zip" -O /opt/cmdline-tools.zip && \ - # Pinned to android_cmdline_tools 13114758 (.github/build-config.json); update both together. - echo "7ec965280a073311c339e571cd5de778b9975026cfcbe79f2b1cdcb1e15317ee /opt/cmdline-tools.zip" | sha256sum -c - && \ + # Pinned to android_cmdline_tools 14742923 (.github/build-config.json); update both together. + echo "04453066b540409d975c676d781da1477479dde3761310f1a7eb92a1dfb15af7 /opt/cmdline-tools.zip" | sha256sum -c - && \ unzip /opt/cmdline-tools.zip -d $ANDROID_SDK_ROOT/cmdline-tools && \ mv $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools/* $ANDROID_SDK_ROOT/cmdline-tools/latest/ && \ rm -rf $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools && \ diff --git a/src/Android/CMakeLists.txt b/src/Android/CMakeLists.txt index af4581bf6d18..cc4b559a76ad 100644 --- a/src/Android/CMakeLists.txt +++ b/src/Android/CMakeLists.txt @@ -85,14 +85,14 @@ target_compile_definitions(qtandroidextensions_lockers PRIVATE QTANDROIDEXTENSIO target_link_libraries(qtandroidextensions_lockers PUBLIC Qt6::Core PRIVATE Qt6::CorePrivate) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE qtandroidextensions_lockers) -# Deploy Java sources to build tree (picked up by build.gradle via cpmJavaSrcDir) +# Deploy Java sources to QGC_ANDROID_EXTRA_JAVA_SOURCES_DIR (added to build.gradle's source set) file(GLOB _qjnihelpers_java "${_qjni_dir}/ru.dublgis.qjnihelpers/*.java") file(GLOB _qtandroidhelpers_java "${_qtah_dir}/ru.dublgis.androidhelpers/*.java") file(COPY ${_qjnihelpers_java} - DESTINATION "${CMAKE_BINARY_DIR}/extra_java_sources/ru/dublgis/qjnihelpers" + DESTINATION "${QGC_ANDROID_EXTRA_JAVA_SOURCES_DIR}/ru/dublgis/qjnihelpers" ) file(COPY ${_qtandroidhelpers_java} - DESTINATION "${CMAKE_BINARY_DIR}/extra_java_sources/ru/dublgis/androidhelpers" + DESTINATION "${QGC_ANDROID_EXTRA_JAVA_SOURCES_DIR}/ru/dublgis/androidhelpers" ) # ---------------------------------------------------------------------------- @@ -117,7 +117,7 @@ target_sources(${CMAKE_PROJECT_NAME} ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${_qtat_dir}) -# Deploy Java sources to build tree (picked up by build.gradle via cpmJavaSrcDir) +# Deploy Java sources to QGC_ANDROID_EXTRA_JAVA_SOURCES_DIR (added to build.gradle's source set) file(COPY "${_qtat_dir}/src/com/falsinsoft/qtandroidtools/AndroidScreen.java" - DESTINATION "${CMAKE_BINARY_DIR}/extra_java_sources/com/falsinsoft/qtandroidtools" + DESTINATION "${QGC_ANDROID_EXTRA_JAVA_SOURCES_DIR}/com/falsinsoft/qtandroidtools" ) diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index 7e70445dd051..7f2d33d85782 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -654,6 +654,12 @@ bool QGCApplication::compressEvent(QEvent* event, QObject* receiver, QPostEventL return QGuiApplication::compressEvent(event, receiver, postedEvents); } + // QMetaCallEvent::id() was removed in 6.11; its protected Data is reachable from a derived helper. + struct MetaCallHelper : public QMetaCallEvent { + int id() const { return d.method_offset_ + d.method_relative_; } + }; + const auto methodId = [](const QMetaCallEvent *e) { return static_cast(e)->id(); }; + for (QPostEventList::iterator it = postedEvents->begin(); it != postedEvents->end(); ++it) { QPostEvent& cur = *it; if (cur.receiver != receiver || cur.event == 0 || cur.event->type() != event->type()) { @@ -661,7 +667,7 @@ bool QGCApplication::compressEvent(QEvent* event, QObject* receiver, QPostEventL } const QMetaCallEvent* cur_mce = static_cast(cur.event); if (cur_mce->sender() != mce->sender() || cur_mce->signalId() != mce->signalId() || - cur_mce->id() != mce->id()) { + methodId(cur_mce) != methodId(mce)) { continue; } diff --git a/src/QtLocationPlugin/QGeoTileFetcherQGC.cpp b/src/QtLocationPlugin/QGeoTileFetcherQGC.cpp index 69a420522799..63bce950cacc 100644 --- a/src/QtLocationPlugin/QGeoTileFetcherQGC.cpp +++ b/src/QtLocationPlugin/QGeoTileFetcherQGC.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "MapProvider.h" #include "QGCLoggingCategory.h" @@ -12,9 +13,17 @@ QGC_LOGGING_CATEGORY(QGeoTileFetcherQGCLog, "QtLocationPlugin.QGeoTileFetcherQGC") -QGeoTileFetcherQGC::QGeoTileFetcherQGC(QNetworkAccessManager *networkManager, const QVariantMap ¶meters, QGeoTiledMappingManagerEngineQGC *parent) - : QGeoTileFetcher(parent) - , m_networkManager(networkManager) +namespace { +// Keep pooled sockets warm across sparse tile/terrain fetches; Qt 6.11 otherwise reaps idle ones after 2 min. +constexpr int kConnectionCacheExpirySecs = 300; +constexpr std::chrono::seconds kTcpKeepAliveIdle{60}; +constexpr std::chrono::seconds kTcpKeepAliveInterval{30}; +constexpr int kTcpKeepAliveProbeCount = 3; +} // namespace + +QGeoTileFetcherQGC::QGeoTileFetcherQGC(QNetworkAccessManager* networkManager, const QVariantMap& parameters, + QGeoTiledMappingManagerEngineQGC* parent) + : QGeoTileFetcher(parent), m_networkManager(networkManager) { Q_ASSERT(networkManager); @@ -31,7 +40,7 @@ QGeoTileFetcherQGC::~QGeoTileFetcherQGC() qCDebug(QGeoTileFetcherQGCLog) << this; } -QGeoTiledMapReply* QGeoTileFetcherQGC::getTileImage(const QGeoTileSpec &spec) +QGeoTiledMapReply* QGeoTileFetcherQGC::getTileImage(const QGeoTileSpec& spec) { const SharedMapProvider provider = UrlFactory::getMapProviderFromQtMapId(spec.mapId()); if (!provider) { @@ -47,7 +56,7 @@ QGeoTiledMapReply* QGeoTileFetcherQGC::getTileImage(const QGeoTileSpec &spec) return nullptr; } - QGeoTiledMapReplyQGC *tileImage = new QGeoTiledMapReplyQGC(m_networkManager, request, spec); + QGeoTiledMapReplyQGC* tileImage = new QGeoTiledMapReplyQGC(m_networkManager, request, spec); if (!tileImage->init()) { tileImage->deleteLater(); return nullptr; @@ -66,12 +75,12 @@ bool QGeoTileFetcherQGC::fetchingEnabled() const return initialized(); } -void QGeoTileFetcherQGC::timerEvent(QTimerEvent *event) +void QGeoTileFetcherQGC::timerEvent(QTimerEvent* event) { QGeoTileFetcher::timerEvent(event); } -void QGeoTileFetcherQGC::handleReply(QGeoTiledMapReply *reply, const QGeoTileSpec &spec) +void QGeoTileFetcherQGC::handleReply(QGeoTiledMapReply* reply, const QGeoTileSpec& spec) { if (!reply) { return; @@ -115,6 +124,10 @@ QNetworkRequest QGeoTileFetcherQGC::getNetworkRequest(int mapId, int x, int y, i request.setRawHeader(QByteArrayLiteral("User-Token"), token); } request.setRawHeader(QByteArrayLiteral("Connection"), QByteArrayLiteral("keep-alive")); + request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute, kConnectionCacheExpirySecs); + request.setTcpKeepAliveIdleTimeBeforeProbes(kTcpKeepAliveIdle); + request.setTcpKeepAliveIntervalBetweenProbes(kTcpKeepAliveInterval); + request.setTcpKeepAliveProbeCount(kTcpKeepAliveProbeCount); // request.setRawHeader(QByteArrayLiteral("Accept-Encoding"), QByteArrayLiteral("gzip, deflate, br")); // Attributes diff --git a/src/Utilities/Network/QGCFileDownload.cc b/src/Utilities/Network/QGCFileDownload.cc index 09586ba075bc..0232ec9f83f2 100644 --- a/src/Utilities/Network/QGCFileDownload.cc +++ b/src/Utilities/Network/QGCFileDownload.cc @@ -482,7 +482,7 @@ QString QGCFileDownload::_generateOutputPath(const QString &remoteUrl) const } // Extract filename from URL - QString fileName = QGCNetworkHelper::urlFileName(QUrl(remoteUrl)); + QString fileName = QUrl(remoteUrl).fileName(); if (fileName.isEmpty()) { fileName = QStringLiteral("DownloadedFile"); } diff --git a/src/Utilities/Network/QGCNetworkHelper.cc b/src/Utilities/Network/QGCNetworkHelper.cc index 3354f2263d55..5545ee2cc389 100644 --- a/src/Utilities/Network/QGCNetworkHelper.cc +++ b/src/Utilities/Network/QGCNetworkHelper.cc @@ -1,22 +1,23 @@ #include "QGCNetworkHelper.h" +#include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include "QGCCompression.h" #include "QGCLoggingCategory.h" -#include - QGC_LOGGING_CATEGORY(QGCNetworkHelperLog, "Utilities.QGCNetworkHelper") namespace QGCNetworkHelper { @@ -27,19 +28,20 @@ namespace QGCNetworkHelper { HttpStatusClass classifyHttpStatus(int statusCode) { - if (statusCode >= 100 && statusCode < 200) { + const auto code = static_cast(statusCode); + if (code >= HttpStatusCode::Continue && code < HttpStatusCode::Ok) { return HttpStatusClass::Informational; } - if (statusCode >= 200 && statusCode < 300) { + if (code >= HttpStatusCode::Ok && code < HttpStatusCode::MultipleChoices) { return HttpStatusClass::Success; } - if (statusCode >= 300 && statusCode < 400) { + if (code >= HttpStatusCode::MultipleChoices && code < HttpStatusCode::BadRequest) { return HttpStatusClass::Redirection; } - if (statusCode >= 400 && statusCode < 500) { + if (code >= HttpStatusCode::BadRequest && code < HttpStatusCode::InternalServerError) { return HttpStatusClass::ClientError; } - if (statusCode >= 500 && statusCode < 600) { + if (code >= HttpStatusCode::InternalServerError && code <= HttpStatusCode::NetworkConnectTimeoutError) { return HttpStatusClass::ServerError; } return HttpStatusClass::Unknown; @@ -176,16 +178,12 @@ QString httpStatusText(HttpStatusCode statusCode) case HttpStatusCode::NetworkConnectTimeoutError: return QStringLiteral("Network Connect Timeout Error"); default: - return QStringLiteral("Unknown Status"); + return QStringLiteral("Unknown Status (%1)").arg(static_cast(statusCode)); } } QString httpStatusText(int statusCode) { - if (classifyHttpStatus(statusCode) == HttpStatusClass::Unknown) { - return QStringLiteral("Unknown Status (%1)").arg(statusCode); - } - return httpStatusText(static_cast(statusCode)); } @@ -278,13 +276,10 @@ QUrl normalizeUrl(const QUrl& url) return url; } - QUrl normalized = url; - - // Lowercase scheme and host + QUrl normalized = url.adjusted(QUrl::NormalizePathSegments | QUrl::StripTrailingSlash); normalized.setScheme(normalized.scheme().toLower()); normalized.setHost(normalized.host().toLower()); - // Remove default ports const int port = normalized.port(); const QString scheme = normalized.scheme(); if ((scheme == QLatin1String("http") && port == 80) || (scheme == QLatin1String("https") && port == 443) || @@ -292,13 +287,6 @@ QUrl normalizeUrl(const QUrl& url) normalized.setPort(-1); } - // Remove trailing slash from path (except for root) - QString path = normalized.path(); - if (path.length() > 1 && path.endsWith(QLatin1Char('/'))) { - path.chop(1); - normalized.setPath(path); - } - return normalized; } @@ -349,22 +337,9 @@ QUrl buildUrl(const QString& baseUrl, const QList>& para return url; } -QString urlFileName(const QUrl& url) -{ - const QString path = url.path(); - const int lastSlash = path.lastIndexOf(QLatin1Char('/')); - if (lastSlash >= 0 && lastSlash < path.length() - 1) { - return path.mid(lastSlash + 1); - } - return path; -} - QUrl urlWithoutQuery(const QUrl& url) { - QUrl result = url; - result.setQuery(QString()); - result.setFragment(QString()); - return result; + return url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); } // ============================================================================ @@ -398,27 +373,38 @@ void configureRequest(QNetworkRequest& request, const RequestConfig& config) // Background request request.setAttribute(QNetworkRequest::BackgroundRequestAttribute, config.backgroundRequest); - // Headers - const QString userAgent = config.userAgent.isEmpty() ? defaultUserAgent() : config.userAgent; - request.setHeader(QNetworkRequest::UserAgentHeader, userAgent); - + using WK = QHttpHeaders::WellKnownHeader; + QHttpHeaders headers = request.headers(); + headers.replaceOrAppend(WK::UserAgent, config.userAgent.isEmpty() ? defaultUserAgent() : config.userAgent); if (!config.accept.isEmpty()) { - request.setRawHeader("Accept", config.accept.toUtf8()); + headers.replaceOrAppend(WK::Accept, config.accept); } - if (!config.acceptEncoding.isEmpty()) { - request.setRawHeader("Accept-Encoding", config.acceptEncoding.toUtf8()); + headers.replaceOrAppend(WK::AcceptEncoding, config.acceptEncoding); } - if (!config.contentType.isEmpty()) { - request.setHeader(QNetworkRequest::ContentTypeHeader, config.contentType); + headers.replaceOrAppend(WK::ContentType, config.contentType); } + headers.replaceOrAppend(WK::Connection, "keep-alive"); + request.setHeaders(headers); - for (const auto &[attribute, value] : config.requestAttributes) { + for (const auto& [attribute, value] : config.requestAttributes) { request.setAttribute(attribute, value); } - request.setRawHeader("Connection", "keep-alive"); + if (config.connectionCacheExpirySecs >= 0) { + request.setAttribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute, + config.connectionCacheExpirySecs); + } + if (config.tcpKeepAliveIdleSecs >= 0) { + request.setTcpKeepAliveIdleTimeBeforeProbes(std::chrono::seconds(config.tcpKeepAliveIdleSecs)); + } + if (config.tcpKeepAliveIntervalSecs >= 0) { + request.setTcpKeepAliveIntervalBetweenProbes(std::chrono::seconds(config.tcpKeepAliveIntervalSecs)); + } + if (config.tcpKeepAliveProbeCount >= 0) { + request.setTcpKeepAliveProbeCount(config.tcpKeepAliveProbeCount); + } } QNetworkRequest createRequest(const QUrl& url, const RequestConfig& config) @@ -430,17 +416,22 @@ QNetworkRequest createRequest(const QUrl& url, const RequestConfig& config) void setStandardHeaders(QNetworkRequest& request, const QString& userAgent) { - const QString ua = userAgent.isEmpty() ? defaultUserAgent() : userAgent; - request.setHeader(QNetworkRequest::UserAgentHeader, ua); - request.setRawHeader("Accept", "*/*"); - request.setRawHeader("Accept-Encoding", "gzip, deflate"); - request.setRawHeader("Connection", "keep-alive"); + using WK = QHttpHeaders::WellKnownHeader; + QHttpHeaders headers = request.headers(); + headers.replaceOrAppend(WK::UserAgent, userAgent.isEmpty() ? defaultUserAgent() : userAgent); + headers.replaceOrAppend(WK::Accept, "*/*"); + headers.replaceOrAppend(WK::AcceptEncoding, "gzip, deflate"); + headers.replaceOrAppend(WK::Connection, "keep-alive"); + request.setHeaders(headers); } void setJsonHeaders(QNetworkRequest& request) { - request.setRawHeader("Accept", "application/json"); - request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + using WK = QHttpHeaders::WellKnownHeader; + QHttpHeaders headers = request.headers(); + headers.replaceOrAppend(WK::Accept, "application/json"); + headers.replaceOrAppend(WK::ContentType, "application/json"); + request.setHeaders(headers); } void setFormHeaders(QNetworkRequest& request) @@ -466,7 +457,9 @@ QString defaultUserAgent() void setBasicAuth(QNetworkRequest& request, const QString& credentials) { - request.setRawHeader("Authorization", ("Basic " + credentials).toUtf8()); + QHttpHeaders headers = request.headers(); + headers.replaceOrAppend(QHttpHeaders::WellKnownHeader::Authorization, "Basic " + credentials); + request.setHeaders(headers); } void setBasicAuth(QNetworkRequest& request, const QString& username, const QString& password) @@ -476,7 +469,9 @@ void setBasicAuth(QNetworkRequest& request, const QString& username, const QStri void setBearerToken(QNetworkRequest& request, const QString& token) { - request.setRawHeader("Authorization", ("Bearer " + token).toUtf8()); + QHttpHeaders headers = request.headers(); + headers.replaceOrAppend(QHttpHeaders::WellKnownHeader::Authorization, "Bearer " + token); + request.setHeaders(headers); } QString createBasicAuthCredentials(const QString& username, const QString& password) @@ -544,19 +539,20 @@ QList loadCaCertificates(const QString& filePath, QString* erro return certs; } -bool loadClientCertAndKey(const QString& certPath, const QString& keyPath, - QSslCertificate& certOut, QSslKey& keyOut, +bool loadClientCertAndKey(const QString& certPath, const QString& keyPath, QSslCertificate& certOut, QSslKey& keyOut, QString* errorOut) { const auto certs = QSslCertificate::fromPath(certPath, QSsl::Pem); if (certs.isEmpty()) { - if (errorOut) *errorOut = QStringLiteral("No certificate found in %1").arg(certPath); + if (errorOut) + *errorOut = QStringLiteral("No certificate found in %1").arg(certPath); return false; } QFile keyFile(keyPath); if (!keyFile.open(QIODevice::ReadOnly)) { - if (errorOut) *errorOut = QStringLiteral("Cannot open key file %1").arg(keyPath); + if (errorOut) + *errorOut = QStringLiteral("Cannot open key file %1").arg(keyPath); return false; } @@ -566,7 +562,8 @@ bool loadClientCertAndKey(const QString& certPath, const QString& keyPath, key = QSslKey(&keyFile, QSsl::Ec); } if (key.isNull()) { - if (errorOut) *errorOut = QStringLiteral("Invalid key in %1").arg(keyPath); + if (errorOut) + *errorOut = QStringLiteral("Invalid key in %1").arg(keyPath); return false; } diff --git a/src/Utilities/Network/QGCNetworkHelper.h b/src/Utilities/Network/QGCNetworkHelper.h index da6d8ce7dea4..cb92f2d03a3d 100644 --- a/src/Utilities/Network/QGCNetworkHelper.h +++ b/src/Utilities/Network/QGCNetworkHelper.h @@ -136,9 +136,6 @@ QUrl buildUrl(const QString& baseUrl, const QMap& params); /// Build URL with query parameters from a list of pairs QUrl buildUrl(const QString& baseUrl, const QList>& params); -/// Extract filename from URL path (last path segment) -QString urlFileName(const QUrl& url); - /// Get URL without query string and fragment QUrl urlWithoutQuery(const QUrl& url); @@ -159,6 +156,11 @@ struct RequestConfig QString acceptEncoding; QString contentType; QList> requestAttributes; + + int connectionCacheExpirySecs = -1; + int tcpKeepAliveIdleSecs = -1; + int tcpKeepAliveIntervalSecs = -1; + int tcpKeepAliveProbeCount = -1; }; /// Configure a QNetworkRequest with standard settings @@ -266,8 +268,7 @@ QList loadCaCertificates(const QString& filePath, QString* erro /// @param keyOut Receives the loaded key /// @param errorOut Optional pointer to receive error message /// @return true on success -bool loadClientCertAndKey(const QString& certPath, const QString& keyPath, - QSslCertificate& certOut, QSslKey& keyOut, +bool loadClientCertAndKey(const QString& certPath, const QString& keyPath, QSslCertificate& certOut, QSslKey& keyOut, QString* errorOut = nullptr); // ============================================================================ diff --git a/src/VideoManager/VideoReceiver/GStreamer/GstAppSinkAdapter.cc b/src/VideoManager/VideoReceiver/GStreamer/GstAppSinkAdapter.cc index a67972ff227c..d9af438cb087 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/GstAppSinkAdapter.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/GstAppSinkAdapter.cc @@ -63,18 +63,18 @@ QVideoFrameFormat::ColorSpace toQtColorSpace(GstVideoColorMatrix matrix) QVideoFrameFormat::ColorTransfer toQtColorTransfer(GstVideoTransferFunction transfer) { // Mapping mirrors qt6/qtmultimedia/.../qgst.cpp QGstCaps::formatAndVideoInfo() — keep - // in sync if Qt changes its mapping (last cross-check: Qt 6.10.3). + // in sync if Qt changes its mapping (last cross-check: Qt 6.11.1). switch (transfer) { case GST_VIDEO_TRANSFER_BT601: return QVideoFrameFormat::ColorTransfer_BT601; case GST_VIDEO_TRANSFER_BT2020_10: case GST_VIDEO_TRANSFER_BT2020_12: - case GST_VIDEO_TRANSFER_BT709: return QVideoFrameFormat::ColorTransfer_BT709; - case GST_VIDEO_TRANSFER_GAMMA20: return QVideoFrameFormat::ColorTransfer_BT709; // best fit per Qt - case GST_VIDEO_TRANSFER_SMPTE240M: return QVideoFrameFormat::ColorTransfer_BT709; // near-identical to BT.709 per Qt qgst.cpp:424 + case GST_VIDEO_TRANSFER_BT709: + case GST_VIDEO_TRANSFER_GAMMA18: // Qt: GAMMA18/GAMMA20 fall through to BT709 ("not quite, but best fit") + case GST_VIDEO_TRANSFER_GAMMA20: return QVideoFrameFormat::ColorTransfer_BT709; case GST_VIDEO_TRANSFER_GAMMA22: + case GST_VIDEO_TRANSFER_SMPTE240M: // Qt: grouped with GAMMA22/SRGB/ADOBERGB case GST_VIDEO_TRANSFER_SRGB: case GST_VIDEO_TRANSFER_ADOBERGB: return QVideoFrameFormat::ColorTransfer_Gamma22; - case GST_VIDEO_TRANSFER_GAMMA18: return QVideoFrameFormat::ColorTransfer_Gamma22; // closest Qt equivalent case GST_VIDEO_TRANSFER_GAMMA28: return QVideoFrameFormat::ColorTransfer_Gamma28; case GST_VIDEO_TRANSFER_GAMMA10: return QVideoFrameFormat::ColorTransfer_Linear; case GST_VIDEO_TRANSFER_SMPTE2084: return QVideoFrameFormat::ColorTransfer_ST2084; diff --git a/src/VideoManager/VideoReceiver/GStreamer/HwBuffers/GstHwVideoBuffer.cc b/src/VideoManager/VideoReceiver/GStreamer/HwBuffers/GstHwVideoBuffer.cc index 1a985ae72184..7ae703301e2e 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/HwBuffers/GstHwVideoBuffer.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/HwBuffers/GstHwVideoBuffer.cc @@ -4,7 +4,7 @@ GstHwVideoBuffer::GstHwVideoBuffer(QVideoFrame::HandleType handleType, GstSample *sample, const GstVideoInfo &videoInfo, QVideoFrameFormat format) - : QHwVideoBuffer(handleType, nullptr) + : QHwVideoBuffer(handleType) , _sample(sample ? gst_sample_ref(sample) : nullptr) , _videoInfo(videoInfo) , _format(std::move(format)) diff --git a/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc b/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc index 8c5c227b5dc3..20b82f65483e 100644 --- a/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc +++ b/src/VideoManager/VideoReceiver/GStreamer/gstqgc/gstqgcvideosinkbin.cc @@ -305,7 +305,7 @@ gst_qgc_video_sink_bin_setup(GstQgcVideoSinkBin *self) if (self->gpu_zerocopy) { // List only features the build can consume zero-copy; stale features waste a caps-intersection pass on every link. - // Y444 omitted: Qt 6.10 has no Format_YUV444* and toQtPixelFormat returns Invalid + // Y444 omitted: Qt 6.11 has no Format_YUV444* and toQtPixelFormat returns Invalid // → onNewSample errors out. Re-add when Qt grows the enum. static constexpr const char kFormats[] = "{ NV12, NV21, I420, YV12, Y42B, P010_10LE, AYUV, YUY2, UYVY, " diff --git a/test/UnitTestFramework/UnitTest.cc b/test/UnitTestFramework/UnitTest.cc index 0fdc9a6a3ba9..4158fc5a48d6 100644 --- a/test/UnitTestFramework/UnitTest.cc +++ b/test/UnitTestFramework/UnitTest.cc @@ -800,6 +800,27 @@ void UnitTest::init() ignoreLogMessage("Vehicle.RemoteIDManager", QtWarningMsg, QRegularExpression(QStringLiteral("^GCS GPS error:"))); + // offscreen QPA exposes no GLX/EGL display, so Qt 6.11's QRhiGles2 probe can't + // create a context and warns; UI tests use the software backend, so it's benign. + ignoreLogMessage("default", QtWarningMsg, + QRegularExpression(QStringLiteral("^QRhiGles2: Failed to create"))); + + // QQuickPinchArea declares its own `enabled` property (gesture enable, distinct + // from QQuickItem::enabled); Qt 6.11's property-cache shadow check warns once per + // QML engine. A framework quirk, not QGC's — every UI test scene hits it. + ignoreLogMessage("qt.qml.propertyCache.append", QtWarningMsg, + QRegularExpression(QStringLiteral("QQuickPinchArea overrides a member"))); + + // QtGraphs warns when a LineSeries is given the GraphsView's own axes (redundant + // association); cosmetic, the chart still renders. Surfaces in the Analyze charts. + ignoreLogMessage("qt.graphs2d.axis.properties", QtWarningMsg, + QRegularExpression(QStringLiteral("axis already associated with"))); + + // Headless software-GL (xvfb) gives GStreamer no usable X11/EGL GL context, so the + // GL bridge disables itself and falls back to software decode — by design in tests. + ignoreLogMessage("Video.GStreamer.HwBuffers.GstGlBridge", QtWarningMsg, + QRegularExpression(QStringLiteral("GL bridge disabled"))); + // Start capturing log messages for this test (cleared from previous test) LogManager::clearCapturedMessages(); LogManager::setCaptureEnabled(true); diff --git a/test/Utilities/Network/QGCNetworkHelperTest.cc b/test/Utilities/Network/QGCNetworkHelperTest.cc index 957cf799ccab..589b8fb8cc24 100644 --- a/test/Utilities/Network/QGCNetworkHelperTest.cc +++ b/test/Utilities/Network/QGCNetworkHelperTest.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include "Fixtures/RAIIFixtures.h" #include "QGCNetworkHelper.h" @@ -126,6 +127,9 @@ void QGCNetworkHelperTest::_testHttpStatusText() QCOMPARE(QGCNetworkHelper::httpStatusText(503), QStringLiteral("Service Unavailable")); // Unknown status should return formatted message QVERIFY(QGCNetworkHelper::httpStatusText(999).contains("999")); + // In-range but unnamed codes must still surface the numeric value + QVERIFY(QGCNetworkHelper::httpStatusText(218).contains("218")); + QVERIFY(QGCNetworkHelper::httpStatusText(450).contains("450")); } void QGCNetworkHelperTest::_testHttpStatusTextFromEnum() @@ -327,14 +331,6 @@ void QGCNetworkHelperTest::_testBuildUrlFromList() QVERIFY(query.contains("key1=value2")); } -void QGCNetworkHelperTest::_testUrlFileName() -{ - QCOMPARE(QGCNetworkHelper::urlFileName(QUrl("http://example.com/path/to/file.txt")), QStringLiteral("file.txt")); - QCOMPARE(QGCNetworkHelper::urlFileName(QUrl("http://example.com/file.zip")), QStringLiteral("file.zip")); - QCOMPARE(QGCNetworkHelper::urlFileName(QUrl("http://example.com/")), QStringLiteral("/")); - QCOMPARE(QGCNetworkHelper::urlFileName(QUrl("http://example.com/path/")), QStringLiteral("/path/")); -} - void QGCNetworkHelperTest::_testUrlWithoutQuery() { QUrl original("http://example.com/path?key=value#fragment"); @@ -387,6 +383,26 @@ void QGCNetworkHelperTest::_testRequestConfigAttributes() QCOMPARE(attribute.toInt(), static_cast(QNetworkRequest::AlwaysNetwork)); } +void QGCNetworkHelperTest::_testRequestConfigKeepAlive() +{ + const QNetworkRequest defaultRequest = QGCNetworkHelper::createRequest(QUrl(QStringLiteral("https://example.com"))); + QVERIFY(!defaultRequest.attribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute).isValid()); + QCOMPARE(defaultRequest.tcpKeepAliveProbeCount(), 0); + + QGCNetworkHelper::RequestConfig config; + config.connectionCacheExpirySecs = 300; + config.tcpKeepAliveIdleSecs = 60; + config.tcpKeepAliveIntervalSecs = 30; + config.tcpKeepAliveProbeCount = 3; + + const QNetworkRequest request = + QGCNetworkHelper::createRequest(QUrl(QStringLiteral("https://example.com")), config); + QCOMPARE(request.attribute(QNetworkRequest::ConnectionCacheExpiryTimeoutSecondsAttribute).toInt(), 300); + QCOMPARE(request.tcpKeepAliveIdleTimeBeforeProbes().count(), std::chrono::seconds::rep(60)); + QCOMPARE(request.tcpKeepAliveIntervalBetweenProbes().count(), std::chrono::seconds::rep(30)); + QCOMPARE(request.tcpKeepAliveProbeCount(), 3); +} + void QGCNetworkHelperTest::_testSetJsonHeaders() { QNetworkRequest request(QUrl(QStringLiteral("https://example.com"))); diff --git a/test/Utilities/Network/QGCNetworkHelperTest.h b/test/Utilities/Network/QGCNetworkHelperTest.h index f47676c56390..4b14265c6178 100644 --- a/test/Utilities/Network/QGCNetworkHelperTest.h +++ b/test/Utilities/Network/QGCNetworkHelperTest.h @@ -37,13 +37,13 @@ private slots: void _testEnsureScheme(); void _testBuildUrlFromMap(); void _testBuildUrlFromList(); - void _testUrlFileName(); void _testUrlWithoutQuery(); // Request configuration tests void _testDefaultUserAgent(); void _testRequestConfigDefaults(); void _testRequestConfigAttributes(); + void _testRequestConfigKeepAlive(); void _testSetJsonHeaders(); void _testCreateBasicAuthCredentials(); void _testSetBasicAuthHeader(); diff --git a/test/VideoManager/GStreamer/GStreamerTest.cc b/test/VideoManager/GStreamer/GStreamerTest.cc index 3c7d920bbff1..01416be1c590 100644 --- a/test/VideoManager/GStreamer/GStreamerTest.cc +++ b/test/VideoManager/GStreamer/GStreamerTest.cc @@ -1418,7 +1418,7 @@ void GStreamerTest::_testColorimetryPixelFormatMapping() QCOMPARE(toQtPixelFormat(GST_VIDEO_FORMAT_RGBA), QVideoFrameFormat::Format_RGBA8888); QCOMPARE(toQtPixelFormat(GST_VIDEO_FORMAT_I420_10LE), QVideoFrameFormat::Format_YUV420P10); QCOMPARE(toQtPixelFormat(GST_VIDEO_FORMAT_P016_LE), QVideoFrameFormat::Format_P016); - // Y444 is intentionally NOT in caps — Qt 6.10 has no Format_YUV444*, so any negotiation + // Y444 is intentionally NOT in caps — Qt 6.11 has no Format_YUV444*, so any negotiation // would dead-end at GST_FLOW_ERROR. Re-enable when Qt grows the enum. QCOMPARE(toQtPixelFormat(GST_VIDEO_FORMAT_Y444), QVideoFrameFormat::Format_Invalid); } @@ -1440,8 +1440,9 @@ void GStreamerTest::_testColorimetryTransferMapping() QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_ARIB_STD_B67), QVideoFrameFormat::ColorTransfer_STD_B67); QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_GAMMA10), QVideoFrameFormat::ColorTransfer_Linear); QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_GAMMA28), QVideoFrameFormat::ColorTransfer_Gamma28); - QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_SMPTE240M), QVideoFrameFormat::ColorTransfer_BT709); + QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_SMPTE240M), QVideoFrameFormat::ColorTransfer_Gamma22); QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_ADOBERGB), QVideoFrameFormat::ColorTransfer_Gamma22); + QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_GAMMA18), QVideoFrameFormat::ColorTransfer_BT709); QCOMPARE(toQtColorTransfer(GST_VIDEO_TRANSFER_LOG100), QVideoFrameFormat::ColorTransfer_Unknown); } diff --git a/tools/README.md b/tools/README.md index cca86ed30dc3..0310f9af45a2 100644 --- a/tools/README.md +++ b/tools/README.md @@ -350,9 +350,9 @@ Version numbers and build settings are centralized in `.github/build-config.json ```json { - "qt_version": "6.10.1", + "qt_version": "6.11.1", "qt_modules": "qtgraphs qtlocation ...", - "gstreamer_default_version": "1.24.13", + "gstreamer_default_version": "1.28.1", "ndk_version": "r27c", ... } diff --git a/tools/setup/install_dependencies/_packages.py b/tools/setup/install_dependencies/_packages.py index b7c0cf9d2272..b66967038e1c 100644 --- a/tools/setup/install_dependencies/_packages.py +++ b/tools/setup/install_dependencies/_packages.py @@ -72,6 +72,12 @@ "libxrender-dev", "libunwind-dev", "libegl-dev", + # Headless GL: xvfb gives ctest a virtual display so the offscreen plugin's + # GLX path can create a Mesa llvmpipe context (see cmake_helper.py). + "xvfb", + "xauth", + "libgl1-mesa-dri", + "libglx-mesa0", ], "gstreamer": [ "libgstreamer1.0-dev",