diff --git a/.dockerignore b/.dockerignore index 371dcb14d3b9..3698d7ab4aac 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,81 @@ -# Ignore Git repository files +# ============================================================================== +# Docker Build Context Exclusions +# ============================================================================== + +# Git .git .gitignore .gitattributes +.gitmodules +.mailmap -# Ignore node_modules or other build artifacts -node_modules -build - -# Ignore Docker-related files -Dockerfile* -docker-compose.yml +# GitHub (except build-config.json needed by Dockerfile) +.github/ +!.github/build-config.json +# Documentation (not needed for build) +docs/ README.md +CHANGELOG.md +LICENSE-* + +# Build artifacts +build/ +cmake-build-*/ +out/ +cpm_modules/ +compile_commands.json CMakeLists.txt.user* +CMakeUserPresets.json + +# IDEs & Editors +.idea/ +.vscode/ +.qtcreator/ +.fleet/ +.vs/ +.settings/ +*.swp +*~ +tags + +# Node.js (docs build) +node_modules/ +package-lock.json + +# Python +*.pyc +__pycache__/ +.ruff_cache/ + +# Local development +.claude/ +.cache/ +sessions/ +CLAUDE.md +AGENTS.md +GEMINI.md + +# Platform artifacts +.DS_Store +*.exe +*.dll +*.so +*.dylib +*.app +*.dmg +*.deb +*.rpm +*.AppImage +*.apk +*.aab +*.ipa + +# Test artifacts +*.gcov +*.gcda +*.gcno +*.coverage + +# Vagrant +.vagrant/ diff --git a/.github/CITATION.cff b/.github/CITATION.cff new file mode 100644 index 000000000000..cd479d33ba00 --- /dev/null +++ b/.github/CITATION.cff @@ -0,0 +1,25 @@ +cff-version: 1.2.0 +title: QGroundControl +message: >- + If you use QGroundControl in your research, please cite it using + the metadata from this file. +type: software +authors: + - name: QGroundControl Development Team + website: https://qgroundcontrol.com +repository-code: https://github.com/mavlink/qgroundcontrol +url: https://qgroundcontrol.com +abstract: >- + QGroundControl is an intuitive and powerful ground control station + for UAVs. It provides full flight control and mission planning for + any MAVLink-enabled drone, including PX4 and ArduPilot vehicles. +keywords: + - ground-control-station + - uav + - drone + - mavlink + - px4 + - ardupilot + - flight-planning + - telemetry +license: Apache-2.0 AND GPL-3.0-only diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..65c607ee5ac2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,15 @@ +# CODEOWNERS - Auto-assign reviewers to pull requests +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# Default owners for everything +# * @mavlink/qgc-maintainers + +# Documentation +/docs/ @hamishwillee + +# QML/UI +*.qml @DonLakeFlyer + +# Build system +CMakeLists.txt @HTRamsey +/cmake/ @HTRamsey diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ff794fcc9826..7192165eac94 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -21,7 +21,7 @@ Before you begin, please: 1. Read the [Developer Guide](https://dev.qgroundcontrol.com/en/) 2. Review the [Build Instructions](https://dev.qgroundcontrol.com/en/getting_started/) -3. Familiarize yourself with the [Architecture](copilot-instructions.md) +3. Familiarize yourself with the [Architecture Patterns](#architecture-patterns) in this guide ### Development Environment @@ -59,6 +59,10 @@ Feature requests are welcome! Please: 3. Consider implementation complexity 4. Be prepared to contribute code if possible +### Contributing Translations + +QGroundControl uses [Crowdin](https://crowdin.com/project/qgroundcontrol) for community translations. See [tools/translations/README.md](../tools/translations/README.md) for details on how translations are managed. + ### Contributing Code 1. **Fork the repository** @@ -96,6 +100,8 @@ Feature requests are welcome! Please: ## Coding Standards +For the complete coding style guide with examples, see [CODING_STYLE.md](../CODING_STYLE.md). + ### C++ Guidelines - **Standard**: C++20 @@ -126,7 +132,8 @@ Feature requests are welcome! Please: - **Code formatting**: - Run `clang-format` before committing - - Follow `.clang-format` in the repository + - Follow `.clang-format`, `.clang-tidy`, `.editorconfig` in repo root + - See `CodingStyle.h`, `CodingStyle.cc`, `CodingStyle.qml` for examples - 4 spaces for indentation (no tabs) ### QML Guidelines @@ -151,36 +158,66 @@ qCCritical(MyComponentLog) << "Critical error"; ### Architecture Patterns -#### Fact System (Required for Parameters) +#### Fact System (Most Important!) -Always use the Fact System for vehicle parameters: +The Fact System handles ALL vehicle parameters. Never create custom parameter storage. ```cpp +// Access parameters (always null-check!) Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME"); -if (param) { - param->setCookedValue(newValue); // For display values - // param->setRawValue(newValue); // For MAVLink values +if (param && param->validate(newValue, false).isEmpty()) { + param->setCookedValue(newValue); // Use cookedValue for UI (with units) + // param->rawValue() for MAVLink/storage } ``` -#### Multi-Vehicle Awareness +**Key classes:** +- `Fact` - Single parameter with validation, units, metadata +- `FactGroup` - Hierarchical container (handles MAVLink via `handleMessage()`) +- `FactMetaData` - JSON-based metadata (min/max, enums, descriptions) + +**Rules:** +- Wait for `parametersReady` signal before accessing +- Use `cookedValue` (display) vs `rawValue` (storage) +- Metadata in `*.FactMetaData.json` files -Always check for null vehicles: +#### Multi-Vehicle Support + +Always null-check the active vehicle: ```cpp Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle(); -if (vehicle) { - // Use vehicle -} +if (!vehicle) return; + +// Other managers +SettingsManager::instance()->appSettings()->... +LinkManager::instance()->... ``` #### Firmware Plugin System -Use FirmwarePlugin for firmware-specific behavior instead of hardcoding: +Use FirmwarePlugin for firmware-specific behavior: ```cpp -vehicle->firmwarePlugin()->isCapable(capability); +// FirmwarePlugin - Firmware behavior (flight modes, capabilities) vehicle->firmwarePlugin()->flightModes(); +vehicle->firmwarePlugin()->isCapable(capability); + +// AutoPilotPlugin - Vehicle setup UI +// VehicleComponent - Individual setup items (Radio, Sensors, Safety) +``` + +#### QML/C++ Integration + +```cpp +Q_OBJECT +QML_ELEMENT // Creatable in QML +QML_SINGLETON // Singleton +QML_UNCREATABLE("") // C++-only + +Q_PROPERTY(Type name READ getter WRITE setter NOTIFY signal) +Q_INVOKABLE void method(); +Q_ENUM(EnumType) ``` --- @@ -308,7 +345,6 @@ For more details, see [COPYING.md](COPYING.md). - **User Manual**: https://docs.qgroundcontrol.com/en/ - **Developer Guide**: https://dev.qgroundcontrol.com/en/ -- **API Documentation**: In-code documentation and `copilot-instructions.md` - **Support Guide**: For help and community resources, see [SUPPORT.md](SUPPORT.md) - **Discussion Forum**: https://discuss.px4.io/c/qgroundcontrol - **Discord**: https://discord.gg/dronecode diff --git a/.github/DISCUSSION_TEMPLATE/polls.yml b/.github/DISCUSSION_TEMPLATE/polls.yml new file mode 100644 index 000000000000..7bfd1855c961 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/polls.yml @@ -0,0 +1,18 @@ +title: "[Poll] " +labels: [] +body: + - type: markdown + attributes: + value: | + Create a poll to gather community feedback. After creating this discussion, use the poll button in the editor to add your poll options. + + - type: textarea + id: context + attributes: + label: Poll Context + description: Provide background information for your poll + placeholder: | + What decision or feedback are you seeking? + Why is this important to the community? + validations: + required: true diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml new file mode 100644 index 000000000000..0c6348d8e73d --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/q-a.yml @@ -0,0 +1,86 @@ +title: "[Q&A] " +labels: [] +body: + - type: markdown + attributes: + value: | + Thanks for your question! Please check the following resources first: + - [User Guide](https://docs.qgroundcontrol.com/) + - [Developer Guide](https://dev.qgroundcontrol.com/) + - [Existing discussions](https://github.com/mavlink/qgroundcontrol/discussions) + + - type: dropdown + id: category + attributes: + label: Question Category + description: What area does your question relate to? + options: + - Setup / Installation + - Vehicle Connection + - Flight Planning / Missions + - Video Streaming + - Telemetry / Parameters + - Custom Builds + - Development / Building from Source + - Other + validations: + required: true + + - type: dropdown + id: platform + attributes: + label: Platform + description: What platform are you using? + options: + - Windows + - macOS + - Linux + - Android + - iOS + - Multiple / N/A + validations: + required: true + + - type: dropdown + id: firmware + attributes: + label: Flight Stack + description: What firmware are you using? + options: + - PX4 + - ArduPilot + - Both / Either + - N/A + validations: + required: false + + - type: input + id: version + attributes: + label: QGC Version + description: Which version of QGroundControl? + placeholder: "e.g., 4.4.0, daily build" + validations: + required: false + + - type: textarea + id: question + attributes: + label: Question + description: Describe your question in detail + placeholder: | + What I'm trying to do: + + What I've tried: + + What I expected vs what happened: + validations: + required: true + + - type: textarea + id: context + attributes: + label: Additional Context + description: Any additional information, screenshots, or logs + validations: + required: false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000000..537969bcaf4f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,4 @@ +# Funding links displayed on repository sidebar +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository + +custom: ['https://www.dronecode.org'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index ef52ed76da6f..a81ac38a42a5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug report description: Create a report to help us improve -labels: ["Report: Bug"] +labels: ["bug"] type: bug body: @@ -30,7 +30,7 @@ body: 1. **Expected Behavior** – What you expected to happen. 2. **Current Behavior** – What actually happened instead. 3. **Steps To Reproduce** – Step-by-step list of actions to reproduce the issue. - 3. **Additional Details** – Any other context that helps explain the problem. + 4. **Additional Details** – Any other context that helps explain the problem. value: | **Expected Behavior** Describe what you thought should happen here. @@ -48,6 +48,31 @@ body: validations: required: true + - type: dropdown + id: platform + attributes: + label: Platform + description: Which platform are you running QGC on? + options: + - Windows + - macOS + - Linux + - Android + - iOS + validations: + required: true + + - type: dropdown + id: flight-stack + attributes: + label: Flight Stack + description: Which flight stack are you using? + options: + - PX4 + - ArduPilot + - Other + - N/A + - type: textarea id: system-information attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 791982fa7e52..ddae3345f48d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,20 @@ blank_issues_enabled: true contact_links: + - name: "❓ Questions & Help" + url: https://github.com/mavlink/qgroundcontrol/discussions/categories/q-a + about: "Ask questions in GitHub Discussions - please don't open issues for support questions." - name: "💬 Developer Chat" url: https://discord.gg/dronecode - about: "QGroundControl developers (and many regular/deeply-involved users) can be found on the #QGroundControl channel on the Dronecode Discord." + about: "Chat with developers on the #QGroundControl channel on Dronecode Discord." + - name: "📖 User Guide" + url: https://docs.qgroundcontrol.com/ + about: "Check the QGC User Guide for usage questions and troubleshooting." + - name: "🛠️ Developer Guide" + url: https://dev.qgroundcontrol.com/ + about: "Developer documentation for building and contributing to QGC." + - name: "💬 PX4 Forum" + url: https://discuss.px4.io/c/qgroundcontrol/15 + about: "Community discussion forum for QGC topics." + - name: "🔍 Search Existing Issues" + url: https://github.com/mavlink/qgroundcontrol/issues?q=is%3Aissue + about: "Search for existing issues before creating a new one." diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7c29789ac7d0..0cb1cdc35796 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature request description: Tell us about your new idea -labels: ["Feature Request"] +labels: ["enhancement"] type: feature body: @@ -22,6 +22,13 @@ body: validations: required: true + - type: textarea + id: use-case + attributes: + label: Use Case + description: What problem does this feature solve? Describe the scenario where this would be useful. + placeholder: "As a user, I want to... so that I can..." + - type: dropdown id: flight-stacks attributes: @@ -42,4 +49,20 @@ body: - Multirotor - Fixed-wing - VTOL + - Rover + - Boat - Submarine + + - type: dropdown + id: platforms + attributes: + label: Platforms + description: Select the platforms this feature should work on. + multiple: true + options: + - Windows + - macOS + - Linux + - Android + - iOS + - All diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml index 40a3ec505eaa..404d9b665b1d 100644 --- a/.github/ISSUE_TEMPLATE/question.yml +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -1,6 +1,7 @@ name: Question description: Ask a question related to QGC source -labels: ["Question"] +labels: ["question"] +type: question body: - type: markdown @@ -11,9 +12,27 @@ body: - If your question still isn't answered, please check the forums: https://discuss.px4.io/c/qgroundcontrol/15. - If it is about Qt or any QGC dependencies, please refer to them instead. + **Note:** For more involved discussions or design questions, please use [GitHub Discussions](https://github.com/mavlink/qgroundcontrol/discussions) instead. + + - type: dropdown + id: area + attributes: + label: Area + description: What area of QGC is your question about? + options: + - UI/QML + - Video Streaming + - MAVLink/Communication + - Vehicle/Flight Stack + - Build System + - Testing + - Other + - type: textarea id: question attributes: label: Your Question description: Write your question related to the QGC source code. placeholder: Enter your question here. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 000000000000..3bcbdbea9a97 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,13 @@ +name: Task +description: Record a task for maintainers +labels: ["task"] +type: task + +body: + - type: textarea + id: description + attributes: + label: Description + description: What needs to be done? + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE/bugfix.md b/.github/PULL_REQUEST_TEMPLATE/bugfix.md new file mode 100644 index 000000000000..9441431f6d5a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bugfix.md @@ -0,0 +1,39 @@ +## Bug Description + + +Fixes # + +## Root Cause + + + +## Solution + + + +## Testing +- [ ] Tested locally +- [ ] Added regression test +- [ ] Tested with simulator (SITL) +- [ ] Tested with hardware + +### Platforms Tested +- [ ] Linux +- [ ] Windows +- [ ] macOS +- [ ] Android +- [ ] iOS + +### Flight Stacks Tested +- [ ] PX4 +- [ ] ArduPilot +- [ ] N/A + +## Checklist +- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md) +- [ ] My code follows the project's coding standards +- [ ] I have added a test that reproduces the bug +- [ ] New and existing unit tests pass locally + +--- +By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3). diff --git a/.github/PULL_REQUEST_TEMPLATE/ci.md b/.github/PULL_REQUEST_TEMPLATE/ci.md new file mode 100644 index 000000000000..f2a1a38aa58d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/ci.md @@ -0,0 +1,24 @@ +## CI/Build Changes + + + +## Reason + + + +## Testing +- [ ] Tested workflow locally (act or similar) +- [ ] Verified YAML syntax +- [ ] Tested on fork before submitting + +## Impact + + + +## Checklist +- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md) +- [ ] Workflow permissions follow least-privilege principle +- [ ] No secrets are exposed in logs + +--- +By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3). diff --git a/.github/PULL_REQUEST_TEMPLATE/docs.md b/.github/PULL_REQUEST_TEMPLATE/docs.md new file mode 100644 index 000000000000..a7e4800801cc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/docs.md @@ -0,0 +1,20 @@ +## Documentation Changes + + + +## Reason + + + +## Checklist +- [ ] I have checked for spelling/grammar errors +- [ ] Links are valid and working +- [ ] Screenshots are up to date (if applicable) +- [ ] I have previewed the changes locally + +## Related Issues + + + +--- +By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3). diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md new file mode 100644 index 000000000000..2a9ecbd6e1d6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -0,0 +1,41 @@ +## Feature Description + + + +## Implementation Details + + + +## Testing +- [ ] Tested locally +- [ ] Added/updated unit tests +- [ ] Tested with simulator (SITL) +- [ ] Tested with hardware + +### Platforms Tested +- [ ] Linux +- [ ] Windows +- [ ] macOS +- [ ] Android +- [ ] iOS + +### Flight Stacks Tested +- [ ] PX4 +- [ ] ArduPilot + +## Screenshots / Demo + + + +## Checklist +- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md) +- [ ] My code follows the project's coding standards +- [ ] I have added tests that prove my feature works +- [ ] New and existing unit tests pass locally + +## Related Issues + + + +--- +By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3). diff --git a/.github/PULL_REQUEST_TEMPLATE/maintainer.md b/.github/PULL_REQUEST_TEMPLATE/maintainer.md new file mode 100644 index 000000000000..d94fb27084b6 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/maintainer.md @@ -0,0 +1,6 @@ +## Summary + + + +## Related Issues + diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 572629861a8b..24d78e216736 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -6,9 +6,8 @@ The following is a list of versions the development team is currently supporting | Version | Supported | | ------- | ------------------ | -| 5.0 | :white_check_mark: | -| 4.4.x | :white_check_mark: | -| < 4.4 | :x: | +| 5.0.x | :white_check_mark: | +| < 5.0 | :x: | ## Reporting a Vulnerability diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 5b140f13f73f..1e387ed91ec5 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -7,8 +7,8 @@ Welcome to the QGroundControl support guide. This document provides information ## 1. Getting Started - **Documentation**: Comprehensive user and developer guides are available on the official website: - - User Guide: https://docs.qgroundcontrol.com/master/en/qgc-user-guide/ - - Developer Guide: https://docs.qgroundcontrol.com/master/en/qgc-dev-guide/ + - User Guide: https://docs.qgroundcontrol.com/ + - Developer Guide: https://dev.qgroundcontrol.com/ --- @@ -51,22 +51,20 @@ We welcome contributions of all kinds: - **Source Repository**: https://github.com/mavlink/qgroundcontrol - **Contribution Guide**: https://dev.qgroundcontrol.com/en/contribute/contribute.html -- **Coding Standards**: Follow Qt 6 and C++17 guidelines; see `.clang-format`, `.pre-commit-config.yaml`, and CodingStyle files. +- **Coding Standards**: Follow Qt 6 and C++20 guidelines; see `.clang-format` and `.pre-commit-config.yaml`. - **Pull Requests**: Ensure all CI checks pass and include relevant test coverage. --- ## 6. Security and Privacy -To report security vulnerabilities or privacy concerns: - -- Submit an issue and include a detailed description and any proof-of-concept code. +To report security vulnerabilities, please use GitHub's Security tab to privately report the issue. See [SECURITY.md](SECURITY.md) for details. --- ## 7. License -QGroundControl is licensed under the GNU General Public License v3.0. See [LICENSE](https://github.com/mavlink/qgroundcontrol/blob/master/LICENSE) for details. +QGroundControl is dual-licensed under the Apache License 2.0 and the GNU General Public License v3. See [COPYING.md](COPYING.md) for details. --- diff --git a/.github/actions/build-action/action.yml b/.github/actions/build-action/action.yml index 9f7f22c4f54b..700186f96d62 100644 --- a/.github/actions/build-action/action.yml +++ b/.github/actions/build-action/action.yml @@ -11,13 +11,13 @@ inputs: runs: using: composite steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: repository: ${{ inputs.repo }} ref: ${{ inputs.ref }} path: build-action - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22 cache: npm diff --git a/.github/actions/build-config/action.yml b/.github/actions/build-config/action.yml new file mode 100644 index 000000000000..dd04ab5ea4cd --- /dev/null +++ b/.github/actions/build-config/action.yml @@ -0,0 +1,66 @@ +name: Build Config +description: Read build toolchain versions from central config +outputs: + qt_version: + description: Qt version + value: ${{ steps.read.outputs.qt_version }} + qt_modules: + description: Qt modules (desktop) + value: ${{ steps.read.outputs.qt_modules }} + qt_modules_ios: + description: Qt modules (iOS) - excludes unsupported modules + value: ${{ steps.read.outputs.qt_modules_ios }} + gstreamer_version: + description: GStreamer version + value: ${{ steps.read.outputs.gstreamer_version }} + xcode_version: + description: Xcode version for macOS/iOS builds + value: ${{ steps.read.outputs.xcode_version }} + ndk_version: + description: Android NDK version + value: ${{ steps.read.outputs.ndk_version }} + java_version: + description: Java version for Android builds + value: ${{ steps.read.outputs.java_version }} + android_platform: + description: Android platform/API level + value: ${{ steps.read.outputs.android_platform }} + ccache_version: + description: ccache version + value: ${{ steps.read.outputs.ccache_version }} +runs: + using: composite + steps: + - name: Validate build config + shell: bash + run: | + CONFIG_FILE="${GITHUB_WORKSPACE}/.github/build-config.json" + if ! jq empty "$CONFIG_FILE" 2>/dev/null; then + echo "::error::Invalid JSON in $CONFIG_FILE" + exit 1 + fi + + - name: Read build config + id: read + shell: bash + run: | + CONFIG_FILE="${GITHUB_WORKSPACE}/.github/build-config.json" + QT_VERSION=$(jq -r '.qt_version' "$CONFIG_FILE") + QT_MODULES=$(jq -r '.qt_modules' "$CONFIG_FILE") + GST_VERSION=$(jq -r '.gstreamer_version // "1.22.12"' "$CONFIG_FILE") + XCODE_VERSION=$(jq -r '.xcode_version // "latest-stable"' "$CONFIG_FILE") + NDK_VERSION=$(jq -r '.ndk_version // "r27c"' "$CONFIG_FILE") + JAVA_VERSION=$(jq -r '.java_version // "17"' "$CONFIG_FILE") + ANDROID_PLATFORM=$(jq -r '.android_platform // "35"' "$CONFIG_FILE") + CCACHE_VERSION=$(jq -r '.ccache_version // "4.11.3"' "$CONFIG_FILE") + # Filter out modules not supported on iOS (no serial port support) + QT_MODULES_IOS=$(echo "$QT_MODULES" | sed 's/qtserialport//g; s/ */ /g; s/^ *//; s/ *$//') + echo "qt_version=$QT_VERSION" >> $GITHUB_OUTPUT + echo "qt_modules=$QT_MODULES" >> $GITHUB_OUTPUT + echo "qt_modules_ios=$QT_MODULES_IOS" >> $GITHUB_OUTPUT + echo "gstreamer_version=$GST_VERSION" >> $GITHUB_OUTPUT + echo "xcode_version=$XCODE_VERSION" >> $GITHUB_OUTPUT + echo "ndk_version=$NDK_VERSION" >> $GITHUB_OUTPUT + echo "java_version=$JAVA_VERSION" >> $GITHUB_OUTPUT + echo "android_platform=$ANDROID_PLATFORM" >> $GITHUB_OUTPUT + echo "ccache_version=$CCACHE_VERSION" >> $GITHUB_OUTPUT diff --git a/.github/actions/cache/action.yml b/.github/actions/cache/action.yml index f846bfa8cb22..28e9a26f8cf9 100644 --- a/.github/actions/cache/action.yml +++ b/.github/actions/cache/action.yml @@ -10,61 +10,64 @@ inputs: build-type: description: Build Type required: true - type: choice - options: - - Debug - - Release cpm-modules: description: Path to CPM Modules required: false ccache-version: - description: ccache Version to Install + description: ccache version (use build-config.outputs.ccache_version) required: false - default: 4.11.3 + default: '4.11.3' + ccache-max-size: + description: Maximum ccache size + required: false + default: '2G' + save-cache: + description: Save cache (set to false for PR builds to save storage) + required: false + default: 'true' runs: using: composite steps: + - name: Cache ccache binary (Linux) + if: runner.os == 'Linux' && inputs.target != 'linux_gcc_arm64' + id: ccache-binary + uses: actions/cache@v4 + with: + path: /usr/local/bin/ccache + key: ccache-binary-${{ inputs.ccache-version }}-linux-x86_64 + - name: Install ccache (Linux) - if: inputs.host == 'linux' && inputs.target != 'linux_gcc_arm64' + if: runner.os == 'Linux' && inputs.target != 'linux_gcc_arm64' && steps.ccache-binary.outputs.cache-hit != 'true' shell: bash run: | set -e - echo "Downloading ccache..." wget --quiet https://github.com/ccache/ccache/releases/download/v${{ inputs.ccache-version }}/ccache-${{ inputs.ccache-version }}-linux-x86_64.tar.xz - echo "Extracting archive..." - tar -xvf ccache-${{ inputs.ccache-version }}-linux-x86_64.tar.xz - cd ccache-${{ inputs.ccache-version }}-linux-x86_64 - echo "Installing ccache..." - sudo make install - - # - name: Setup sccache (Windows) - # if: runner.os == 'Windows' && inputs.target != 'android' - # uses: mozilla-actions/sccache-action@v0.0.9 + tar -xf ccache-${{ inputs.ccache-version }}-linux-x86_64.tar.xz + sudo make -C ccache-${{ inputs.ccache-version }}-linux-x86_64 install - - name: Setup sccache (Windows) - if: runner.os == 'Windows' && inputs.target != 'android' - uses: ./.github/actions/build-action - with: - repo: mozilla-actions/sccache-action - ref: main + - name: Setup sccache (Windows x64 only) + if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' + uses: mozilla-actions/sccache-action@v0.0.9 - run: echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV" - if: runner.os == 'Windows' && inputs.target != 'android' + if: runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' shell: bash - name: Setup Build Cache - uses: hendrikmuhs/ccache-action@main + uses: hendrikmuhs/ccache-action@v1.2 with: - variant: ${{ runner.os == 'Windows' && inputs.target != 'android' && 'sccache' || 'ccache' }} + variant: ${{ runner.os == 'Windows' && inputs.target != 'android' && inputs.host != 'windows_arm64' && 'sccache' || 'ccache' }} key: ${{ inputs.host }}-${{ inputs.target }}-${{ inputs.build-type }} restore-keys: | ${{ inputs.host }}-${{ inputs.target }} ${{ inputs.host }}- - max-size: 1G - verbose: 1 + max-size: ${{ inputs.ccache-max-size }} + verbose: 2 evict-old-files: job + save: ${{ inputs.save-cache == 'true' }} - name: Ensure cpm-modules directory exists + if: inputs.cpm-modules != '' run: mkdir -p "${{ inputs.cpm-modules }}" shell: bash @@ -75,4 +78,3 @@ runs: path: ${{ inputs.cpm-modules }} key: ${{ github.workflow }}-cpm-modules-${{ hashFiles('**/CMakeLists.txt', '**/*.cmake') }} restore-keys: ${{ github.workflow }}-cpm-modules- - # enableCrossOsArchive: true diff --git a/.github/actions/checks/action.yml b/.github/actions/checks/action.yml deleted file mode 100644 index 949a76acd196..000000000000 --- a/.github/actions/checks/action.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Source Checks -description: Run Various Checks on Source -inputs: - format: - description: Run Clang Format - default: 'false' - spelling: - description: Run Spelling - default: 'false' -runs: - using: "composite" - steps: - - uses: actions/checkout@v4 - - - name: Run clang-format style check for C++ Source Files. - if: inputs.format == 'true' - uses: jidicula/clang-format-action@main - with: - clang-format-version: '17' - check-path: 'src' - - - name: Check spelling - if: inputs.spelling == 'true' - uses: crate-ci/typos@master diff --git a/.github/actions/common/action.yml b/.github/actions/common/action.yml index 37ff9f3e364b..8e599acdc741 100644 --- a/.github/actions/common/action.yml +++ b/.github/actions/common/action.yml @@ -5,15 +5,10 @@ runs: steps: - uses: lukka/get-cmake@latest - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '>=3.9' - name: Create build directory shell: bash run: mkdir -p "$RUNNER_TEMP/build" - - - name: Fetch all tags (for version-string logic) - shell: bash - working-directory: ${{ github.workspace }} - run: git fetch --all --tags --force --depth 1 diff --git a/.github/actions/custom-build/action.yml b/.github/actions/custom-build/action.yml new file mode 100644 index 000000000000..ab0a86964452 --- /dev/null +++ b/.github/actions/custom-build/action.yml @@ -0,0 +1,23 @@ +name: Enable Custom Build +description: Copies custom-example folder to enable custom build configuration + +runs: + using: composite + steps: + - name: Enable custom build (Windows) + if: runner.os == 'Windows' + shell: cmd + run: | + if not exist ".\custom-example" ( + echo Directory ".\custom-example" does not exist. && exit /b 1 + ) + xcopy /E /I ".\custom-example" ".\custom" + + - name: Enable custom build (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + if [ ! -d "./custom-example" ]; then + echo "Directory ./custom-example does not exist." && exit 1 + fi + cp -r ./custom-example ./custom diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml index 2f981476ec45..b52524fdf25c 100644 --- a/.github/actions/docker/action.yml +++ b/.github/actions/docker/action.yml @@ -1,8 +1,44 @@ -# action.yml -name: "QGC Linux Builder" -description: "Helper action to build QGC in Ubuntu 22.04" +name: Docker Build +description: Build QGC using Docker (Ubuntu) + +inputs: + build-type: + description: CMake build type (Release or Debug) + required: false + default: 'Release' + runs: - using: "docker" - image: "./deploy/docker/Dockerfile-build-ubuntu" - args: - - ${{ inputs.build-type }} + using: composite + steps: + - name: Get build config + id: config + uses: ./.github/actions/build-config + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./deploy/docker/Dockerfile-build-ubuntu + tags: qgc-ubuntu-builder:latest + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + QT_VERSION=${{ steps.config.outputs.qt_version }} + + - name: Run Docker build + shell: bash + run: | + mkdir -p "${{ github.workspace }}/build" + docker run \ + --rm \ + --cap-add SYS_ADMIN \ + --device /dev/fuse \ + --security-opt apparmor:unconfined \ + -v "${{ github.workspace }}:/project/source" \ + -v "${{ github.workspace }}/build:/project/build" \ + -e BUILD_TYPE="${{ inputs.build-type }}" \ + qgc-ubuntu-builder:latest diff --git a/.github/actions/gstreamer/action.yml b/.github/actions/gstreamer/action.yml index 650a4c434ca0..8ccd42e37d7b 100644 --- a/.github/actions/gstreamer/action.yml +++ b/.github/actions/gstreamer/action.yml @@ -1,107 +1,47 @@ name: Build GStreamer -description: Builds GStreamer using Meson +description: Build GStreamer from source for QGC video streaming inputs: - gst_version: - description: Version of GStreamer to Build - required: true - default: 1.24.8 - build_type: - description: Build Type "release" or "debug" + version: + description: GStreamer version (use build-config.outputs.gstreamer_version) required: true + build-type: + description: Build type (release or debug) + required: false default: release - working_directory: - description: Where to clone GStreamer source - required: true + work-dir: + description: Working directory for source + required: false default: ${{ runner.temp }} - install_directory: - description: Where to install GStreamer Build - required: true + prefix: + description: Install prefix + required: false default: ${{ runner.temp }}/gst runs: - using: "composite" + using: composite steps: - - name: Clone GStreamer - working-directory: ${{ inputs.working_directory }} - run: git clone --depth 1 --branch ${{ inputs.gst_version }} https://github.com/GStreamer/gstreamer.git - shell: bash - - - name: Install Dependencies - run: python3 -m pip install --user ninja meson - shell: bash - - - name: Configure GStreamer - working-directory: ${{ inputs.working_directory }}/gstreamer - run: meson setup - --prefix=${{ inputs.install_directory }} - --buildtype=${{ inputs.build_type }} - --wrap-mode=forcefallback - --strip - -Dauto_features=disabled - -Dgst-full-libraries=video,gl - -Dgpl=enabled - -Dlibav=enabled - -Dorc=enabled - -Dqt6=enabled - -Dvaapi=enabled - -Dbase=enabled - -Dgst-plugins-base:app=enabled - -Dgst-plugins-base:gl=enabled - -Dgst-plugins-base:gl_api=opengl,gles2 - -Dgst-plugins-base:gl_platform=glx,egl - -Dgst-plugins-base:gl_winsys=x11,egl,wayland - -Dgst-plugins-base:playback=enabled - -Dgst-plugins-base:tcp=enabled - -Dgst-plugins-base:x11=enabled - -Dgood=enabled - -Dgst-plugins-good:isomp4=enabled - -Dgst-plugins-good:matroska=enabled - -Dgst-plugins-good:qt-egl=enabled - -Dgst-plugins-good:qt-method=auto - -Dgst-plugins-good:qt-wayland=enabled - -Dgst-plugins-good:qt-x11=enabled - -Dgst-plugins-good:qt6=enabled - -Dgst-plugins-good:rtp=enabled - -Dgst-plugins-good:rtpmanager=enabled - -Dgst-plugins-good:rtsp=enabled - -Dgst-plugins-good:udp=enabled - -Dbad=enabled - -Dgst-plugins-bad:gl=enabled - -Dgst-plugins-bad:mpegtsdemux=enabled - -Dgst-plugins-bad:rtp=enabled - -Dgst-plugins-bad:sdp=enabled - -Dgst-plugins-bad:va=enabled - -Dgst-plugins-bad:videoparsers=enabled - -Dgst-plugins-bad:wayland=enabled - -Dgst-plugins-bad:x11=enabled - -Dgst-plugins-bad:x265=enabled - -Dugly=enabled - -Dgst-plugins-ugly:x264=enabled - builddir - # --default-library=static - # --prefer_static=true - # -Dgst-full-target-type=static_library - # -Dgstreamer:gstreamer-static-full=true - shell: bash - - - name: Compile GStreamer - working-directory: ${{ inputs.working_directory }}/gstreamer - run: meson compile -C builddir - shell: bash - - - name: Install GStreamer - working-directory: ${{ inputs.working_directory }}/gstreamer - run: meson install -C builddir + - name: Build GStreamer shell: bash + run: | + "${{ github.workspace }}/tools/setup/build-gstreamer.sh" \ + --version "${{ inputs.version }}" \ + --type "${{ inputs.build-type }}" \ + --work-dir "${{ inputs.work-dir }}" \ + --prefix "${{ inputs.prefix }}" - name: Setup Environment - working-directory: ${{ runner.temp }}/gstreamer - run: > - echo "PKG_CONFIG_PATH=${{ inputs.install_directory }}/lib/x86_64-linux-gnu/pkgconfig:${{ - inputs.install_directory }}/lib/x86_64-linux-gnu/gstreamer-1.0/pkgconfig:${{ env.PKG_CONFIG_PATH }}" >> "$GITHUB_ENV" shell: bash + run: | + ARCH=$(uname -m) + case "$ARCH" in + x86_64) ARCH_DIR="x86_64-linux-gnu" ;; + aarch64) ARCH_DIR="aarch64-linux-gnu" ;; + *) ARCH_DIR="$ARCH-linux-gnu" ;; + esac + echo "PKG_CONFIG_PATH=${{ inputs.prefix }}/lib/$ARCH_DIR/pkgconfig:$PKG_CONFIG_PATH" >> "$GITHUB_ENV" + echo "GST_PLUGIN_PATH=${{ inputs.prefix }}/lib/$ARCH_DIR/gstreamer-1.0" >> "$GITHUB_ENV" - name: Save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: - name: GStreamer-${{ inputs.build_type }} - path: ${{ inputs.install_directory }} + name: GStreamer-${{ inputs.build-type }} + path: ${{ inputs.prefix }} diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 000000000000..f67a0d58dc52 --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,60 @@ +name: Install Dependencies +description: Install platform-specific build dependencies (GStreamer, etc.) + +inputs: + gstreamer: + description: 'Install GStreamer (Windows x64 only)' + required: false + default: 'true' + gstreamer-version: + description: 'GStreamer version (default: from build-config.json)' + required: false + default: '' + vulkan: + description: 'Install Vulkan SDK (Windows only, for validation layers)' + required: false + default: 'false' + extra-packages: + description: 'Additional packages to install (space-separated, Linux only)' + required: false + default: '' + +runs: + using: composite + steps: + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + $params = @{} + if ('${{ inputs.gstreamer-version }}') { + $params['GStreamerVersion'] = '${{ inputs.gstreamer-version }}' + } + if ('${{ inputs.gstreamer }}' -ne 'true') { + $params['SkipGStreamer'] = $true + } + if ('${{ inputs.vulkan }}' -eq 'true') { + $params['InstallVulkan'] = $true + } + & "${{ github.workspace }}/tools/setup/install-dependencies-windows.ps1" @params + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + if [ -f "./tools/setup/install-dependencies-debian.sh" ]; then + chmod a+x ./tools/setup/install-dependencies-debian.sh + sudo ./tools/setup/install-dependencies-debian.sh + fi + if [ -n "${{ inputs.extra-packages }}" ]; then + sudo apt-get install -y ${{ inputs.extra-packages }} + fi + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + if [ -f "./tools/setup/install-dependencies-osx.sh" ]; then + chmod a+x ./tools/setup/install-dependencies-osx.sh + ./tools/setup/install-dependencies-osx.sh + fi diff --git a/.github/actions/playstore/action.yml b/.github/actions/playstore/action.yml index d3364f9d8b18..ca2f1765bcb9 100644 --- a/.github/actions/playstore/action.yml +++ b/.github/actions/playstore/action.yml @@ -1,5 +1,5 @@ name: Publish Android Build to Play Store -description: Checks out the QGC repo with all the correct settings +description: Upload Android APK to Google Play Store inputs: artifact: description: Build File To Upload @@ -11,8 +11,8 @@ runs: using: "composite" steps: - name: Deploy to Play Store - if: ${{ github.event_name != 'pull_request' && github.ref_name == 'master' }} - uses: r0adkll/upload-google-play@v1 + if: github.event_name != 'pull_request' && (github.ref_name == 'master' || github.ref_type == 'tag') + uses: r0adkll/upload-google-play@v1.1.3 with: serviceAccountJsonPlainText: ${{ inputs.service_account_json }} packageName: com.mavlink.qgroundcontrol diff --git a/.github/actions/qt-android/action.yml b/.github/actions/qt-android/action.yml index 3a2283354240..1747f76222cd 100644 --- a/.github/actions/qt-android/action.yml +++ b/.github/actions/qt-android/action.yml @@ -9,8 +9,10 @@ inputs: required: true version: description: Qt Version - required: false - default: 6.10.1 + required: true + modules: + description: Qt Modules + required: true abis: description: ABIs to Build required: false @@ -18,14 +20,38 @@ inputs: cpm-modules: description: CPM Cache Path required: false + build-type: + description: Build Type (Debug or Release) + required: false + default: Release + ndk-version: + description: Android NDK version (use build-config.outputs.ndk_version) + required: false + default: 'r27c' + java-version: + description: Java version (use build-config.outputs.java_version) + required: false + default: '17' + android-platform: + description: Android platform/API level (use build-config.outputs.android_platform) + required: false + default: '35' + ccache-version: + description: ccache version (use build-config.outputs.ccache_version) + required: false + default: '4.11.3' + save-cache: + description: Save cache (set to false for PR builds) + required: false + default: 'true' runs: using: composite steps: - name: Setup Java Environment - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin - java-version: 17 + java-version: ${{ inputs.java-version }} cache: gradle cache-dependency-path: | **/*.gradle* @@ -36,14 +62,14 @@ runs: id: setup-android with: cmdline-tools-version: 13114758 - packages: 'platform-tools platforms;android-35 build-tools;35.0.0' # ndk;27.2.12479018' + packages: 'platform-tools platforms;android-${{ inputs.android-platform }} build-tools;${{ inputs.android-platform }}.0.0' log-accepted-android-sdk-licenses: false - name: Install Android NDK uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: r27c + ndk-version: ${{ inputs.ndk-version }} add-to-path: false local-cache: true @@ -66,8 +92,10 @@ runs: with: host: ${{ inputs.host }} target: android - build-type: ${{ matrix.BuildType }} + build-type: ${{ inputs.build-type }} cpm-modules: ${{ inputs.cpm-modules }} + ccache-version: ${{ inputs.ccache-version }} + save-cache: ${{ inputs.save-cache }} - name: Install Qt for ${{ runner.os }} uses: jurplel/install-qt-action@v4 @@ -77,12 +105,12 @@ runs: target: desktop arch: ${{ inputs.arch }} dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ inputs.modules }} setup-python: false cache: true - name: Install Qt for Android (arm64_v8a) - if: contains( inputs.abis, 'arm64-v8a') + if: contains(inputs.abis, 'arm64-v8a') uses: jurplel/install-qt-action@v4 with: version: ${{ inputs.version }} @@ -90,12 +118,12 @@ runs: target: android arch: android_arm64_v8a dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ inputs.modules }} setup-python: false cache: true - name: Install Qt for Android (armv7) - if: contains( inputs.abis, 'armeabi-v7a') + if: contains(inputs.abis, 'armeabi-v7a') uses: jurplel/install-qt-action@v4 with: version: ${{ inputs.version }} @@ -103,12 +131,12 @@ runs: target: android arch: android_armv7 dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ inputs.modules }} setup-python: false cache: true - name: Install Qt for Android (x86_64) - if: contains( inputs.abis, 'x86_64') + if: contains(inputs.abis, 'x86_64') uses: jurplel/install-qt-action@v4 with: version: ${{ inputs.version }} @@ -116,12 +144,12 @@ runs: target: android arch: android_x86_64 dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ inputs.modules }} setup-python: false cache: true - name: Install Qt for Android (x86) - if: contains( inputs.abis, 'x86') + if: contains(inputs.abis, 'x86') uses: jurplel/install-qt-action@v4 with: version: ${{ inputs.version }} @@ -129,6 +157,6 @@ runs: target: android arch: android_x86 dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ inputs.modules }} setup-python: false cache: true diff --git a/.github/actions/upload/action.yml b/.github/actions/upload/action.yml index be52339f418e..921f41abbad9 100644 --- a/.github/actions/upload/action.yml +++ b/.github/actions/upload/action.yml @@ -16,22 +16,24 @@ inputs: aws_distribution_id: description: AWS distribution ID required: false - github_token: - description: GitHub Token - required: false upload_aws: description: Upload artifact to AWS - required: true + required: false default: 'true' + retention-days: + description: Number of days to retain the artifact + required: false + default: '90' runs: using: composite steps: - name: Save artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ inputs.package_name }} path: ${{ runner.temp }}/build/${{ inputs.artifact_name }} + retention-days: ${{ inputs.retention-days }} - name: Configure AWS Credentials if: ${{ fromJSON(inputs.upload_aws) && github.event_name == 'push' && github.repository_owner == 'mavlink' && inputs.aws_key_id != '' && inputs.aws_secret_access_key != '' }} @@ -67,7 +69,3 @@ runs: --distribution-id "${{ inputs.aws_distribution_id }}" \ --paths "/latest/${{ inputs.artifact_name }}" shell: bash - env: - AWS_ACCESS_KEY_ID: ${{ inputs.aws_key_id }} - AWS_SECRET_ACCESS_KEY: ${{ inputs.aws_secret_access_key }} - AWS_REGION: us-west-2 diff --git a/.github/build-config.json b/.github/build-config.json new file mode 100644 index 000000000000..4aebb6212d31 --- /dev/null +++ b/.github/build-config.json @@ -0,0 +1,10 @@ +{ + "qt_version": "6.10.1", + "qt_modules": "qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml", + "gstreamer_version": "1.24.13", + "xcode_version": "16.x", + "ndk_version": "r27c", + "java_version": "17", + "android_platform": "35", + "ccache_version": "4.11.3" +} diff --git a/codecov.yml b/.github/codecov.yml similarity index 85% rename from codecov.yml rename to .github/codecov.yml index cef4e70c46ed..7890eb6edf53 100644 --- a/codecov.yml +++ b/.github/codecov.yml @@ -35,10 +35,17 @@ ignore: - "tools/**/*" - "deploy/**/*" - "custom/**/*" + - "custom-example/**/*" - "libs/**/*" + - "docs/**/*" + - "translations/**/*" + - "android/**/*" - "*.qml" - "*.js" - - "android/**/*" + - "**/moc_*.cpp" + - "**/qrc_*.cpp" + - "**/ui_*.h" + - "**/*_autogen/**" flags: debug: diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000000..a4b734c7b5b5 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,29 @@ +# ============================================================================= +# CodeQL Configuration +# Custom configuration for C++ security analysis +# ============================================================================= + +name: "QGroundControl CodeQL Config" + +# Paths to analyze +paths: + - src + - test + +# Paths to exclude from analysis +paths-ignore: + - '**/moc_*' + - '**/qrc_*' + - '**/ui_*' + - '**/*_autogen/**' + - '**/libs/**' + - '**/3rdparty/**' + - '**/build/**' + +# Query suites to use +# Options: security-extended, security-and-quality, or specific query packs +queries: + - uses: security-extended + +# Trap caching for faster subsequent runs +trap-caching: true diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e6419818cc34..b34cf104b8b4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,171 +1,38 @@ -# QGroundControl Development Guide +# QGroundControl - AI Assistant Guide Ground Control Station for MAVLink-enabled UAVs supporting PX4 and ArduPilot. -## Quick Start for AI Assistants +## Quick Start -**First-time here?** Start with these critical files: -1. **This file** - Architecture patterns and coding standards -2. `.github/CONTRIBUTING.md` - Contribution workflow -3. `src/FactSystem/Fact.h` - The most important pattern in QGC -4. `src/Vehicle/Vehicle.h` - Core vehicle model - -**Most Critical Pattern**: The **Fact System** handles ALL vehicle parameters. Never create custom parameter storage - always use Facts. - -**Golden Rule**: Multi-vehicle support means ALWAYS null-check `MultiVehicleManager::instance()->activeVehicle()`. +**Read these files first:** +1. **[CONTRIBUTING.md](CONTRIBUTING.md)** - Coding standards and architecture patterns +2. `src/FactSystem/Fact.h` - The most important pattern in QGC +3. `src/Vehicle/Vehicle.h` - Core vehicle model ## Tech Stack + - **C++20** with **Qt 6.10.1** (QtQml, QtQuick) - **Build**: CMake 3.25+, Ninja - **Protocol**: MAVLink 2.0 - **Platforms**: Windows, macOS, Linux, Android, iOS -## Critical Architecture Patterns - -### 1. Fact System (Parameter Management) -**Most important pattern in QGC** - All vehicle parameters use this. - -```cpp -// Access parameters (always null-check!) -Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME"); -if (param && param->validate(newValue, false).isEmpty()) { - param->setCookedValue(newValue); // Use cookedValue for UI (with units) - // param->rawValue() for MAVLink/storage -} - -// Expose to QML -Q_PROPERTY(Fact* myParam READ myParam CONSTANT) -``` - -**Key classes:** -- `Fact` - Single parameter with validation, units, metadata -- `FactGroup` - Hierarchical container (handles MAVLink via `handleMessage()`) -- `FactMetaData` - JSON-based metadata (min/max, enums, descriptions) - -**Rules:** -- Wait for `parametersReady` signal before accessing -- Use `cookedValue` (display) vs `rawValue` (storage) -- Metadata in `*.FactMetaData.json` files - -### 2. Plugin Architecture -Three types handle firmware customization: - -**FirmwarePlugin** - Firmware behavior (flight modes, capabilities) -```cpp -virtual QList flightModes() override; -virtual bool setFlightMode(const QString& mode) override; -``` - -**AutoPilotPlugin** - Vehicle setup UI (returns `VehicleComponent` list) - -**VehicleComponent** - Individual setup items (Radio, Sensors, Safety) -```cpp -virtual QString name() const override; -virtual bool setupComplete() const override; -virtual QUrl setupSource() const override; // QML UI -``` - -### 3. Manager Singletons -```cpp -// Always null-check activeVehicle (multi-vehicle support) -Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle(); -if (vehicle) { - vehicle->parameterManager()->getParameter(...); -} - -// Other managers -SettingsManager::instance()->appSettings()->... -LinkManager::instance()->... -``` - -### 4. QML/C++ Integration -```cpp -Q_OBJECT -QML_ELEMENT // Creatable in QML -QML_SINGLETON // Singleton -QML_UNCREATABLE("") // C++-only - -Q_PROPERTY(Type name READ getter WRITE setter NOTIFY signal) -Q_INVOKABLE void method(); -Q_ENUM(EnumType) -``` - -### 5. State Machines -For complex workflows (parameter loading, calibration): -```cpp -QGCStateMachine machine("Workflow", vehicle); -auto* sendCmd = new SendMavlinkCommandState("SendCmd", &machine); -auto* waitMsg = new WaitForMavlinkMessageState("WaitResponse", &machine); -sendCmd->addTransition(sendCmd, &SendMavlinkCommandState::done, waitMsg); -machine.start(); -``` - -### 6. Settings Framework -```cpp -class MySettings : public SettingsGroup { - DEFINE_SETTINGFACT(settingName) // Creates Fact with JSON metadata -}; -// Access: SettingsManager::instance()->mySettings()->settingName() -``` +## Build Commands -## Code Structure -``` -src/ -├── Vehicle/ # Vehicle state/comms (Vehicle.h is key) -├── FactSystem/ # Parameter management (READ THIS FIRST!) -├── FirmwarePlugin/ # PX4/ArduPilot abstraction -├── AutoPilotPlugins/ # Vehicle setup UI -├── MissionManager/ # Mission planning -├── MAVLink/ # Protocol handling -├── Comms/ # Serial/UDP/TCP/Bluetooth -├── Settings/ # Persistent settings -├── UI/ # QML UI (MainWindow.qml) -└── Utilities/ # StateMachine, helpers +```bash +git submodule update --init --recursive +~/Qt/6.10.1/gcc_64/bin/qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug +cmake --build build --config Debug +./build/Debug/QGroundControl --unittest # Run tests ``` -## Coding Standards - -**Naming:** -- Classes/Enums: `PascalCase` -- Methods/properties: `camelCase` -- Private members: `_leadingUnderscore` -- Files: `ClassName.h`, `ClassName.cc` +## Critical Rules -**Logging:** -```cpp -Q_DECLARE_LOGGING_CATEGORY(MyLog) -QGC_LOGGING_CATEGORY(MyLog, "qgc.component.name") -qCDebug(MyLog) << "Message:" << value; -``` - -**Defensive Coding (Critical!):** -```cpp -void method(Vehicle* vehicle) { - if (!vehicle) { - qCWarning(Log) << "Invalid vehicle"; - return; // Early return on invalid state - } - // Always use braces - if (condition) { - doSomething(); - } -} -``` +### The Two Golden Rules -**Code Style Tools:** -- Use `.clang-format`, `.clang-tidy`, `.editorconfig` configured in repo root -- Keep comments minimal - code should be self-documenting -- Do NOT create verbose file headers or unnecessary documentation files -- Do NOT add README files unless explicitly requested +1. **Fact System**: ALL vehicle parameters use the Fact System. Never create custom parameter storage. +2. **Multi-Vehicle**: ALWAYS null-check `MultiVehicleManager::instance()->activeVehicle()`. -**Security & Dependencies:** -- Never commit secrets, API keys, or credentials -- Validate all external inputs (MAVLink messages, file uploads, user input) -- Use Qt's built-in sanitization for SQL and string operations -- When adding dependencies, check for known vulnerabilities -- Prefer Qt's built-in functionality over external libraries - -## Common Pitfalls (DO NOT!) +### Common Pitfalls (DO NOT!) 1. ❌ Assume single vehicle - Always null-check `activeVehicle()` 2. ❌ Access Facts before `parametersReady` signal @@ -175,84 +42,13 @@ void method(Vehicle* vehicle) { 6. ❌ Ignore platform differences - Check `#ifdef Q_OS_*` 7. ❌ Mix cookedValue/rawValue without conversion 8. ❌ Create custom parameter storage - Use Fact System -9. ❌ Access FactGroups before `telemetryAvailable` -10. ❌ Forget to emit property signals (breaks QML bindings) - -## Build Commands -```bash -git submodule update --init --recursive -~/Qt/6.10.1/gcc_64/bin/qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -cmake --build build --config Debug -./build/Debug/QGroundControl --unittest # Run tests -``` - -**Key CMake Options** (cmake/CustomOptions.cmake): -- `QGC_STABLE_BUILD` - Release vs daily -- `QGC_BUILD_TESTING` - Unit tests -- `QGC_ENABLE_BLUETOOTH` - Bluetooth support -- `QGC_DISABLE_APM_PLUGIN` / `QGC_DISABLE_PX4_PLUGIN` - -## Testing - -### Running Tests -```bash -# Run all unit tests -./build/Debug/QGroundControl --unittest - -# Run specific test -./build/Debug/QGroundControl --unittest: - -# Run with verbose output -./build/Debug/QGroundControl --unittest --logging:full -``` - -### Test Structure -- Tests mirror `src/` structure in `test/` directory -- Use `UnitTest` base class from Qt Test framework -- Mock vehicle connections when testing vehicle-dependent code -- Always test with null vehicle checks +9. ❌ Forget to emit property signals (breaks QML bindings) -### Adding New Tests -```cpp -class MyComponentTest : public UnitTest { - Q_OBJECT -private slots: - void init(); // Called before each test - void cleanup(); // Called after each test - void testMyFunction(); -}; -``` - -## Troubleshooting - -### Build Issues -- **Qt not found**: Set `CMAKE_PREFIX_PATH` to Qt installation, or use qt-cmake -- **Submodule errors**: Run `git submodule update --init --recursive` -- **Missing dependencies**: Check platform-specific build instructions at https://dev.qgroundcontrol.com/ -- **CMake cache issues**: Delete `build/` directory and reconfigure - -### Runtime Issues -- **Crash on startup**: Check log files in `~/.local/share/QGroundControl/` (Linux/macOS) or `%LOCALAPPDATA%\QGroundControl` (Windows) -- **Vehicle not connecting**: Verify MAVLink protocol compatibility, check link configuration -- **Parameter load failures**: Ensure `parametersReady` signal before accessing Facts - -### Development Environment -- **Qt Creator recommended**: Import CMakeLists.txt as project -- **clangd for VSCode**: Uses `.clangd` config in repo root -- **Pre-commit hooks**: Run `pre-commit install` to enable automatic formatting - -## Performance Tips -- Batch updates: `fact->setSendValueChangedSignals(false)` -- Suppress live updates: `factGroup->setLiveUpdates(false)` -- Cache frequently accessed values -- MAVLink handled on separate thread -- Qt parent/child ownership for cleanup - -## Common Tasks +## Code Examples ### Working with Vehicle Parameters + ```cpp -// 1. Get parameter (always null-check!) Vehicle* vehicle = MultiVehicleManager::instance()->activeVehicle(); if (!vehicle) return; @@ -262,32 +58,26 @@ if (!param) { return; } -// 2. Validate before setting QString error = param->validate(newValue, false); if (!error.isEmpty()) { qCWarning(Log) << "Invalid value:" << error; return; } -// 3. Set value (cookedValue for UI with units) param->setCookedValue(newValue); - -// 4. Listen to changes -connect(param, &Fact::valueChanged, this, [](QVariant value) { - qCDebug(Log) << "Parameter changed:" << value; -}); ``` ### Creating a Settings Group + ```cpp -// 1. Define in MySettings.h +// MySettings.h class MySettings : public SettingsGroup { Q_OBJECT public: - DEFINE_SETTINGFACT(mySetting) // Creates Fact with JSON metadata + DEFINE_SETTINGFACT(mySetting) }; -// 2. Create MySettings.SettingsGroup.json with metadata +// MySettings.SettingsGroup.json { "mySetting": { "shortDescription": "My setting", @@ -298,39 +88,18 @@ public: } } -// 3. Access anywhere +// Access anywhere int value = SettingsManager::instance()->mySettings()->mySetting()->rawValue().toInt(); ``` -### Adding a Vehicle Component -```cpp -// 1. Create MyComponent.h (subclass VehicleComponent) -class MyComponent : public VehicleComponent { - Q_OBJECT -public: - MyComponent(Vehicle* vehicle, AutoPilotPlugin* autopilot, QObject* parent = nullptr); - - QString name() const override { return "My Component"; } - QString description() const override { return "Component description"; } - QString iconResource() const override { return "/qmlimages/MyComponentIcon.svg"; } - bool requiresSetup() const override { return true; } - bool setupComplete() const override { return _setupComplete; } - QUrl setupSource() const override { return QUrl::fromUserInput("qrc:/qml/MyComponentSetup.qml"); } -}; - -// 2. Register in AutoPilotPlugin::getVehicleComponents() -``` - ### Handling MAVLink Messages + ```cpp -// In a FactGroup or custom component void MyFactGroup::handleMessage(Vehicle* vehicle, mavlink_message_t& message) { switch (message.msgid) { case MAVLINK_MSG_ID_MY_MESSAGE: { mavlink_my_message_t msg; mavlink_msg_my_message_decode(&message, &msg); - - // Update Facts (thread-safe via Qt signals) myFact()->setRawValue(msg.value); break; } @@ -338,47 +107,61 @@ void MyFactGroup::handleMessage(Vehicle* vehicle, mavlink_message_t& message) { } ``` -### Adding a QML UI Component +### QML Component + ```qml -// 1. Create MyControl.qml import QtQuick import QGroundControl import QGroundControl.Controls QGCButton { text: "My Action" - property var vehicle: QGroundControl.multiVehicleManager.activeVehicle - enabled: vehicle && vehicle.armed onClicked: { if (vehicle) { - // Always null-check vehicle! vehicle.sendMavCommand(...) } } } ``` -## Essential Files to Read -1. `.github/CONTRIBUTING.md` - Contribution guidelines -2. `src/FactSystem/Fact.h` - Parameter system (CRITICAL!) -3. `src/Vehicle/Vehicle.h` - Vehicle model -4. `src/FirmwarePlugin/FirmwarePlugin.h` - Firmware abstraction +## Code Structure + +``` +src/ +├── Vehicle/ # Vehicle state/comms (Vehicle.h is key) +├── FactSystem/ # Parameter management (READ THIS FIRST!) +├── FirmwarePlugin/ # PX4/ArduPilot abstraction +├── AutoPilotPlugins/ # Vehicle setup UI +├── MissionManager/ # Mission planning +├── MAVLink/ # Protocol handling +├── Comms/ # Serial/UDP/TCP/Bluetooth +├── Settings/ # Persistent settings +├── UI/ # QML UI (MainWindow.qml) +└── Utilities/ # StateMachine, helpers +``` + +## Style Reference + +For complete coding standards, naming conventions, and architecture patterns, see **[CONTRIBUTING.md](CONTRIBUTING.md#coding-standards)**. + +**Quick reference:** +- Classes/Enums: `PascalCase` +- Methods/properties: `camelCase` +- Private members: `_leadingUnderscore` +- Files: `ClassName.h`, `ClassName.cc` +- Use `.clang-format`, `.clang-tidy` from repo root +- Minimal comments - code should be self-documenting ## Resources -- **User Manual**: https://docs.qgroundcontrol.com/ + - **Dev Guide**: https://dev.qgroundcontrol.com/ +- **User Manual**: https://docs.qgroundcontrol.com/ - **MAVLink**: https://mavlink.io/ - **Qt Docs**: https://doc.qt.io/qt-6/ -## Memory & Threading -- Qt parent/child ownership (auto-cleanup) -- Use `deleteLater()` for objects with active signals -- `Qt::QueuedConnection` for cross-thread signals -- MAVLink on LinkManager thread, UI on main thread - --- **Key Principle**: Match the style of code you're editing. Use defensive coding, validate inputs, handle errors gracefully, and respect the Fact System architecture. diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000000..5d5b19bd6179 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,72 @@ +# Labeler configuration - auto-label PRs based on changed files +# https://github.com/actions/labeler + +Docs: + - changed-files: + - any-glob-to-any-file: 'docs/**' + +github_actions: + - changed-files: + - any-glob-to-any-file: '.github/**' + +"Platform: Android": + - changed-files: + - any-glob-to-any-file: + - 'android/**' + - 'deploy/android/**' + +"Platform: iOS": + - changed-files: + - any-glob-to-any-file: + - 'deploy/ios/**' + +"Platform: Linux": + - changed-files: + - any-glob-to-any-file: + - 'deploy/linux/**' + - 'deploy/docker/**' + - 'tools/setup/*debian*' + - 'tools/setup/*linux*' + +"Platform: Windows": + - changed-files: + - any-glob-to-any-file: + - 'deploy/windows/**' + - 'tools/setup/*windows*' + +"Platform: OSX": + - changed-files: + - any-glob-to-any-file: + - 'deploy/macos/**' + - 'tools/setup/*macos*' + +Tests: + - changed-files: + - any-glob-to-any-file: 'test/**' + +CMake: + - changed-files: + - any-glob-to-any-file: + - 'CMakeLists.txt' + - 'cmake/**' + - '**/CMakeLists.txt' + +Video: + - changed-files: + - any-glob-to-any-file: + - 'src/VideoManager/**' + - 'src/VideoReceiver/**' + +Translations: + - changed-files: + - any-glob-to-any-file: 'translations/**' + +QML: + - changed-files: + - any-glob-to-any-file: 'src/**/*.qml' + +MAVLink: + - changed-files: + - any-glob-to-any-file: + - 'src/MAVLink/**' + - 'src/comm/**' diff --git a/.github/markdownlint.json b/.github/markdownlint.json new file mode 100644 index 000000000000..cb7ea2ed8fa8 --- /dev/null +++ b/.github/markdownlint.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/main/schema/markdownlint-config-schema.json", + "default": true, + "MD003": { "style": "atx" }, + "MD004": { "style": "dash" }, + "MD007": { "indent": 2 }, + "MD013": false, + "MD024": { "siblings_only": true }, + "MD033": false, + "MD041": false, + "MD046": { "style": "fenced" } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 83187b3d7819..6bbb501c14ab 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,23 +1,52 @@ - +## Description + -Description ------------ - -Test Steps ------------ - +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) +- [ ] CI/Build changes +- [ ] Other -Checklist: ----------- - -- [ ] [Review Contribution Guidelines](https://github.com/mavlink/qgroundcontrol/blob/master/.github/CONTRIBUTING.md). -- [ ] [Review Code of Conduct](https://github.com/mavlink/qgroundcontrol/blob/master/.github/CODE_OF_CONDUCT.md). -- [ ] I have tested my changes. +## Testing + +- [ ] Tested locally +- [ ] Added/updated unit tests +- [ ] Tested with simulator (SITL) +- [ ] Tested with hardware -Related Issue ------------ - +### Platforms Tested + +- [ ] Linux +- [ ] Windows +- [ ] macOS +- [ ] Android +- [ ] iOS +### Flight Stacks Tested + +- [ ] PX4 +- [ ] ArduPilot -By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. +## Screenshots + + + +## Checklist + +- [ ] I have read the [Contribution Guidelines](CONTRIBUTING.md) +- [ ] I have read the [Code of Conduct](CODE_OF_CONDUCT.md) +- [ ] My code follows the project's coding standards +- [ ] I have added tests that prove my fix/feature works +- [ ] New and existing unit tests pass locally + +## Related Issues + + + +--- +By submitting this pull request, I confirm that my contribution is made under the terms of the project's dual license (Apache 2.0 and GPL v3). diff --git a/.github/release.yml b/.github/release.yml index e7de10f55d71..d087963f045d 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,4 +1,10 @@ changelog: + exclude: + labels: + - dependencies + authors: + - dependabot + - dependabot[bot] categories: - title: Features labels: @@ -15,6 +21,12 @@ changelog: labels: - "RN: BUGFIX" - "RN: BUGFIX - CUSTOM BUILD" - - title: Targets + - title: Hardware Support labels: - "RN: NEW BOARD SUPPORT" + - title: Documentation + labels: + - documentation + - title: Other Changes + labels: + - "*" diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 000000000000..6063f9e66f8c --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + ":dependencyDashboard", + ":semanticCommitTypeAll(chore)" + ], + "schedule": ["before 6am on monday"], + "timezone": "UTC", + "labels": ["dependencies"], + "prConcurrentLimit": 5, + "prHourlyLimit": 2, + + "packageRules": [ + { + "description": "Group all GitHub Actions updates", + "matchManagers": ["github-actions"], + "groupName": "GitHub Actions", + "schedule": ["before 6am on monday"] + }, + { + "description": "Auto-merge patch updates for GitHub Actions", + "matchManagers": ["github-actions"], + "matchUpdateTypes": ["patch"], + "automerge": true, + "automergeType": "pr", + "platformAutomerge": true + }, + { + "description": "Group all git submodule updates", + "matchManagers": ["git-submodules"], + "groupName": "Git Submodules", + "schedule": ["before 6am on the first day of the month"] + }, + { + "description": "Group Node.js updates (docs)", + "matchManagers": ["npm"], + "groupName": "Node.js Dependencies", + "schedule": ["before 6am on monday"] + }, + { + "description": "Group Python updates (tools)", + "matchManagers": ["pip_requirements", "pip_setup"], + "groupName": "Python Dependencies", + "schedule": ["before 6am on monday"] + } + ], + + "git-submodules": { + "enabled": true + }, + + "vulnerabilityAlerts": { + "labels": ["security", "priority"], + "schedule": ["at any time"] + } +} diff --git a/.github/vale.ini b/.github/vale.ini new file mode 100644 index 000000000000..56899f9a9853 --- /dev/null +++ b/.github/vale.ini @@ -0,0 +1,23 @@ +# Vale Prose Linting Configuration +# https://vale.sh/docs/ + +StylesPath = vale/styles + +MinAlertLevel = suggestion + +Packages = Google, write-good + +[*.md] +BasedOnStyles = Vale, Google, write-good + +# Disable overly strict rules +Google.Acronyms = NO +Google.FirstPerson = NO +Google.We = NO +Google.Will = NO +write-good.Passive = suggestion +write-good.Weasel = suggestion +write-good.TooWordy = suggestion + +[*.txt] +BasedOnStyles = Vale diff --git a/.github/workflows/android-linux.yml b/.github/workflows/android-linux.yml deleted file mode 100644 index 898a85cff51e..000000000000 --- a/.github/workflows/android-linux.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: Android-Linux - -on: - push: - branches: - - master - - 'Stable*' - tags: - - 'v*' - paths-ignore: - - 'docs/**' - pull_request: - paths: - - '.github/workflows/android-linux.yml' - - 'deploy/android/**' - - 'src/**' - - 'android/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'translations/*' - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -jobs: - build: - name: Build Android-Linux ${{ matrix.qt_version }} ${{ matrix.build_type }} - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - build_type: [Release] - qt_version: [6.10.1] - - defaults: - run: - shell: bash - - env: - PACKAGE: QGroundControl - QT_VERSION: ${{ matrix.qt_version }} - QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/deploy/android/android_release.keystore - QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore - QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - QT_ANDROID_ABIS: ${{ matrix.build_type == 'Release' && 'arm64-v8a;armeabi-v7a' || 'arm64-v8a' }} - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - submodules: recursive - fetch-depth: 1 - fetch-tags: true - - - name: Initial Setup - uses: ./.github/actions/common - - - name: Free Disk Space - if: runner.os == 'Linux' - uses: jlumbroso/free-disk-space@main - with: - tool-cache: false - android: false - large-packages: false - - - name: Install Qt for Android - uses: ./.github/actions/qt-android - with: - host: linux - arch: linux_gcc_64 - version: ${{ matrix.qt_version }} - abis: ${{ env.QT_ANDROID_ABIS }} - cpm-modules: ${{ runner.temp }}/build/cpm_modules - - - name: Configure - working-directory: ${{ runner.temp }}/build - run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - -DCMAKE_WARN_DEPRECATED=FALSE - -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}" - -DQT_ANDROID_BUILD_ALL_ABIS=OFF - -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../gcc_64" - -DQT_ANDROID_SIGN_APK=${{ env.QT_ANDROID_KEYSTORE_STORE_PASS != '' && 'ON' || 'OFF' }} - -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} - - - name: Build - working-directory: ${{ runner.temp }}/build - run: cmake --build . --target all --config ${{ matrix.build_type }} --parallel - - - run: cp ${{ runner.temp }}/build/android-build/*.apk ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk - - - name: Upload Build File - if: matrix.build_type == 'Release' - uses: ./.github/actions/upload - with: - artifact_name: ${{ env.PACKAGE }}.apk - package_name: ${{ env.PACKAGE }} - aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} - github_token: ${{ secrets.GITHUB_TOKEN }} - upload_aws: true - - # - name: Deploy to Play Store - # if: matrix.build_type == 'Release' - # uses: ./.github/actions/playstore - # with: - # artifact_name: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk - # service_account_json: ${{ secrets.SERVICE_ACCOUNT }} diff --git a/.github/workflows/android-macos.yml b/.github/workflows/android-macos.yml deleted file mode 100644 index 89c208608973..000000000000 --- a/.github/workflows/android-macos.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Android-MacOS - -on: - push: - branches: - - master - - 'Stable*' - tags: - - 'v*' - paths-ignore: - - 'docs/**' # Do not trigger for any changes under docs - pull_request: - paths: - - '.github/workflows/android-macos.yml' - - 'deploy/android/**' - - 'src/**' - - 'android/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'translations/*' - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -jobs: - build: - runs-on: macos-latest - - strategy: - matrix: - BuildType: [Release] - - defaults: - run: - shell: bash - - env: - ARTIFACT: QGroundControl.apk - QT_VERSION: 6.10.1 - QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/deploy/android/android_release.keystore - QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore - QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - QT_ANDROID_ABIS: 'arm64-v8a' - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - submodules: recursive - fetch-depth: 1 - fetch-tags: true - - - name: Initial Setup - uses: ./.github/actions/common - - - name: Install Qt for Android - uses: ./.github/actions/qt-android - with: - host: mac - arch: clang_64 - version: ${{ env.QT_VERSION }} - abis: ${{ env.QT_ANDROID_ABIS }} - cpm-modules: ${{ runner.temp }}/build/cpm_modules - - - name: Configure - working-directory: ${{ runner.temp }}/build - run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja - -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }} - -DCMAKE_WARN_DEPRECATED=FALSE - -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}" - -DQT_ANDROID_BUILD_ALL_ABIS=OFF - -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../macos" - -DQT_ANDROID_SIGN_APK=${{ env.QT_ANDROID_KEYSTORE_STORE_PASS != '' && 'ON' || 'OFF' }} - -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} - - - name: Build - working-directory: ${{ runner.temp }}/build - run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel - - - name: Save APK - uses: actions/upload-artifact@v5 - with: - name: ${{ env.ARTIFACT }} - path: ${{ runner.temp }}/build/android-build/*.apk diff --git a/.github/workflows/android-windows.yml b/.github/workflows/android-windows.yml deleted file mode 100644 index aeacbc26b35d..000000000000 --- a/.github/workflows/android-windows.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Android-Windows - -on: - push: - branches: - - master - - 'Stable*' - tags: - - 'v*' - paths-ignore: - - 'docs/**' # Do not trigger for any changes under docs - pull_request: - paths: - - '.github/workflows/android-windows.yml' - - 'deploy/android/**' - - 'src/**' - - 'android/**' - - 'CMakeLists.txt' - - 'cmake/**' - - 'translations/*' - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -jobs: - build: - runs-on: windows-latest - - strategy: - matrix: - BuildType: [Release] - - defaults: - run: - shell: cmd - - env: - ARTIFACT: QGroundControl.apk - QT_VERSION: 6.10.1 - QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}\deploy\android\android_release.keystore - QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore - QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - QT_ANDROID_ABIS: 'arm64-v8a' - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - submodules: recursive - fetch-depth: 1 - fetch-tags: true - - - name: Initial Setup - uses: ./.github/actions/common - - - name: Install Qt for Android - uses: ./.github/actions/qt-android - with: - host: windows - arch: win64_msvc2022_64 - version: ${{ env.QT_VERSION }} - abis: ${{ env.QT_ANDROID_ABIS }} - cpm-modules: ${{ runner.temp }}\build\cpm_modules - - - name: Configure - working-directory: ${{ runner.temp }}/build - run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja - -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }} - -DCMAKE_WARN_DEPRECATED=FALSE - -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}" - -DQT_ANDROID_BUILD_ALL_ABIS=OFF - -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../msvc2022_64" - -DQT_ANDROID_SIGN_APK=OFF - -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} - - - name: Build - working-directory: ${{ runner.temp }}/build - run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel - - - name: Save APK - uses: actions/upload-artifact@v5 - with: - name: ${{ env.ARTIFACT }} - path: ${{ runner.temp }}/build/android-build/*.apk diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 000000000000..ad551551a373 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,164 @@ +name: Android + +on: + push: + branches: + - master + - 'Stable*' + tags: + - 'v*' + paths-ignore: + - 'docs/**' + pull_request: + paths: + - '.github/workflows/android.yml' + - '.github/actions/**' + - 'deploy/android/**' + - 'src/**' + - 'test/**' + - 'android/**' + - 'CMakeLists.txt' + - 'cmake/**' + - 'translations/*' + workflow_dispatch: + inputs: + build_type: + description: 'Build type' + required: false + default: 'Release' + type: choice + options: + - Release + - Debug + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +permissions: + contents: read + +jobs: + build: + name: Android (${{ matrix.host }}) + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + + strategy: + fail-fast: false + matrix: + include: + - host: linux + runner: ubuntu-latest + arch: linux_gcc_64 + qt_host_path: gcc_64 + shell: bash + primary: true + - host: mac + runner: macos-latest + arch: clang_64 + qt_host_path: macos + shell: bash + primary: false + - host: windows + runner: windows-latest + arch: win64_msvc2022_64 + qt_host_path: msvc2022_64 + shell: cmd + primary: false + + defaults: + run: + shell: ${{ matrix.shell }} + + env: + PACKAGE: QGroundControl + QT_ANDROID_KEYSTORE_PATH: ${{ github.workspace }}/deploy/android/android_release.keystore + QT_ANDROID_KEYSTORE_ALIAS: QGCAndroidKeyStore + QT_ANDROID_KEYSTORE_STORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + QT_ANDROID_KEYSTORE_KEY_PASS: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + # Primary build (Linux) includes both ABIs, others just arm64 + QT_ANDROID_ABIS: ${{ matrix.primary && 'arm64-v8a;armeabi-v7a' || 'arm64-v8a' }} + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 1 + fetch-tags: true + + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + + - name: Initial Setup + uses: ./.github/actions/common + + - name: Free Disk Space + if: matrix.host == 'linux' + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: false + large-packages: false + + - name: Install Qt for Android + uses: ./.github/actions/qt-android + with: + host: ${{ matrix.host }} + arch: ${{ matrix.arch }} + version: ${{ steps.build-config.outputs.qt_version }} + modules: ${{ steps.build-config.outputs.qt_modules }} + abis: ${{ env.QT_ANDROID_ABIS }} + build-type: ${{ inputs.build_type || 'Release' }} + cpm-modules: ${{ runner.temp }}/build/cpm_modules + ndk-version: ${{ steps.build-config.outputs.ndk_version }} + java-version: ${{ steps.build-config.outputs.java_version }} + android-platform: ${{ steps.build-config.outputs.android_platform }} + ccache-version: ${{ steps.build-config.outputs.ccache_version }} + save-cache: ${{ github.event_name != 'pull_request' }} + + - name: Configure + working-directory: ${{ runner.temp }}/build + run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja + -DCMAKE_BUILD_TYPE=${{ inputs.build_type || 'Release' }} + -DCMAKE_WARN_DEPRECATED=FALSE + -DQT_ANDROID_ABIS="${{ env.QT_ANDROID_ABIS }}" + -DQT_ANDROID_BUILD_ALL_ABIS=OFF + -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../${{ matrix.qt_host_path }}" + -DQT_ANDROID_SIGN_APK=${{ matrix.primary && env.QT_ANDROID_KEYSTORE_STORE_PASS != '' && 'ON' || 'OFF' }} + -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} + -DQGC_CUSTOM_GST_PACKAGE=ON + + - name: Build + working-directory: ${{ runner.temp }}/build + run: cmake --build . --target all --config ${{ inputs.build_type || 'Release' }} --parallel + + - name: Prepare Artifact + run: cp ${{ runner.temp }}/build/android-build/*.apk ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk + + - name: Upload Build File + if: matrix.primary + uses: ./.github/actions/upload + with: + artifact_name: ${{ env.PACKAGE }}.apk + package_name: ${{ env.PACKAGE }} + aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} + upload_aws: true + + - name: Save APK + if: ${{ !matrix.primary }} + uses: actions/upload-artifact@v5 + with: + name: ${{ env.PACKAGE }}-${{ matrix.host }}.apk + path: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk + + # - name: Deploy to Play Store + # if: matrix.primary + # uses: ./.github/actions/playstore + # with: + # artifact_name: ${{ runner.temp }}/build/${{ env.PACKAGE }}.apk + # service_account_json: ${{ secrets.SERVICE_ACCOUNT }} diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 000000000000..2bc19c0d767d --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,30 @@ +name: Auto-merge Dependabot + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + name: Auto-merge Dependabot PRs + runs-on: ubuntu-latest + timeout-minutes: 5 + if: github.actor == 'dependabot[bot]' + + steps: + - name: Fetch Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable auto-merge for patch/minor updates + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' + run: gh pr merge --auto --squash "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-summary.yml b/.github/workflows/build-summary.yml new file mode 100644 index 000000000000..d19af7d6cdae --- /dev/null +++ b/.github/workflows/build-summary.yml @@ -0,0 +1,152 @@ +name: Build Summary + +on: + workflow_run: + workflows: [Linux, Windows, MacOS, Android] + types: [completed] + +permissions: + pull-requests: write + actions: read + +jobs: + summary: + name: Post Build Summary + runs-on: ubuntu-latest + if: github.event.workflow_run.event == 'pull_request' + timeout-minutes: 10 + + steps: + - name: Get PR number + id: pr + uses: actions/github-script@v7 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id + }); + + // Try to get PR number from the workflow run + const run = context.payload.workflow_run; + if (run.pull_requests && run.pull_requests.length > 0) { + return run.pull_requests[0].number; + } + + // Fallback: search for PR by head SHA + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${run.head_branch}` + }); + + if (prs.data.length > 0) { + return prs.data[0].number; + } + + return null; + + - name: Collect build results + if: steps.pr.outputs.result != 'null' + id: results + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.pr.outputs.result }}; + const headSha = context.payload.workflow_run.head_sha; + + // Get all workflow runs for this commit + const runs = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: headSha, + per_page: 100 + }); + + const platforms = { + 'Linux': { status: '⏳', conclusion: 'pending' }, + 'Windows': { status: '⏳', conclusion: 'pending' }, + 'MacOS': { status: '⏳', conclusion: 'pending' }, + 'Android': { status: '⏳', conclusion: 'pending' } + }; + + for (const run of runs.data.workflow_runs) { + if (platforms[run.name]) { + if (run.status === 'completed') { + platforms[run.name].status = run.conclusion === 'success' ? '✅' : '❌'; + platforms[run.name].conclusion = run.conclusion; + platforms[run.name].url = run.html_url; + } else if (run.status === 'in_progress') { + platforms[run.name].status = '🔄'; + platforms[run.name].conclusion = 'in_progress'; + platforms[run.name].url = run.html_url; + } + } + } + + return platforms; + + - name: Post or update comment + if: steps.pr.outputs.result != 'null' + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.pr.outputs.result }}; + const platforms = ${{ steps.results.outputs.result }}; + const marker = ''; + + // Build the comment body + let body = `${marker}\n## 🏗️ Build Status\n\n`; + body += `| Platform | Status | Details |\n`; + body += `|----------|--------|--------|\n`; + + for (const [name, info] of Object.entries(platforms)) { + const link = info.url ? `[View](${info.url})` : '-'; + body += `| ${name} | ${info.status} | ${link} |\n`; + } + + const allComplete = Object.values(platforms).every(p => + p.conclusion === 'success' || p.conclusion === 'failure' || p.conclusion === 'cancelled' + ); + const allSuccess = Object.values(platforms).every(p => p.conclusion === 'success'); + + if (allComplete) { + if (allSuccess) { + body += `\n✅ **All builds passed!**`; + } else { + body += `\n❌ **Some builds failed.** Please check the logs.`; + } + } else { + body += `\n⏳ *Some builds are still in progress...*`; + } + + body += `\n\nLast updated: ${new Date().toISOString()}`; + + // Find existing comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const existingComment = comments.data.find(c => c.body.includes(marker)); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: body + }); + console.log(`Updated comment ${existingComment.id}`); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); + console.log(`Created new comment on PR #${prNumber}`); + } diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 876007676c2c..b9a7abbb98cd 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -5,9 +5,20 @@ on: - closed workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false + jobs: cleanup: + name: Cleanup Branch Caches runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + shell: bash + permissions: # `actions:write` permission is required to delete caches # See also: https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id diff --git a/.github/workflows/check-links.yml b/.github/workflows/check-links.yml new file mode 100644 index 000000000000..1c697666a746 --- /dev/null +++ b/.github/workflows/check-links.yml @@ -0,0 +1,55 @@ +name: Check Links + +on: + push: + branches: + - master + paths: + - 'docs/**' + - '**.md' + pull_request: + paths: + - 'docs/**' + - '**.md' + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday + +permissions: + contents: read + +jobs: + check-links: + name: Check Links + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Check links + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --verbose + --no-progress + --exclude-mail + --exclude '^https://github.com/mavlink/qgroundcontrol/(pull|issues|compare)/' + --exclude 'localhost' + --exclude '127\.0\.0\.1' + --exclude '192\.168\.' + --exclude '10\.\d+\.' + --accept 200,204,301,302,403,429 + '**/*.md' + 'docs/**' + # Fail on broken links for pushes to master, advisory for PRs + fail: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + + - name: Upload report + uses: actions/upload-artifact@v5 + if: always() + with: + name: link-check-report + path: ./lychee/out.md + retention-days: 7 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000000..e12fbcf9cfa4 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,106 @@ +# ============================================================================= +# CodeQL Security Analysis +# C++ security scanning for QGroundControl +# +# NOTE: This workflow is disabled by default because the upstream repo has +# "default setup" CodeQL enabled, which conflicts with custom workflows. +# The default setup currently only scans JS/Python/Actions, NOT C++. +# +# To enable C++ security scanning, either: +# 1. Ask maintainers to add C++ to the default setup in repo settings +# 2. Or disable default setup and uncomment the triggers below +# ============================================================================= + +name: "CodeQL" + +on: + # Disabled: conflicts with default setup. Uncomment when default setup is disabled. + # push: + # branches: [master, 'Stable*'] + # paths: ['src/**', 'test/**'] + # pull_request: + # branches: [master] + # paths: ['src/**', 'test/**'] + # schedule: + # - cron: '0 6 * * 1' # Weekly on Monday at 6 AM UTC + + # Manual trigger only (doesn't conflict with default setup) + workflow_dispatch: + inputs: + query_suite: + description: 'Query suite to use' + required: false + default: 'security-extended' + type: choice + options: + - default + - security-extended + - security-and-quality + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + security-events: write + actions: read + +jobs: + analyze: + name: Analyze C++ + runs-on: ubuntu-latest + timeout-minutes: 120 + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 1 + + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: c-cpp + queries: ${{ inputs.query_suite || 'security-extended' }} + config-file: ./.github/codeql/codeql-config.yml + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ steps.build-config.outputs.qt_version }} + host: linux + target: desktop + arch: linux_gcc_64 + dir: ${{ runner.temp }} + modules: ${{ steps.build-config.outputs.qt_modules }} + setup-python: false + cache: true + + - name: Configure + working-directory: ${{ runner.temp }}/build + run: | + ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja \ + -DCMAKE_BUILD_TYPE=Debug \ + -DQGC_BUILD_TESTING=OFF + + - name: Build + working-directory: ${{ runner.temp }}/build + run: cmake --build . --target all --config Debug --parallel + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:c-cpp" diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 000000000000..acda19dca135 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,129 @@ +name: Code Coverage + +on: + push: + branches: + - master + paths: + - 'src/**' + - 'test/**' + - 'CMakeLists.txt' + - 'cmake/**' + pull_request: + paths: + - 'src/**' + - 'test/**' + schedule: + - cron: '0 7 * * 1' # Weekly on Monday at 7 AM UTC + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + coverage: + name: Generate Coverage + runs-on: ubuntu-22.04 + timeout-minutes: 120 + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + fetch-depth: 1 + + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + + - name: Initial Setup + uses: ./.github/actions/common + + - name: Install Dependencies + uses: ./.github/actions/install-dependencies + + - name: Install Coverage Tools + run: | + sudo apt-get update + sudo apt-get install -y lcov + pip install gcovr + + - name: Setup Caching + uses: ./.github/actions/cache + with: + host: linux + target: linux_gcc_64 + build-type: Coverage + cpm-modules: ${{ runner.temp }}/build/cpm_modules + ccache-version: ${{ steps.build-config.outputs.ccache_version }} + save-cache: ${{ github.event_name != 'pull_request' }} + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: ${{ steps.build-config.outputs.qt_version }} + host: linux + target: desktop + arch: linux_gcc_64 + dir: ${{ runner.temp }} + modules: ${{ steps.build-config.outputs.qt_modules }} + setup-python: false + cache: true + + - name: Configure with Coverage + working-directory: ${{ runner.temp }}/build + run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja + -DCMAKE_BUILD_TYPE=Debug + -DQGC_ENABLE_COVERAGE=ON + -DQGC_BUILD_TESTING=ON + + - name: Build + working-directory: ${{ runner.temp }}/build + run: cmake --build . --target all --config Debug --parallel + + - name: Run Tests + working-directory: ${{ runner.temp }}/build + run: xvfb-run -a ctest --output-on-failure --timeout 300 + continue-on-error: true + + - name: Generate Coverage Report + working-directory: ${{ runner.temp }}/build + run: | + gcovr --root ${{ github.workspace }} \ + --filter '${{ github.workspace }}/src/' \ + --exclude '.*moc_.*' \ + --exclude '.*qrc_.*' \ + --exclude '.*ui_.*' \ + --exclude '.*_autogen.*' \ + --xml coverage.xml \ + --html coverage.html \ + --html-details \ + --print-summary + + - name: Upload to Codecov + uses: codecov/codecov-action@v5 + with: + files: ${{ runner.temp }}/build/coverage.xml + flags: unittests + name: qgc-coverage + fail_ci_if_error: false + verbose: true + + - name: Upload Coverage Artifact + uses: actions/upload-artifact@v5 + with: + name: coverage-report + path: | + ${{ runner.temp }}/build/coverage.xml + ${{ runner.temp }}/build/coverage.html + ${{ runner.temp }}/build/coverage.*.html + retention-days: 14 diff --git a/.github/workflows/crowdin_docs_download.yml b/.github/workflows/crowdin_docs_download.yml index da8aa03d8ae1..ca3896827717 100644 --- a/.github/workflows/crowdin_docs_download.yml +++ b/.github/workflows/crowdin_docs_download.yml @@ -7,13 +7,23 @@ on: - cron: '0 0 * * 0' # Runs every Sunday at 00:00 UTC workflow_dispatch: +concurrency: + group: crowdin-download + cancel-in-progress: false + permissions: contents: write pull-requests: write jobs: synchronize-with-crowdin: + name: Download from Crowdin runs-on: ubuntu-latest + timeout-minutes: 30 + + defaults: + run: + shell: bash steps: - name: Checkout @@ -31,6 +41,7 @@ jobs: create_pull_request: true pull_request_title: 'New QGC guide translations (Crowdin)' pull_request_body: 'New QGC guide Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)' + pull_request_labels: 'translations, automated' pull_request_base_branch_name: 'master' env: # A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository). diff --git a/.github/workflows/crowdin_docs_upload.yml b/.github/workflows/crowdin_docs_upload.yml index 43a1d0ef7fac..e0df08fae22b 100644 --- a/.github/workflows/crowdin_docs_upload.yml +++ b/.github/workflows/crowdin_docs_upload.yml @@ -8,19 +8,24 @@ on: - master paths: - 'docs/en/**' - pull_request: - types: - - closed - branches: - - master - paths: - - 'docs/en/**' workflow_dispatch: +concurrency: + group: crowdin-upload + cancel-in-progress: false + +permissions: + contents: read + jobs: upload-to-crowdin: - if: github.event.pull_request.merged == true || github.event_name == 'push' || github.event_name == 'workflow_dispatch' + name: Upload to Crowdin runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + shell: bash steps: - name: Checkout diff --git a/.github/workflows/custom.yml b/.github/workflows/custom.yml deleted file mode 100644 index e490b07bab77..000000000000 --- a/.github/workflows/custom.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: Custom build - -on: - push: - branches: - - master - - Stable* - tags: - - v* - paths-ignore: - - docs/** - pull_request: - paths: - - .github/workflows/windows.yml - - deploy/windows/** - - src/** - - custom-example/** - - CMakeLists.txt - - cmake/** - - tools/setup/*windows* - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -jobs: - build: - runs-on: windows-latest - - defaults: - run: - shell: cmd - - env: - QT_VERSION: 6.10.1 - GST_VERSION: 1.22.12 - - steps: - - name: Checkout repo - uses: actions/checkout@v6 - with: - submodules: recursive - fetch-depth: 1 - fetch-tags: true - - - name: Initial Setup - uses: ./.github/actions/common - - - name: Enable custom build - run: | - if not exist ".\custom-example" ( - echo Directory ".\custom-example" does not exist. && exit /b 1 - ) - xcopy /E /I ".\custom-example" ".\custom" - - - name: Install GStreamer - uses: blinemedical/setup-gstreamer@v1 - with: - version: ${{ env.GST_VERSION }} - - - name: Setup Caching - uses: ./.github/actions/cache - with: - host: windows - target: win64_msvc2022_64 - build-type: Release - cpm-modules: ${{ runner.temp }}\build\cpm_modules - - - name: Install Qt - uses: jurplel/install-qt-action@v4 - with: - version: ${{ env.QT_VERSION }} - host: windows - target: desktop - arch: win64_msvc2022_64 - dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml - setup-python: false - cache: true - - - name: Set up MSVC environment - uses: ilammy/msvc-dev-cmd@v1 - - - name: Configure - working-directory: ${{ runner.temp }}\build - run: | - ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja ^ - -DCMAKE_BUILD_TYPE=Release ^ - -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} - - - name: Build - working-directory: ${{ runner.temp }}\build - run: cmake --build . --target all --config Release --parallel - - - name: Verify executable - working-directory: ${{ runner.temp }}\build\Release - run: Custom-QGroundControl.exe --simple-boot-test diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 000000000000..935f23114c3b --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,29 @@ +name: Dependency Review + +on: + pull_request: + paths: + - '**/CMakeLists.txt' + - '**/*.cmake' + - '.gitmodules' + - 'package*.json' + +permissions: + contents: read + pull-requests: write + +jobs: + dependency-review: + name: Review Dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + comment-summary-in-pr: always diff --git a/.github/workflows/docker-linux.yml b/.github/workflows/docker-linux.yml index 7db2a1d64b43..2c747691ce9f 100644 --- a/.github/workflows/docker-linux.yml +++ b/.github/workflows/docker-linux.yml @@ -12,19 +12,26 @@ on: pull_request: paths: - '.github/workflows/docker-linux.yml' + - '.github/actions/**' - 'deploy/docker/**' - 'deploy/linux/**' - 'src/**' - 'CMakeLists.txt' - 'cmake/**' + workflow_dispatch: -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +permissions: + contents: read jobs: build: + name: Docker Linux runs-on: ubuntu-latest + timeout-minutes: 120 defaults: run: @@ -38,11 +45,5 @@ jobs: fetch-depth: 1 fetch-tags: true - - name: Get all tags for correct version determination - working-directory: ${{ github.workspace }} - run: git fetch --all --tags --force --depth 1 - - - run: chmod a+x ./deploy/docker/run-docker-ubuntu.sh - - - name: Run Docker Build - run: ./deploy/docker/run-docker-ubuntu.sh + - name: Build with Docker + uses: ./.github/actions/docker diff --git a/.github/workflows/docs_deploy.yml b/.github/workflows/docs_deploy.yml index 94ee4aad16d8..265e4fa6db36 100644 --- a/.github/workflows/docs_deploy.yml +++ b/.github/workflows/docs_deploy.yml @@ -14,12 +14,26 @@ on: - 'package*.json' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + env: BRANCH_NAME: ${{ github.head_ref || github.ref_name }} jobs: build: + name: Build Docs runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + shell: bash + steps: - name: Checkout uses: actions/checkout@v6 @@ -46,13 +60,19 @@ jobs: retention-days: 1 deploy: - if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged) }} + name: Deploy Docs + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' needs: build runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + shell: bash steps: - name: Download Artifact - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v5 with: name: qgc_docs_build path: ~/_book diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 65c847a4db2e..8a97d2bec9b7 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -1,11 +1,29 @@ name: flatpak -on: [workflow_dispatch] + +on: + workflow_dispatch: + inputs: + gnome_version: + description: 'GNOME runtime version' + required: false + default: '47' + type: string + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + jobs: flatpak-builder: name: Flatpak runs-on: ubuntu-latest + timeout-minutes: 120 container: - image: bilelmoussaoui/flatpak-github-actions:gnome-47 + # Update gnome version periodically: https://hub.docker.com/r/bilelmoussaoui/flatpak-github-actions/tags + image: bilelmoussaoui/flatpak-github-actions:gnome-${{ inputs.gnome_version || '47' }} options: --privileged steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 9abdc12ddb81..ee2b36752fb8 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -3,26 +3,29 @@ name: iOS on: workflow_dispatch: -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +permissions: + contents: read jobs: build: + name: iOS runs-on: macos-latest - - strategy: - matrix: - BuildType: [Release] + timeout-minutes: 120 defaults: run: shell: bash + strategy: + matrix: + build_type: [Release] + env: - ARTIFACT: QGroundControl.app PACKAGE: QGroundControl - QT_VERSION: 6.10.1 steps: - name: Checkout repo @@ -32,55 +35,61 @@ jobs: fetch-depth: 1 fetch-tags: true + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + - name: Initial Setup uses: ./.github/actions/common - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: latest-stable + xcode-version: ${{ steps.build-config.outputs.xcode_version }} - name: Setup Caching uses: ./.github/actions/cache with: host: mac target: ios - build-type: ${{ matrix.BuildType }} + build-type: ${{ matrix.build_type }} cpm-modules: ${{ runner.temp }}/build/cpm_modules + ccache-version: ${{ steps.build-config.outputs.ccache_version }} + save-cache: ${{ github.event_name != 'pull_request' }} - name: Install Qt for MacOS uses: jurplel/install-qt-action@v4 with: - version: ${{ env.QT_VERSION }} + version: ${{ steps.build-config.outputs.qt_version }} host: mac target: desktop arch: clang_64 dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ steps.build-config.outputs.qt_modules }} setup-python: false cache: true - name: Install Qt for iOS uses: jurplel/install-qt-action@v4 with: - version: ${{ env.QT_VERSION }} + version: ${{ steps.build-config.outputs.qt_version }} host: mac target: ios arch: ios dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors + modules: ${{ steps.build-config.outputs.qt_modules_ios }} cache: true - name: Configure working-directory: ${{ runner.temp }}/build run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja - -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DQT_HOST_PATH="${{ env.QT_ROOT_DIR }}/../macos" -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} - name: Build working-directory: ${{ runner.temp }}/build - run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel + run: cmake --build . --target all --config ${{ matrix.build_type }} --parallel - name: Save App uses: actions/upload-artifact@v5 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000000..c94f3909bb00 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,21 @@ +name: Labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +jobs: + label: + name: Label PR + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/labeler@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + sync-labels: true diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 2efb5e8513d1..62cc522e90a1 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -8,7 +8,7 @@ on: tags: - 'v*' paths-ignore: - - 'docs/**' # Do not trigger for any changes under docs + - 'docs/**' pull_request: paths: - '.github/workflows/linux.yml' @@ -19,15 +19,29 @@ on: - 'CMakeLists.txt' - 'cmake/**' - 'tools/setup/*debian*' - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + workflow_dispatch: + inputs: + build_type: + description: 'Build type' + required: false + default: 'Release' + type: choice + options: + - Release + - Debug + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +permissions: + contents: read jobs: build: name: Build ${{ matrix.arch }} ${{ matrix.build_type }} runs-on: ${{ matrix.os }} + timeout-minutes: 120 strategy: fail-fast: false @@ -53,9 +67,6 @@ jobs: run: shell: bash - env: - QT_VERSION: 6.10.1 - steps: - name: Checkout repo uses: actions/checkout@v6 @@ -64,13 +75,15 @@ jobs: fetch-depth: 1 fetch-tags: true + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + - name: Initial Setup uses: ./.github/actions/common - name: Install Dependencies - run: | - chmod a+x ./tools/setup/install-dependencies-debian.sh - sudo ./tools/setup/install-dependencies-debian.sh + uses: ./.github/actions/install-dependencies - name: Setup Caching uses: ./.github/actions/cache @@ -79,16 +92,18 @@ jobs: target: ${{ matrix.arch }} build-type: ${{ matrix.build_type }} cpm-modules: ${{ runner.temp }}/build/cpm_modules + ccache-version: ${{ steps.build-config.outputs.ccache_version }} + save-cache: ${{ github.event_name != 'pull_request' }} - name: Install Qt for Linux uses: jurplel/install-qt-action@v4 with: - version: ${{ env.QT_VERSION }} + version: ${{ steps.build-config.outputs.qt_version }} host: ${{ matrix.host }} target: desktop arch: ${{ matrix.arch }} dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ steps.build-config.outputs.qt_modules }} setup-python: false cache: true @@ -134,4 +149,3 @@ jobs: aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lupdate.yaml b/.github/workflows/lupdate.yml similarity index 80% rename from .github/workflows/lupdate.yaml rename to .github/workflows/lupdate.yml index 5418aa2420b2..b5469f7be21b 100644 --- a/.github/workflows/lupdate.yaml +++ b/.github/workflows/lupdate.yml @@ -2,19 +2,23 @@ name: Update Translations on: workflow_dispatch: -# schedule: -# - cron: '0 0 * * 0' + schedule: + - cron: '0 0 * * 0' # Weekly on Sunday at midnight UTC concurrency: group: 'lupdate' cancel-in-progress: true -permissions: - contents: write - jobs: update-translations: + name: Update Translations runs-on: ubuntu-latest + timeout-minutes: 30 + + defaults: + run: + shell: bash + permissions: contents: write pull-requests: write @@ -23,10 +27,14 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + - name: Install Qt for Linux uses: jurplel/install-qt-action@v4 with: - version: 6.10.1 + version: ${{ steps.build-config.outputs.qt_version }} cache: true - name: Update translation files diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3730dd03fbb9..c51eed2a19d5 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -12,30 +12,44 @@ on: pull_request: paths: - '.github/workflows/macos.yml' + - '.github/actions/**' - 'deploy/macos/**' - 'src/**' + - 'test/**' - 'CMakeLists.txt' - 'cmake/**' - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + workflow_dispatch: + inputs: + build_type: + description: 'Build type' + required: false + default: 'Release' + type: choice + options: + - Release + - Debug + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +permissions: + contents: read jobs: build: + name: macOS runs-on: macos-latest + timeout-minutes: 120 strategy: matrix: - BuildType: [Release] + build_type: [Release] defaults: run: shell: bash - env: - QT_VERSION: 6.10.1 - steps: - name: Checkout repo uses: actions/checkout@v6 @@ -44,48 +58,53 @@ jobs: fetch-depth: 1 fetch-tags: true + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + - name: Initial Setup uses: ./.github/actions/common - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '<=16.x' + xcode-version: ${{ steps.build-config.outputs.xcode_version }} - - name: Install Dependencies (include GStreamer) - working-directory: ${{ github.workspace }}/tools/setup - run: sh install-dependencies-osx.sh + - name: Install Dependencies + uses: ./.github/actions/install-dependencies - name: Setup Caching uses: ./.github/actions/cache with: host: mac target: clang_64 - build-type: ${{ matrix.BuildType }} + build-type: ${{ matrix.build_type }} cpm-modules: ${{ runner.temp }}/build/cpm_modules + ccache-version: ${{ steps.build-config.outputs.ccache_version }} + save-cache: ${{ github.event_name != 'pull_request' }} - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: ${{ env.QT_VERSION }} + version: ${{ steps.build-config.outputs.qt_version }} host: mac target: desktop arch: clang_64 dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ steps.build-config.outputs.qt_modules }} setup-python: false cache: true - name: CMake configure working-directory: ${{ runner.temp }}/build run: ${{ env.QT_ROOT_DIR }}/bin/qt-cmake -S ${{ github.workspace }} -B . -G Ninja - -DCMAKE_BUILD_TYPE=${{ matrix.BuildType }} + -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DQGC_STABLE_BUILD=${{ github.ref_type == 'tag' || contains(github.ref, 'Stable') && 'ON' || 'OFF' }} -DQGC_MACOS_SIGN_WITH_IDENTITY=${{ github.event_name != 'pull_request' && 'ON' || 'OFF' }} - name: Build working-directory: ${{ runner.temp }}/build - run: cmake --build . --target all --config ${{ matrix.BuildType }} --parallel + run: cmake --build . --target all --config ${{ matrix.build_type }} --parallel - name: Sanity check dev build executable working-directory: ${{ runner.temp }}/build/Release/QGroundControl.app/Contents/MacOS @@ -100,7 +119,7 @@ jobs: - name: Create signed/notarized/stapled app bundle working-directory: ${{ runner.temp }}/build - run: cmake --install . --config ${{ matrix.BuildType }} + run: cmake --install . --config ${{ matrix.build_type }} env: QGC_MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} QGC_MACOS_NOTARIZATION_USERNAME: ${{ secrets.MACOS_NOTARIZATION_USERNAME }} @@ -116,7 +135,7 @@ jobs: run: ./QGroundControl --simple-boot-test - name: Upload Build File - if: matrix.BuildType == 'Release' + if: matrix.build_type == 'Release' uses: ./.github/actions/upload with: artifact_name: QGroundControl.dmg @@ -124,4 +143,3 @@ jobs: aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index f484f6b15571..08c984da7cf5 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,12 +1,44 @@ name: pre-commit -on: [push, pull_request] +on: + push: + paths: + - 'src/**' + - 'test/**' + - '.pre-commit-config.yaml' + - '.github/workflows/pre-commit.yml' + pull_request: + paths: + - 'src/**' + - 'test/**' + - '.pre-commit-config.yaml' + - '.github/workflows/pre-commit.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read jobs: pre-commit: + name: Pre-commit Checks runs-on: ubuntu-latest + timeout-minutes: 30 + + defaults: + run: + shell: bash + steps: - uses: actions/checkout@v6 + with: + fetch-tags: true + + - name: Get build config + id: build-config + uses: ./.github/actions/build-config - uses: actions/setup-python@v6 - name: Install Qt and qmllint @@ -17,7 +49,7 @@ jobs: pipx ensurepath USER_BASE_BIN="$(python3 -m site --user-base)/bin" export PATH="$USER_BASE_BIN:$PATH" - QT_VERSION=6.10.1 + QT_VERSION=${{ steps.build-config.outputs.qt_version }} QT_ARCH=linux_gcc_64 QT_PATH=/opt/Qt aqt install-qt linux desktop $QT_VERSION $QT_ARCH -O $QT_PATH diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..2317bb26f0d7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,211 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to create release for (e.g., v4.4.0)' + required: true + type: string + +permissions: + contents: write + +jobs: + wait-for-builds: + name: Wait for Platform Builds + runs-on: ubuntu-latest + timeout-minutes: 180 + + steps: + - name: Wait for builds to complete + uses: actions/github-script@v7 + with: + script: | + const tag = context.ref.replace('refs/tags/', '') || '${{ inputs.tag }}'; + console.log(`Waiting for builds for tag: ${tag}`); + + // Workflows to wait for + const requiredWorkflows = ['Linux', 'Windows', 'MacOS', 'Android']; + const maxWait = 170 * 60 * 1000; // 170 minutes + const pollInterval = 60 * 1000; // 1 minute + const startTime = Date.now(); + + while (Date.now() - startTime < maxWait) { + const runs = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: context.sha, + per_page: 100 + }); + + const workflowStatus = {}; + for (const run of runs.data.workflow_runs) { + if (requiredWorkflows.includes(run.name)) { + workflowStatus[run.name] = run.status === 'completed' ? run.conclusion : run.status; + } + } + + console.log('Current status:', JSON.stringify(workflowStatus, null, 2)); + + const allComplete = requiredWorkflows.every(w => + workflowStatus[w] === 'success' || workflowStatus[w] === 'failure' + ); + + if (allComplete) { + const failed = requiredWorkflows.filter(w => workflowStatus[w] !== 'success'); + if (failed.length > 0) { + core.setFailed(`Builds failed: ${failed.join(', ')}`); + return; + } + console.log('All builds completed successfully!'); + return; + } + + console.log(`Waiting ${pollInterval / 1000}s for builds to complete...`); + await new Promise(r => setTimeout(r, pollInterval)); + } + + core.setFailed('Timeout waiting for builds to complete'); + + create-release: + name: Create GitHub Release + needs: wait-for-builds + runs-on: ubuntu-latest + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get tag info + id: tag + run: | + TAG="${GITHUB_REF#refs/tags/}" + if [ -z "$TAG" ] || [ "$TAG" = "$GITHUB_REF" ]; then + TAG="${{ inputs.tag }}" + fi + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Tag: $TAG" + + # Check if this is a prerelease + if [[ "$TAG" == *"-"* ]]; then + echo "prerelease=true" >> $GITHUB_OUTPUT + else + echo "prerelease=false" >> $GITHUB_OUTPUT + fi + + - name: Download artifacts + uses: actions/download-artifact@v5 + with: + path: artifacts + merge-multiple: true + + - name: List artifacts + run: find artifacts -type f | head -50 || echo "No artifacts found" + + - name: Generate changelog + id: changelog + uses: actions/github-script@v7 + with: + script: | + const tag = '${{ steps.tag.outputs.tag }}'; + + // Get previous tag + const tags = await github.rest.repos.listTags({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 10 + }); + + let previousTag = null; + for (let i = 0; i < tags.data.length; i++) { + if (tags.data[i].name === tag && i + 1 < tags.data.length) { + previousTag = tags.data[i + 1].name; + break; + } + } + + console.log(`Generating changelog from ${previousTag || 'beginning'} to ${tag}`); + + // Get commits between tags + let commits; + if (previousTag) { + const comparison = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: previousTag, + head: tag + }); + commits = comparison.data.commits; + } else { + const commitList = await github.rest.repos.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: tag, + per_page: 50 + }); + commits = commitList.data; + } + + // Categorize commits + const categories = { + 'Features': [], + 'Bug Fixes': [], + 'Documentation': [], + 'CI/Build': [], + 'Other': [] + }; + + for (const commit of commits) { + const msg = commit.commit.message.split('\n')[0]; + const sha = commit.sha.substring(0, 7); + const entry = `- ${msg} (${sha})`; + + if (msg.match(/^feat|^add|^new/i)) { + categories['Features'].push(entry); + } else if (msg.match(/^fix|^bug/i)) { + categories['Bug Fixes'].push(entry); + } else if (msg.match(/^doc/i)) { + categories['Documentation'].push(entry); + } else if (msg.match(/^ci|^build|^chore/i)) { + categories['CI/Build'].push(entry); + } else { + categories['Other'].push(entry); + } + } + + // Build changelog + let changelog = ''; + for (const [category, entries] of Object.entries(categories)) { + if (entries.length > 0) { + changelog += `### ${category}\n${entries.join('\n')}\n\n`; + } + } + + if (!changelog) { + changelog = 'No notable changes in this release.'; + } + + const fs = require('fs'); + fs.writeFileSync('CHANGELOG.md', changelog); + return changelog; + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: QGroundControl ${{ steps.tag.outputs.tag }} + body_path: CHANGELOG.md + prerelease: ${{ steps.tag.outputs.prerelease }} + files: | + artifacts/**/*.AppImage + artifacts/**/*.dmg + artifacts/**/*.exe + artifacts/**/*.apk + fail_on_unmatched_files: false diff --git a/.github/workflows/repo-stats.yml b/.github/workflows/repo-stats.yml new file mode 100644 index 000000000000..7e2fcf387292 --- /dev/null +++ b/.github/workflows/repo-stats.yml @@ -0,0 +1,130 @@ +name: Repository Stats + +on: + schedule: + - cron: '0 9 * * 1' # Weekly on Monday at 9 AM UTC + workflow_dispatch: + +permissions: + contents: read + +jobs: + stats: + name: Generate Weekly Stats + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Generate statistics + id: stats + uses: actions/github-script@v7 + with: + script: | + const now = new Date(); + const weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000); + const weekAgoISO = weekAgo.toISOString(); + + // Get repo stats + const repo = await github.rest.repos.get({ + owner: context.repo.owner, + repo: context.repo.repo + }); + + // Get issues opened this week + const newIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'all', + since: weekAgoISO, + per_page: 100 + }); + + const issuesOpened = newIssues.data.filter(i => + !i.pull_request && new Date(i.created_at) > weekAgo + ).length; + + const issuesClosed = newIssues.data.filter(i => + !i.pull_request && i.state === 'closed' && i.closed_at && new Date(i.closed_at) > weekAgo + ).length; + + // Get PRs + const prsOpened = newIssues.data.filter(i => + i.pull_request && new Date(i.created_at) > weekAgo + ).length; + + const prsClosed = newIssues.data.filter(i => + i.pull_request && i.state === 'closed' && i.closed_at && new Date(i.closed_at) > weekAgo + ).length; + + // Get stale issues count + const staleIssues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'stale', + per_page: 1 + }); + + const netIssues = issuesOpened - issuesClosed; + const netPRs = prsOpened - prsClosed; + const stats = [ + '## 📊 Weekly Repository Stats', + '', + `**Week of ${weekAgo.toDateString()} - ${now.toDateString()}**`, + '', + '### Repository', + '| Metric | Count |', + '|--------|-------|', + `| ⭐ Stars | ${repo.data.stargazers_count.toLocaleString()} |`, + `| 🍴 Forks | ${repo.data.forks_count.toLocaleString()} |`, + `| 👀 Watchers | ${repo.data.subscribers_count.toLocaleString()} |`, + '', + '### This Week\'s Activity', + '| Metric | Opened | Closed | Net |', + '|--------|--------|--------|-----|', + `| Issues | ${issuesOpened} | ${issuesClosed} | ${netIssues >= 0 ? '+' : ''}${netIssues} |`, + `| PRs | ${prsOpened} | ${prsClosed} | ${netPRs >= 0 ? '+' : ''}${netPRs} |`, + '', + '### Current State', + '| Metric | Count |', + '|--------|-------|', + `| 📋 Open Issues/PRs | ${repo.data.open_issues_count} |`, + `| 🏷️ Stale Items | ${staleIssues.data.length > 0 ? '1+' : '0'} |`, + '', + '---', + `*Generated automatically by GitHub Actions on ${now.toISOString()}*` + ].join('\n'); + + console.log(stats); + + // Output to workflow summary + const fs = require('fs'); + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, stats); + + // Save stats as JSON for historical tracking + const jsonStats = { + date: now.toISOString(), + week_start: weekAgo.toISOString(), + repository: { + stars: repo.data.stargazers_count, + forks: repo.data.forks_count, + watchers: repo.data.subscribers_count + }, + weekly: { + issues_opened: issuesOpened, + issues_closed: issuesClosed, + prs_opened: prsOpened, + prs_closed: prsClosed + }, + current: { + open_issues: repo.data.open_issues_count + } + }; + fs.writeFileSync('stats.json', JSON.stringify(jsonStats, null, 2)); + + - name: Upload stats artifact + uses: actions/upload-artifact@v5 + with: + name: repo-stats-${{ github.run_id }} + path: stats.json + retention-days: 90 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 000000000000..ddefc8be57ae --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,39 @@ +name: OpenSSF Scorecard + +on: + push: + branches: + - master + schedule: + - cron: '0 5 * * 1' # Weekly on Monday at 5 AM UTC + workflow_dispatch: + +permissions: read-all + +jobs: + analysis: + name: Scorecard Analysis + runs-on: ubuntu-latest + timeout-minutes: 15 + + permissions: + security-events: write + id-token: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Run Scorecard + uses: ossf/scorecard-action@v2.4.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/size-check.yml b/.github/workflows/size-check.yml new file mode 100644 index 000000000000..7221446828b5 --- /dev/null +++ b/.github/workflows/size-check.yml @@ -0,0 +1,123 @@ +name: Size Check + +on: + workflow_run: + workflows: [Linux, Windows, MacOS, Android] + types: [completed] + branches: [master] + +permissions: + contents: read + actions: read + +jobs: + check-size: + name: Check Artifact Sizes + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + timeout-minutes: 15 + + steps: + - name: Download artifacts + uses: actions/download-artifact@v5 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + path: artifacts + + - name: Check sizes + id: sizes + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + // Size thresholds in bytes + const thresholds = { + 'AppImage': 200 * 1024 * 1024, // 200 MB + 'dmg': 150 * 1024 * 1024, // 150 MB + 'exe': 200 * 1024 * 1024, // 200 MB + 'apk': 150 * 1024 * 1024 // 150 MB + }; + + // Warning threshold (% increase from baseline) + const warningPercent = 10; + + function getFiles(dir, files = []) { + if (!fs.existsSync(dir)) return files; + const items = fs.readdirSync(dir); + for (const item of items) { + const fullPath = path.join(dir, item); + if (fs.statSync(fullPath).isDirectory()) { + getFiles(fullPath, files); + } else { + files.push(fullPath); + } + } + return files; + } + + const files = getFiles('artifacts'); + const sizes = {}; + const warnings = []; + + for (const file of files) { + const stats = fs.statSync(file); + const ext = path.extname(file).slice(1); + const name = path.basename(file); + const sizeMB = (stats.size / (1024 * 1024)).toFixed(2); + + sizes[name] = { + bytes: stats.size, + mb: sizeMB, + extension: ext + }; + + console.log(`${name}: ${sizeMB} MB`); + + // Check against threshold + const threshold = thresholds[ext]; + if (threshold && stats.size > threshold) { + const thresholdMB = (threshold / (1024 * 1024)).toFixed(0); + warnings.push(`⚠️ ${name} (${sizeMB} MB) exceeds ${thresholdMB} MB threshold`); + } + } + + // Output sizes as JSON + fs.writeFileSync('sizes.json', JSON.stringify(sizes, null, 2)); + + // Create summary + let summary = '## 📦 Artifact Sizes\n\n'; + summary += '| Artifact | Size |\n'; + summary += '|----------|------|\n'; + + for (const [name, info] of Object.entries(sizes)) { + summary += `| ${name} | ${info.mb} MB |\n`; + } + + if (warnings.length > 0) { + summary += '\n### ⚠️ Warnings\n'; + summary += warnings.join('\n'); + } + + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); + + return { sizes, warnings }; + + - name: Upload size report + uses: actions/upload-artifact@v5 + with: + name: size-report-${{ github.event.workflow_run.id }} + path: sizes.json + retention-days: 90 + + - name: Warn on size regression + if: steps.sizes.outputs.result != '' + uses: actions/github-script@v7 + with: + script: | + const result = ${{ steps.sizes.outputs.result }}; + if (result.warnings && result.warnings.length > 0) { + core.warning(`Artifact size warnings:\n${result.warnings.join('\n')}`); + } diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ddfd97eae73c..36f56fd189ca 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -3,14 +3,27 @@ on: schedule: - cron: '30 1 * * *' +permissions: + issues: write + pull-requests: write + jobs: stale: + name: Mark Stale Issues runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/stale@v10 with: - days-before-stale: 30 + days-before-stale: 90 days-before-close: -1 stale-issue-label: 'stale' stale-pr-label: 'stale' remove-stale-when-updated: true + stale-issue-message: > + This issue has been automatically marked as stale due to 90 days of inactivity. + It will remain open but may receive lower priority. If this is still relevant, + please comment to remove the stale label. + stale-pr-message: > + This PR has been automatically marked as stale due to 90 days of inactivity. + Please rebase and update if still relevant, or it may be closed in the future. diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml new file mode 100644 index 000000000000..7a5c3efc35ed --- /dev/null +++ b/.github/workflows/welcome.yml @@ -0,0 +1,40 @@ +name: Welcome + +on: + pull_request_target: + types: [opened] + issues: + types: [opened] + +permissions: + issues: write + pull-requests: write + +jobs: + welcome: + name: Welcome First-time Contributors + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: | + Thanks for opening your first issue! 👋 + + A maintainer will review this soon. In the meantime: + - 📖 Check our [User Guide](https://docs.qgroundcontrol.com/) and [Dev Guide](https://dev.qgroundcontrol.com/) + - 💬 Join [GitHub Discussions](https://github.com/mavlink/qgroundcontrol/discussions) for Q&A + - 🔍 Search [existing issues](https://github.com/mavlink/qgroundcontrol/issues) for similar reports + + Please make sure you've provided all requested information in the template. + pr-message: | + Thanks for your first pull request! 🎉 + + A maintainer will review this soon. Please ensure: + - [ ] CI checks pass + - [ ] Code follows [coding standards](https://github.com/mavlink/qgroundcontrol/blob/master/CODING_STYLE.md) + - [ ] Changes tested on relevant platforms + + We appreciate your contribution to QGroundControl! diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 25189b49d67b..065f6dfd3718 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,28 +4,45 @@ on: push: branches: - master - - Stable* + - 'Stable*' tags: - - v* + - 'v*' paths-ignore: - - docs/** + - 'docs/**' pull_request: paths: - - .github/workflows/windows.yml - - deploy/windows/** - - src/** - - CMakeLists.txt - - cmake/** - - tools/setup/*windows* - -# concurrency: -# group: ${{ github.workflow }}-${{ github.ref }} -# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + - '.github/workflows/windows.yml' + - '.github/actions/**' + - 'deploy/windows/**' + - 'src/**' + - 'test/**' + - 'custom-example/**' + - 'CMakeLists.txt' + - 'cmake/**' + - 'tools/setup/*windows*' + workflow_dispatch: + inputs: + build_type: + description: 'Build type' + required: false + default: 'Release' + type: choice + options: + - Release + - Debug + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +permissions: + contents: read jobs: build: - name: Build ${{ matrix.arch }} ${{ matrix.build_type }} + name: Build ${{ matrix.custom_build && 'Custom' || matrix.arch }} ${{ matrix.build_type }} runs-on: ${{ matrix.os }} + timeout-minutes: 120 strategy: fail-fast: false @@ -52,13 +69,17 @@ jobs: package: QGroundControl-installer-AMD64-ARM64 qt_version: '6.10.0' + # Custom build example (tests custom-example template) + - os: windows-latest + host: windows + arch: win64_msvc2022_64 + build_type: Release + custom_build: true + defaults: run: shell: cmd - env: - QT_VERSION: 6.10.1 - steps: - name: Checkout repo uses: actions/checkout@v6 @@ -67,18 +88,22 @@ jobs: fetch-depth: 1 fetch-tags: true + - name: Enable custom build + if: matrix.custom_build + uses: ./.github/actions/custom-build + + - name: Get build config + id: build-config + uses: ./.github/actions/build-config + - name: Common setup uses: ./.github/actions/common - # - name: Install dependencies - # shell: pwsh - # run: ./tools/setup/install-dependencies-windows.ps1 -Force - - name: Install GStreamer if: matrix.arch == 'win64_msvc2022_64' uses: blinemedical/setup-gstreamer@v1 with: - version: 1.22.12 + version: ${{ steps.build-config.outputs.gstreamer_version }} - name: Setup cache uses: ./.github/actions/cache @@ -87,16 +112,18 @@ jobs: target: ${{ matrix.arch }} build-type: ${{ matrix.build_type }} cpm-modules: ${{ runner.temp }}\build\cpm_modules + ccache-version: ${{ steps.build-config.outputs.ccache_version }} + save-cache: ${{ github.event_name != 'pull_request' }} - - name: Install Qt ${{ matrix.qt_version || env.QT_VERSION }} + - name: Install Qt ${{ matrix.qt_version || steps.build-config.outputs.qt_version }} uses: jurplel/install-qt-action@v4 with: - version: ${{ matrix.qt_version || env.QT_VERSION }} + version: ${{ matrix.qt_version || steps.build-config.outputs.qt_version }} host: ${{ matrix.host }} target: desktop arch: ${{ matrix.arch }} dir: ${{ runner.temp }} - modules: qtcharts qtlocation qtpositioning qtspeech qt5compat qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml + modules: ${{ steps.build-config.outputs.qt_modules }} setup-python: false cache: true @@ -121,14 +148,15 @@ jobs: - name: Verify Executable if: matrix.build_type == 'Release' && matrix.arch != 'win64_msvc2022_arm64_cross_compiled' working-directory: ${{ runner.temp }}\build\${{ matrix.build_type }} - run: QGroundControl.exe --simple-boot-test + run: ${{ matrix.custom_build && 'Custom-QGroundControl.exe' || 'QGroundControl.exe' }} --simple-boot-test - name: Create Installer + if: ${{ !matrix.custom_build }} working-directory: ${{ runner.temp }}\build run: cmake --install . --config ${{ matrix.build_type }} - name: Upload artifact (installer) - if: matrix.build_type == 'Release' + if: matrix.build_type == 'Release' && !matrix.custom_build uses: ./.github/actions/upload with: artifact_name: ${{ matrix.package }}.exe @@ -136,5 +164,4 @@ jobs: aws_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws_distribution_id: ${{ secrets.AWS_DISTRIBUTION_ID }} - github_token: ${{ secrets.GITHUB_TOKEN }} upload_aws: ${{ matrix.arch == 'win64_msvc2022_arm64_cross_compiled' && 'false' || 'true' }} diff --git a/.gitignore b/.gitignore index 2e3cd5566edd..1844b942c44d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,11 @@ # IDEs & Editors # ------------------------------------------------------------------------------ .idea/ -.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/extensions.json +!.vscode/tasks.json +!.vscode/launch.json .qtcreator/ .fleet/ .vs/ @@ -71,8 +75,9 @@ bin/mac *.dylib *.dll -# Build tool artifacts -Makefile +# Build tool artifacts (CMake-generated) +**/Makefile +!/Makefile *.ninja .ninja_log build.ninja @@ -152,9 +157,13 @@ Qt*-linux*.tar.* # Claude Code .claude/ +# AI assistant config files +AGENTS.md +CLAUDE.md +GEMINI.md + # cc-sessions - personal AI development workflow sessions/ -CLAUDE.md # Pre-commit hooks .ruff_cache/ diff --git a/.mailmap b/.mailmap new file mode 100644 index 000000000000..d093e4f534c4 --- /dev/null +++ b/.mailmap @@ -0,0 +1,30 @@ +# Git author name/email normalization +# Format: Canonical Name Alternate Name + +# Don Gagne - primary maintainer +Don Gagne +Don Gagne +Don Gagne DonLakeFlyer +Don Gagne +Don Gagne DoinLakeFlyer +Don Gagne +Don Gagne +Don Gagne Donald Gagne +Don Gagne Don Gagne <> + +# Gus Grubba +Gus Grubba +Gus Grubba +Gus Grubba dogmaphobic + +# Lorenz Meier - founder +Lorenz Meier +Lorenz Meier +Lorenz Meier LM +Lorenz Meier lm +Lorenz Meier lm +Lorenz Meier pixhawk + +# Bots +PX4BuildBot +PX4BuildBot PX4 Build Bot diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000000..209e3ef4b624 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 62121cbb2eb5..241836389501 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,7 @@ repos: - id: check-yaml args: [--allow-multiple-documents, --unsafe] - id: check-json + exclude: ^\.vscode/ # VSCode uses JSONC (JSON with comments) - id: check-xml exclude: ^test/ - id: check-merge-conflict @@ -47,6 +48,7 @@ repos: rev: v0.11.0.1 hooks: - id: shellcheck + args: ['-x', '-e', 'SC1091'] # -x follows sources, -e excludes SC1091 (not following) # YAML validation - repo: https://github.com/adrienverge/yamllint @@ -86,19 +88,83 @@ repos: - id: check-no-qassert name: Check for Q_ASSERT in production code - entry: bash + entry: python3 language: system files: \.(h|cc|cpp)$ - exclude: ^(test/|UnitTest\.h|UnitTest\.cc) - pass_filenames: false + exclude: ^test/ + pass_filenames: true + verbose: true + args: + - -c + - | + import sys, re + found = [] + for filepath in sys.argv[1:]: + try: + with open(filepath) as f: + for i, line in enumerate(f, 1): + if 'Q_ASSERT' in line and not line.strip().startswith('//'): + found.append(f"{filepath}:{i}: {line.rstrip()}") + except Exception: + pass + if found: + print("Warning: Q_ASSERT found. Consider defensive checks with early returns instead.") + print("Q_ASSERT is removed in release builds and can hide bugs.\n") + for f in found: + print(f" {f}") + # Exit 0 to warn but not block (change to sys.exit(1) to enforce) + sys.exit(0) + + - id: check-old-qml-connections + name: Check for deprecated QML Connections syntax + entry: python3 + language: system + files: \.qml$ + exclude: ^build/ + pass_filenames: true args: - -c - | - FILES=$(git diff --cached --name-only --diff-filter=d | grep -E '\.(h|cc|cpp)$' | grep -v '^test/' || true) - if [ -n "$FILES" ]; then - if echo "$FILES" | xargs grep -n 'Q_ASSERT' 2>/dev/null; then - echo "Warning: Q_ASSERT found in production code. Consider using defensive checks with early returns." - fi - fi + import sys, re + + # Pattern for old-style signal handlers: onSignalName: (not function onSignalName) + OLD_HANDLER = re.compile(r'^\s+on[A-Z][a-zA-Z0-9]*\s*:\s*[^/]') + CONNECTIONS_START = re.compile(r'Connections\s*\{') + FUNCTION_HANDLER = re.compile(r'^\s+function\s+on[A-Z]') + + errors = [] + for filepath in sys.argv[1:]: + try: + with open(filepath) as f: + content = f.read() + lines = content.split('\n') + + # Only check files that have Connections blocks + if 'Connections' not in content: + continue + + in_connections = 0 + for i, line in enumerate(lines, 1): + if CONNECTIONS_START.search(line): + in_connections += 1 + elif in_connections > 0: + if '{' in line: + in_connections += line.count('{') + if '}' in line: + in_connections -= line.count('}') + # Check for old-style handler inside Connections + if OLD_HANDLER.match(line) and not FUNCTION_HANDLER.match(line): + errors.append(f"{filepath}:{i}: {line.strip()}") + except Exception as e: + print(f"Error reading {filepath}: {e}", file=sys.stderr) + + if errors: + print("Deprecated QML Connections syntax found. Use 'function onSignal()' instead of 'onSignal:'") + print("See: https://doc.qt.io/qt-6/qml-qtqml-connections.html") + print() + for err in errors: + print(f" {err}") + # Exit 0 to warn but not block (change to sys.exit(1) to enforce) + sys.exit(0) exclude: ^build/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000000..b647e56bf51a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "bracketSpacing": true, + "proseWrap": "preserve", + "endOfLine": "lf", + "overrides": [ + { + "files": "*.md", + "options": { + "tabWidth": 2, + "proseWrap": "preserve" + } + }, + { + "files": ["*.yml", "*.yaml"], + "options": { + "tabWidth": 2, + "singleQuote": false + } + }, + { + "files": "*.json", + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/.qmlformat.ini b/.qmlformat.ini new file mode 100644 index 000000000000..17f19ccedfe1 --- /dev/null +++ b/.qmlformat.ini @@ -0,0 +1,8 @@ +# QML Formatting Configuration +# https://doc.qt.io/qt-6/qtquick-tool-qmlformat.html + +[General] +IndentWidth=4 +UseTabs=false +NormalizeOrder=true +NewlineType=native diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 000000000000..fa402cb0449a --- /dev/null +++ b/.typos.toml @@ -0,0 +1,57 @@ +# Typos Configuration +# https://github.com/crate-ci/typos + +[default] +extend-ignore-identifiers-re = [ + # Hexadecimal values + "[0-9a-fA-F]+", +] + +[default.extend-words] +# Project-specific terms +QGroundControl = "QGroundControl" +MAVLink = "MAVLink" +ArduPilot = "ArduPilot" +PX4 = "PX4" + +# Common abbreviations in codebase +Ack = "Ack" +ack = "ack" +Nack = "Nack" +nack = "nack" +Navit = "Navit" +Ser = "Ser" +ser = "ser" +TELEM = "TELEM" +telem = "telem" +RSSI = "RSSI" +rssi = "rssi" +VTOL = "VTOL" +vtol = "vtol" +ADSB = "ADSB" +adsb = "adsb" +RTK = "RTK" +rtk = "rtk" +RTL = "RTL" +rtl = "rtl" +HSV = "HSV" +NED = "NED" +ENU = "ENU" +FTP = "FTP" +ftp = "ftp" +ULog = "ULog" +ulog = "ulog" + +# Parameter names that look like typos +Slew = "Slew" +slew = "slew" + +[files] +extend-exclude = [ + "build/", + "translations/", + "*.ts", + "libs/", + "*.json", + "*.geojson", +] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000000..011925b20f0e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,19 @@ +{ + "recommendations": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "twxs.cmake", + "llvm-vs-code-extensions.vscode-clangd", + "AoiHosizora.vscode-qt-qmake", + "nicothin.qt-qml", + "streetsidesoftware.code-spell-checker", + "DavidAnson.vscode-markdownlint", + "EditorConfig.EditorConfig", + "eamodio.gitlens", + "mhutchie.git-graph", + "ms-vscode.hexeditor", + "redhat.vscode-yaml", + "tamasfe.even-better-toml" + ], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000000..e46f01b9fe57 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,114 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch QGroundControl", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/QGroundControl", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "QT_QPA_PLATFORM", + "value": "xcb" + }, + { + "name": "QML_IMPORT_TRACE", + "value": "0" + } + ], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Load Qt pretty printers", + "text": "source ${workspaceFolder}/tools/gdb-pretty-printers/qt6.py", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: Build" + }, + { + "name": "Launch QGroundControl (LLDB)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/build/QGroundControl", + "args": [], + "cwd": "${workspaceFolder}", + "env": { + "QT_QPA_PLATFORM": "cocoa", + "QML_IMPORT_TRACE": "0" + }, + "preLaunchTask": "CMake: Build" + }, + { + "name": "Run Unit Tests (Debug)", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/QGroundControl", + "args": ["--unittest"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: Build" + }, + { + "name": "Debug Specific Test", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/QGroundControl", + "args": ["--unittest:${input:testClass}"], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "CMake: Build" + }, + { + "name": "Attach to QGroundControl", + "type": "cppdbg", + "request": "attach", + "program": "${workspaceFolder}/build/QGroundControl", + "processId": "${command:pickProcess}", + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ], + "inputs": [ + { + "id": "testClass", + "type": "promptString", + "description": "Test class name (e.g., FactSystemTest)" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000000..590c6c6f4bde --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,45 @@ +{ + "editor.formatOnSave": false, + "editor.rulers": [120], + "editor.tabSize": 4, + "editor.insertSpaces": true, + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.associations": { + "*.qml": "qml", + "*.SettingsGroup.json": "json", + "*.FactMetaData.json": "json", + "CMakePresets.json": "jsonc" + }, + "files.watcherExclude": { + "**/build/**": true, + "**/.cache/**": true + }, + "search.exclude": { + "**/build": true, + "**/.cache": true, + "**/libs": true + }, + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "C_Cpp.codeAnalysis.clangTidy.enabled": true, + "C_Cpp.codeAnalysis.clangTidy.useBuildPath": true, + "C_Cpp.formatting.clangFormatStyle": "file", + "C_Cpp.default.cppStandard": "c++20", + "cmake.configureOnOpen": true, + "cmake.buildDirectory": "${workspaceFolder}/build", + "cmake.generator": "Ninja", + "cmake.parallelJobs": 0, + "clangd.arguments": [ + "--background-index", + "--clang-tidy", + "--header-insertion=never", + "--completion-style=detailed", + "--function-arg-placeholders=false" + ], + "qml.format.enabled": true, + "python.defaultInterpreterPath": "python3", + "[markdown]": { + "editor.wordWrap": "on" + }, + "git.ignoreLimitWarning": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000000..a28691a6886a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,116 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: Configure (Debug)", + "type": "shell", + "command": "cmake", + "args": [ + "--preset", "debug" + ], + "group": "build", + "problemMatcher": [] + }, + { + "label": "CMake: Configure (Release)", + "type": "shell", + "command": "cmake", + "args": [ + "--preset", "release" + ], + "group": "build", + "problemMatcher": [] + }, + { + "label": "CMake: Build", + "type": "shell", + "command": "cmake", + "args": [ + "--build", "build", + "--parallel" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$gcc" + }, + { + "label": "CMake: Clean", + "type": "shell", + "command": "cmake", + "args": [ + "--build", "build", + "--target", "clean" + ], + "group": "build", + "problemMatcher": [] + }, + { + "label": "Run Unit Tests", + "type": "shell", + "command": "${workspaceFolder}/build/QGroundControl", + "args": ["--unittest"], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [], + "dependsOn": "CMake: Build" + }, + { + "label": "Run Specific Test", + "type": "shell", + "command": "${workspaceFolder}/build/QGroundControl", + "args": ["--unittest:${input:testClass}"], + "group": "test", + "problemMatcher": [], + "dependsOn": "CMake: Build" + }, + { + "label": "Format: Check Changed Files", + "type": "shell", + "command": "${workspaceFolder}/tools/format-check.sh", + "args": ["--check"], + "group": "none", + "problemMatcher": [] + }, + { + "label": "Format: Fix Changed Files", + "type": "shell", + "command": "${workspaceFolder}/tools/format-check.sh", + "args": [], + "group": "none", + "problemMatcher": [] + }, + { + "label": "Static Analysis", + "type": "shell", + "command": "${workspaceFolder}/tools/analyze.sh", + "group": "none", + "problemMatcher": "$gcc" + }, + { + "label": "Clean All", + "type": "shell", + "command": "${workspaceFolder}/tools/clean.sh", + "group": "none", + "problemMatcher": [] + }, + { + "label": "Update Translations", + "type": "shell", + "command": "source", + "args": ["${workspaceFolder}/tools/translations/qgc-lupdate.sh"], + "group": "none", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "testClass", + "type": "promptString", + "description": "Test class name (e.g., FactSystemTest)" + } + ] +} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index a212caa12fd6..000000000000 --- a/AGENTS.md +++ /dev/null @@ -1,85 +0,0 @@ -# QGroundControl Quick Reference for AI Assistants - -**Ground Control Station** for UAVs using MAVLink protocol. **C++20/Qt 6.10.1** with QML UI. - -## 🔑 Most Critical Architecture Pattern - -**Fact System** - QGC's type-safe parameter management. Every vehicle parameter uses it. - -```cpp -// ALWAYS use this pattern for parameters -Fact* param = vehicle->parameterManager()->getParameter(-1, "PARAM_NAME"); -if (param) { - param->setCookedValue(newValue); // cookedValue = UI (with units) - // param->rawValue() = MAVLink/storage -} -``` - -**Rules:** -- Wait for `parametersReady` signal before accessing -- Use cookedValue (display) vs rawValue (storage) -- Never create custom parameter storage - -## 🏗️ Key Patterns - -1. **Plugins**: FirmwarePlugin (PX4/ArduPilot behavior), AutoPilotPlugin (setup UI), VehicleComponent (individual items) -2. **Managers**: Singleton pattern - `MultiVehicleManager::instance()->activeVehicle()` (always null-check!) -3. **QML Integration**: `QML_ELEMENT`, `Q_PROPERTY`, `Q_INVOKABLE` -4. **State Machines**: Use `QGCStateMachine` for complex workflows (calibration, parameter loading) - -## 📂 Code Structure - -``` -src/ -├── FactSystem/ # Parameter management (READ FIRST!) -├── Vehicle/ # Vehicle state (Vehicle.h is critical) -├── FirmwarePlugin/ # PX4/ArduPilot abstraction -├── AutoPilotPlugins/ # Vehicle setup UI -├── MissionManager/ # Mission planning -├── Comms/ # Serial/UDP/TCP/Bluetooth links -``` - -## ⚡ Quick Build - -```bash -git submodule update --init --recursive -~/Qt/6.10.1/gcc_64/bin/qt-cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -cmake --build build --config Debug -./build/Debug/QGroundControl --unittest # Run tests -``` - -## ❌ Common Mistakes (DO NOT!) - -1. Assume single vehicle (always null-check `activeVehicle()`) -2. Access Facts before `parametersReady` -3. Use `Q_ASSERT` in production code -4. Bypass FirmwarePlugin for firmware-specific behavior -5. Mix cookedValue/rawValue without conversion - -## 📖 Essential Reading - -1. **`.github/copilot-instructions.md`** - Detailed architecture guide -2. **`.github/CONTRIBUTING.md`** - Contribution guidelines -3. **`src/FactSystem/Fact.h`** - Parameter system (MOST CRITICAL!) -4. **`src/Vehicle/Vehicle.h`** - Vehicle model (~1477 lines) - -## 🧑‍💻 Coding Style - -- **Naming**: Classes `PascalCase`, methods `camelCase`, privates `_leadingUnderscore` -- **Files**: `ClassName.h`, `ClassName.cc` -- **Defensive**: Always validate inputs, null-check pointers, early returns -- **Logging**: `QGC_LOGGING_CATEGORY(MyLog, "qgc.component")` + `qCDebug(MyLog)` -- **Braces**: Always use, even for single-line if statements -- **Formatting**: `.clang-format`, `.clang-tidy`, `.editorconfig` configured in repo root -- **Comments**: Keep minimal - code should be self-documenting. No verbose headers or documentation files unless explicitly requested. - -## 🔗 Resources - -- **Dev Guide**: https://dev.qgroundcontrol.com/ -- **User Docs**: https://docs.qgroundcontrol.com/ -- **MAVLink**: https://mavlink.io/ -- **Qt 6**: https://doc.qt.io/qt-6/ - ---- - -**Key Principle**: Match existing code style. Use defensive coding. Respect the Fact System architecture. diff --git a/CMakePresets.json.template b/CMakePresets.json.template index c5593e94083d..4b2aca0dac2b 100644 --- a/CMakePresets.json.template +++ b/CMakePresets.json.template @@ -1,9 +1,9 @@ { - "version": 4, + "version": 6, "cmakeMinimumRequired": { "major": 3, - "minor": 22, - "patch": 1 + "minor": 25, + "patch": 0 }, "include": [ "cmake/presets/common.json", diff --git a/CODING_STYLE.md b/CODING_STYLE.md new file mode 100644 index 000000000000..ddf2d459e13a --- /dev/null +++ b/CODING_STYLE.md @@ -0,0 +1,268 @@ +# QGroundControl Coding Style Guide + +This document describes the coding conventions for QGroundControl. For complete examples, see the reference files: + +- [CodingStyle.h](tools/coding-style/CodingStyle.h) - C++ header example +- [CodingStyle.cc](tools/coding-style/CodingStyle.cc) - C++ implementation example +- [CodingStyle.qml](tools/coding-style/CodingStyle.qml) - QML example + +## General + +- **Indentation**: 4 spaces (no tabs) +- **Line endings**: LF (Unix-style) +- **File encoding**: UTF-8 +- **Max line length**: No hard limit, use judgment + +## Naming Conventions + +| Element | Convention | Example | +|---------|------------|---------| +| Classes | PascalCase | `VehicleManager` | +| Methods/Functions | camelCase | `getActiveVehicle()` | +| Variables | camelCase | `activeVehicle` | +| Private members | _leadingUnderscore | `_vehicleList` | +| Constants | UPPER_SNAKE_CASE | `MAX_RETRY_COUNT` | +| Enums | PascalCase (scoped) | `enum class FlightMode` | +| Files | ClassName.h/.cc | `Vehicle.h`, `Vehicle.cc` | + +## C++ Style + +### Headers + +```cpp +#pragma once + +// System headers first +#include +#include + +// Qt headers second (use full paths) +#include +#include + +// Project headers last +#include "Vehicle.h" +``` + +### Class Declaration Order + +```cpp +class MyClass : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(...) + +public: + // Constructors/destructor + // Enums + // Public methods + // Getters/setters + +signals: + // Signals + +public slots: + // Public slots (only if connected externally) + +protected: + // Protected members (only for base classes) + +private slots: + // Private slots + +private: + // Private methods (prefixed with _) + // Private members (prefixed with _) +}; +``` + +### Modern C++ (C++20) + +QGroundControl uses C++20. Prefer modern features: + +```cpp +// Use [[nodiscard]] for functions with important return values +[[nodiscard]] bool isValid() const; + +// Use std::string_view for read-only string parameters (non-Qt code) +bool validate(std::string_view input); + +// Use std::span instead of pointer + size +void processData(std::span data); + +// Use ranges for cleaner algorithms +auto filtered = data | std::views::filter([](int n) { return n > 0; }); + +// Use designated initializers for structs +Config config{.timeout = 30, .retries = 3}; + +// Use constexpr for compile-time constants +static constexpr int MaxRetries = 5; +``` + +### Defensive Coding + +```cpp +// Always null-check pointers +Vehicle* vehicle = _manager->activeVehicle(); +if (!vehicle) { + qCWarning(MyLog) << "No active vehicle"; + return; +} + +// Validate inputs early +if (param.isEmpty()) { + return; +} + +// Avoid Q_ASSERT in production - use defensive checks instead +// Q_ASSERT is removed in release builds +``` + +### Logging + +```cpp +// Declare in header +Q_DECLARE_LOGGING_CATEGORY(MyComponentLog) + +// Define in source (use QGC macro for runtime configuration) +QGC_LOGGING_CATEGORY(MyComponentLog, "qgc.component.name") + +// Use categorized logging +qCDebug(MyComponentLog) << "Debug message"; +qCWarning(MyComponentLog) << "Warning message"; +``` + +## Qt6 / QML Integration + +### Exposing C++ to QML + +```cpp +class MyClass : public QObject +{ + Q_OBJECT + QML_ELEMENT // For QML-creatable types + QML_UNCREATABLE("C++ only") // For C++-only instantiation + QML_SINGLETON // For singletons + + Q_MOC_INCLUDE("Vehicle.h") // For forward-declared types in Q_PROPERTY + + Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) + Q_PROPERTY(Vehicle* vehicle READ vehicle CONSTANT) +}; +``` + +### Signal Emission + +```cpp +void MyClass::setValue(int newValue) +{ + if (_value != newValue) { + _value = newValue; + emit valueChanged(_value); // Always emit when property changes + } +} +``` + +## QML Style + +### File Structure + +```qml +import QtQuick +import QtQuick.Controls + +import QGroundControl +import QGroundControl.Controls + +Item { + id: root + + // 1. Property bindings (width, height, anchors) + width: ScreenTools.defaultFontPixelHeight * 10 + + // 2. Public properties + property int myProperty: 0 + + // 3. Private properties (underscore prefix) + readonly property bool _isValid: myProperty > 0 + + // 4. Signals + signal clicked() + + // 5. Functions + function doSomething() { } + + // 6. Visual children + QGCButton { + text: qsTr("Click Me") + onClicked: root.clicked() + } + + // 7. Connections (use function syntax) + Connections { + target: someObject + function onSignalName() { } // NOT: onSignalName: { } + } + + // 8. Component.onCompleted + Component.onCompleted: { } +} +``` + +### QML Guidelines + +- **No hardcoded sizes**: Use `ScreenTools.defaultFontPixelHeight/Width` +- **No hardcoded colors**: Use `QGCPalette` for theming +- **Use QGC controls**: `QGCButton`, `QGCLabel`, `QGCTextField`, etc. +- **Translations**: Wrap user-visible strings with `qsTr()` +- **Null checks**: Always check `_activeVehicle` before use + +### Connections Syntax (Qt6) + +```qml +// CORRECT - Qt6 function syntax +Connections { + target: vehicle + function onArmedChanged() { + console.log("Armed state changed") + } +} + +// DEPRECATED - Old Qt5 syntax (triggers pre-commit warning) +Connections { + target: vehicle + onArmedChanged: { } // Don't use this +} +``` + +## Common Pitfalls + +1. **Assuming single vehicle** - Always null-check `activeVehicle()` +2. **Accessing Facts before ready** - Wait for `parametersReady` signal +3. **Bypassing FirmwarePlugin** - Use plugin for firmware-specific behavior +4. **Using Q_ASSERT in production** - Use defensive checks instead +5. **Mixing cookedValue/rawValue** - Understand the difference +6. **Hardcoded QML sizes/colors** - Use ScreenTools and QGCPalette + +## Formatting Tools + +The repository includes configuration for automatic formatting: + +- `.clang-format` - C++ formatting +- `.clang-tidy` - C++ static analysis +- `.qmlformat.ini` - QML formatting +- `.qmllint.ini` - QML linting +- `.editorconfig` - Editor settings + +Run pre-commit checks: +```bash +pre-commit run --all-files +``` + +## Additional Resources + +- [Qt 6 Documentation](https://doc.qt.io/qt-6/) +- [QGroundControl Dev Guide](https://dev.qgroundcontrol.com/) +- [MAVLink Protocol](https://mavlink.io/) diff --git a/Makefile b/Makefile new file mode 100644 index 000000000000..4f6dfbe0de7f --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# QGroundControl Development Makefile +# Convenience wrapper for common commands + +.PHONY: help configure build test clean lint format docker submodules + +# Default target +help: + @echo "QGroundControl Development Commands" + @echo "" + @echo " make configure - Configure CMake build (Debug)" + @echo " make build - Build the project" + @echo " make test - Run unit tests" + @echo " make clean - Remove build directory" + @echo " make lint - Run pre-commit checks" + @echo " make format - Format C++ code with clang-format" + @echo " make docker - Build using Docker (Ubuntu)" + @echo " make submodules - Initialize git submodules" + @echo "" + @echo "Environment variables:" + @echo " QT_DIR - Qt installation directory (default: ~/Qt//gcc_64)" + @echo " BUILD_TYPE - CMake build type (default: Debug)" + @echo "" + @echo "Configuration from .github/build-config.json:" + @echo " Qt version: $(QT_VERSION)" + +# Configuration - reads Qt version from centralized config +QT_VERSION := $(shell ./tools/setup/read-config.sh qt_version 2>/dev/null || echo "6.10.1") +QT_DIR ?= $(HOME)/Qt/$(QT_VERSION)/gcc_64 +BUILD_TYPE ?= Debug +BUILD_DIR := build + +submodules: + git submodule update --init --recursive + +configure: submodules + $(QT_DIR)/bin/qt-cmake -B $(BUILD_DIR) -G Ninja -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) + +build: + cmake --build $(BUILD_DIR) --config $(BUILD_TYPE) --parallel + +test: + ./$(BUILD_DIR)/$(BUILD_TYPE)/QGroundControl --unittest + +clean: + rm -rf $(BUILD_DIR) + +lint: + pre-commit run --all-files + +format: + find src -name "*.h" -o -name "*.cc" -o -name "*.cpp" | xargs clang-format -i + +docker: + ./deploy/docker/run-docker-ubuntu.sh diff --git a/README.md b/README.md index b9405404a77c..ba1c18427d8d 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,20 @@

- - Latest Release + + Latest Release + + + Linux Build + + + macOS Build + + + Windows Build + + + Crowdin

@@ -30,7 +42,7 @@ - ⚙️ *Vehicle Setup*: Tailored configuration for *PX4* and *ArduPilot* platforms. - 🔧 *Fully Open Source*: Customize and extend the software to suit your needs. -🎯 Check out the latest updates in our [New Features and Release Notes](https://github.com/mavlink/qgroundcontrol/blob/master/ChangeLog.md). +🎯 Check out the latest updates in our [New Features and Release Notes](https://github.com/mavlink/qgroundcontrol/blob/master/CHANGELOG.md). --- diff --git a/cmake/Helpers.cmake b/cmake/Helpers.cmake index 92b64621cf39..6c8f274881b2 100644 --- a/cmake/Helpers.cmake +++ b/cmake/Helpers.cmake @@ -54,16 +54,14 @@ function(qgc_config_caching) string(TOLOWER "${_cache_tool}" _cache_tool) if(_cache_tool STREQUAL "ccache") - # set(ENV{CCACHE_CONFIGPATH} "${CMAKE_SOURCE_DIR}/tools/ccache.conf") - # set(ENV{CCACHE_DIR} "${CMAKE_SOURCE_DIR}/.ccache") + set(ENV{CCACHE_DIR} "${CMAKE_SOURCE_DIR}/.cache/ccache") set(ENV{CCACHE_BASEDIR} "${CMAKE_SOURCE_DIR}") + set(ENV{CCACHE_MAXSIZE} "2G") set(ENV{CCACHE_COMPRESSLEVEL} "5") set(ENV{CCACHE_SLOPPINESS} "pch_defines,time_macros,include_file_mtime,include_file_ctime") - # set(ENV{CCACHE_NOHASHDIR} "true") if(APPLE) set(ENV{CCACHE_COMPILERCHECK} "content") endif() - # set(ENV{CCACHE_MAXSIZE} "1G") elseif(_cache_tool STREQUAL "sccache") # set(ENV{SCCACHE_PATH} "") # set(ENV{SCCACHE_DIR} "") diff --git a/cmake/find-modules/FindGStreamer.cmake b/cmake/find-modules/FindGStreamer.cmake index af3a1c208231..5844f980cd8d 100644 --- a/cmake/find-modules/FindGStreamer.cmake +++ b/cmake/find-modules/FindGStreamer.cmake @@ -11,11 +11,16 @@ # ---------------------------------------------------------------------------- # Set default version based on platform +# Android uses 1.22.12 with custom S3 package (full 1.24.x is 1.2GB and causes disk space issues) +# Windows/macOS use 1.24.13 (latest stable release) +# Linux uses system-installed version (minimum 1.20) if(NOT DEFINED GStreamer_FIND_VERSION) if(LINUX) set(GStreamer_FIND_VERSION 1.20) - else() + elseif(ANDROID) set(GStreamer_FIND_VERSION 1.22.12) + else() + set(GStreamer_FIND_VERSION 1.24.13) endif() endif() diff --git a/cmake/Coverage.cmake b/cmake/modules/Coverage.cmake similarity index 100% rename from cmake/Coverage.cmake rename to cmake/modules/Coverage.cmake diff --git a/cmake/presets/Linux.json b/cmake/presets/Linux.json index 1e6e469877f5..c86feb318cf7 100644 --- a/cmake/presets/Linux.json +++ b/cmake/presets/Linux.json @@ -4,120 +4,175 @@ "configurePresets": [ { "name": "Linux", - "displayName": "Linux configuration using Qt6", + "displayName": "Linux Release", "generator": "Ninja", "binaryDir": "${sourceParentDir}/build/qt6-Linux", "toolchainFile": "$penv{QT_ROOT_DIR}/lib/cmake/Qt6/qt.toolchain.cmake", + "inherits": ["release"], "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release", "QT_VERSION_MAJOR": "6" } }, + { + "name": "Linux-debug", + "displayName": "Linux Debug", + "inherits": ["debug", "Linux"], + "binaryDir": "${sourceParentDir}/build/qt6-Linux-debug" + }, { "name": "Linux-ccache", - "displayName": "Linux configuration using Qt6 and ccache", + "displayName": "Linux Release with ccache", "inherits": ["dev", "ccache", "Linux"] + }, + { + "name": "Linux-debug-ccache", + "displayName": "Linux Debug with ccache", + "inherits": ["dev", "ccache", "Linux-debug"] + }, + { + "name": "Linux-coverage", + "displayName": "Linux Coverage", + "inherits": ["coverage", "Linux"], + "binaryDir": "${sourceParentDir}/build/qt6-Linux-coverage" + }, + { + "name": "Linux-minimal", + "displayName": "Linux Minimal (fast build)", + "inherits": ["minimal", "Linux-debug"], + "binaryDir": "${sourceParentDir}/build/qt6-Linux-minimal" } ], "buildPresets": [ { "name": "Linux", - "displayName": "Linux build using Qt6", + "displayName": "Linux Release", "configurePreset": "Linux" }, + { + "name": "Linux-debug", + "displayName": "Linux Debug", + "configurePreset": "Linux-debug" + }, { "name": "Linux-ccache", - "displayName": "Linux build using Qt6 and ccache", + "displayName": "Linux Release with ccache", "configurePreset": "Linux-ccache" + }, + { + "name": "Linux-debug-ccache", + "displayName": "Linux Debug with ccache", + "configurePreset": "Linux-debug-ccache" + }, + { + "name": "Linux-coverage", + "displayName": "Linux Coverage", + "configurePreset": "Linux-coverage" + }, + { + "name": "Linux-minimal", + "displayName": "Linux Minimal", + "configurePreset": "Linux-minimal" } ], "testPresets": [ { "name": "Linux", - "displayName": "Linux tests using Qt6", + "displayName": "Linux Release tests", "configurePreset": "Linux", "inherits": ["default"] }, + { + "name": "Linux-debug", + "displayName": "Linux Debug tests", + "configurePreset": "Linux-debug", + "inherits": ["default"] + }, { "name": "Linux-ccache", - "displayName": "Linux tests using Qt6 and ccache", + "displayName": "Linux Release tests with ccache", "configurePreset": "Linux-ccache", "inherits": ["default"] + }, + { + "name": "Linux-debug-ccache", + "displayName": "Linux Debug tests with ccache", + "configurePreset": "Linux-debug-ccache", + "inherits": ["default"] + }, + { + "name": "Linux-coverage", + "displayName": "Linux Coverage tests", + "configurePreset": "Linux-coverage", + "inherits": ["default"] } ], "packagePresets": [ { "name": "Linux", - "displayName": "Linux package using Qt6", + "displayName": "Linux Release package", "configurePreset": "Linux" }, { "name": "Linux-ccache", - "displayName": "Linux package using Qt6 and ccache", + "displayName": "Linux Release package with ccache", "configurePreset": "Linux-ccache" } ], "workflowPresets": [ { "name": "Linux", - "displayName": "Linux workflow using Qt6", + "displayName": "Linux Release workflow", + "steps": [ + { "type": "configure", "name": "Linux" }, + { "type": "build", "name": "Linux" }, + { "type": "test", "name": "Linux" }, + { "type": "package", "name": "Linux" } + ] + }, + { + "name": "Linux-debug", + "displayName": "Linux Debug workflow", "steps": [ - { - "type": "configure", - "name": "Linux" - }, - { - "type": "build", - "name": "Linux" - }, - { - "type": "test", - "name": "Linux" - }, - { - "type": "package", - "name": "Linux" - } + { "type": "configure", "name": "Linux-debug" }, + { "type": "build", "name": "Linux-debug" }, + { "type": "test", "name": "Linux-debug" } ] }, { "name": "Linux-ccache", - "displayName": "Linux workflow using Qt6 and ccache", + "displayName": "Linux Release workflow with ccache", + "steps": [ + { "type": "configure", "name": "Linux-ccache" }, + { "type": "build", "name": "Linux-ccache" }, + { "type": "test", "name": "Linux-ccache" }, + { "type": "package", "name": "Linux-ccache" } + ] + }, + { + "name": "Linux-debug-ccache", + "displayName": "Linux Debug workflow with ccache", + "steps": [ + { "type": "configure", "name": "Linux-debug-ccache" }, + { "type": "build", "name": "Linux-debug-ccache" }, + { "type": "test", "name": "Linux-debug-ccache" } + ] + }, + { + "name": "Linux-coverage", + "displayName": "Linux Coverage workflow", "steps": [ - { - "type": "configure", - "name": "Linux-ccache" - }, - { - "type": "build", - "name": "Linux-ccache" - }, - { - "type": "test", - "name": "Linux-ccache" - }, - { - "type": "package", - "name": "Linux-ccache" - } + { "type": "configure", "name": "Linux-coverage" }, + { "type": "build", "name": "Linux-coverage" }, + { "type": "test", "name": "Linux-coverage" } ] }, { "name": "Linux-CI", - "displayName": "Linux workflow using Qt6 and ccache for CI", + "displayName": "Linux CI workflow (Release)", "steps": [ - { - "type": "configure", - "name": "Linux-ccache" - }, - { - "type": "build", - "name": "Linux-ccache" - }, - { - "type": "package", - "name": "Linux-ccache" - } + { "type": "configure", "name": "Linux-ccache" }, + { "type": "build", "name": "Linux-ccache" }, + { "type": "package", "name": "Linux-ccache" } ] } ] diff --git a/cmake/presets/common.json b/cmake/presets/common.json index ead509e34015..739a0969b2c2 100644 --- a/cmake/presets/common.json +++ b/cmake/presets/common.json @@ -23,6 +23,52 @@ "CMAKE_C_COMPILER_LAUNCHER": "ccache", "CMAKE_CXX_COMPILER_LAUNCHER": "ccache" } + }, + { + "name": "sccache", + "hidden": true, + "cacheVariables": { + "CMAKE_C_COMPILER_LAUNCHER": "sccache", + "CMAKE_CXX_COMPILER_LAUNCHER": "sccache" + } + }, + { + "name": "debug", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "QGC_BUILD_TESTING": "ON", + "QGC_DEBUG_QML": "ON" + } + }, + { + "name": "release", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "QGC_BUILD_TESTING": "OFF", + "QGC_DEBUG_QML": "OFF", + "QGC_BUILD_INSTALLER": "ON" + } + }, + { + "name": "coverage", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "QGC_BUILD_TESTING": "ON", + "QGC_ENABLE_COVERAGE": "ON" + } + }, + { + "name": "minimal", + "hidden": true, + "cacheVariables": { + "QGC_VIEWER3D": "OFF", + "QGC_ENABLE_BLUETOOTH": "OFF", + "QGC_ENABLE_GST_VIDEOSTREAMING": "OFF", + "QGC_ENABLE_UVC": "OFF" + } } ], "testPresets": [ diff --git a/deploy/docker/Dockerfile-build-ubuntu b/deploy/docker/Dockerfile-build-ubuntu index 3558ec1063a1..4f4abb16b61d 100644 --- a/deploy/docker/Dockerfile-build-ubuntu +++ b/deploy/docker/Dockerfile-build-ubuntu @@ -31,14 +31,18 @@ ENV LANG=en_US.UTF-8 \ # ---------- Bring in the helper scripts ---------- # They must be in the build context next to this Dockerfile. +COPY .github/build-config.json /tmp/qt/ +COPY tools/setup/read-config.sh /tmp/qt/ COPY tools/setup/install-dependencies-debian.sh /tmp/qt/ -COPY tools/setup/install-qt-debian.sh /tmp/qt/ +COPY tools/setup/install-qt-debian.sh /tmp/qt/ RUN chmod +x /tmp/qt/*.sh && \ /tmp/qt/install-dependencies-debian.sh && \ /tmp/qt/install-qt-debian.sh && \ rm -rf /tmp/qt # keep the image slim -ENV QT_ROOT_DIR=/opt/Qt/6.10.1/gcc_64 +# Qt version passed from workflow (reads from .github/build-config.json) +ARG QT_VERSION=6.10.1 +ENV QT_ROOT_DIR=/opt/Qt/${QT_VERSION}/gcc_64 ENV PATH=$QT_ROOT_DIR/bin:$PATH # ---------- Git safe directory (avoids “detected dubious ownership”) ---------- diff --git a/docs/en/qgc-dev-guide/contribute/coding_style.md b/docs/en/qgc-dev-guide/contribute/coding_style.md index 3d4d62420c1a..7e3e0d749620 100644 --- a/docs/en/qgc-dev-guide/contribute/coding_style.md +++ b/docs/en/qgc-dev-guide/contribute/coding_style.md @@ -1,12 +1,40 @@ # Coding Style -High level style information: +See the full **[Coding Style Guide](https://github.com/mavlink/qgroundcontrol/blob/master/CODING_STYLE.md)** for comprehensive documentation. -- Tabs expanded to 4 spaces -- Pascal/CamelCase naming conventions +## Quick Reference -The style itself is documents in the following example files: +- **Indentation**: 4 spaces (no tabs) +- **Naming**: PascalCase for classes, camelCase for methods/variables +- **Private members**: Prefix with underscore (`_myVariable`) +- **C++ Standard**: C++20 +- **Qt Version**: Qt 6.10+ -- [CodingStyle.cc](https://github.com/mavlink/qgroundcontrol/blob/master/CodingStyle.cc) -- [CodingStyle.h](https://github.com/mavlink/qgroundcontrol/blob/master/CodingStyle.h) -- [CodingStyle.qml](https://github.com/mavlink/qgroundcontrol/blob/master/CodingStyle.qml) +## Example Files + +The coding style is demonstrated in these reference files: + +- [CodingStyle.h](https://github.com/mavlink/qgroundcontrol/blob/master/tools/coding-style/CodingStyle.h) - C++ header patterns +- [CodingStyle.cc](https://github.com/mavlink/qgroundcontrol/blob/master/tools/coding-style/CodingStyle.cc) - C++ implementation patterns +- [CodingStyle.qml](https://github.com/mavlink/qgroundcontrol/blob/master/tools/coding-style/CodingStyle.qml) - QML patterns + +## Key Guidelines + +### C++ +- Use `[[nodiscard]]` for functions with important return values +- Use `std::span` instead of pointer + size parameters +- Use defensive checks instead of `Q_ASSERT` (removed in release builds) +- Use `QGC_LOGGING_CATEGORY` for logging + +### QML +- No hardcoded sizes - use `ScreenTools` +- No hardcoded colors - use `QGCPalette` +- Use QGC controls (`QGCButton`, `QGCLabel`, etc.) +- Use `qsTr()` for all user-visible strings +- Use function syntax for Connections: `function onSignal() { }` + +### Common Pitfalls +1. Assuming single vehicle - always null-check `activeVehicle()` +2. Accessing Facts before `parametersReady` signal +3. Using `Q_ASSERT` in production code +4. Hardcoded sizes/colors in QML diff --git a/justfile b/justfile new file mode 100644 index 000000000000..ee33590642ef --- /dev/null +++ b/justfile @@ -0,0 +1,50 @@ +# QGroundControl Development Commands +# Wrapper for Makefile - install: cargo install just, brew install just, or apt install just + +# Default: show available commands +default: + @just --list + +# Show Makefile help +help: + @make help + +# Initialize git submodules +submodules: + make submodules + +# Configure CMake build +configure: + make configure + +# Build the project +build: + make build + +# Run unit tests +test: + make test + +# Clean build directory +clean: + make clean + +# Run pre-commit checks +lint: + make lint + +# Format C++ code +format: + make format + +# Build with Docker (Ubuntu) +docker: + make docker + +# Full rebuild +rebuild: clean configure build + +# Override build type: just release-build +release-build: + make configure BUILD_TYPE=Release + make build BUILD_TYPE=Release diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 000000000000..0dcc292371d6 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/pyrightconfig.json", + "include": [ + "tools/**/*.py", + "test/**/*.py" + ], + "exclude": [ + "build", + "**/__pycache__" + ], + "typeCheckingMode": "basic", + "pythonVersion": "3.10", + "reportMissingImports": "warning", + "reportMissingTypeStubs": false, + "reportUnusedImport": "warning", + "reportUnusedVariable": "warning" +} diff --git a/tools/HeaderUpdater.py b/tools/HeaderUpdater.py index 856a741e2b93..48f7b592a86d 100644 --- a/tools/HeaderUpdater.py +++ b/tools/HeaderUpdater.py @@ -1,11 +1,27 @@ -import os +#!/usr/bin/env python3 +""" +QGroundControl License Header Updater + +Updates or validates license headers in source files. + +Usage: + python HeaderUpdater.py # Update headers in src/ + python HeaderUpdater.py --check # Check headers without modifying (CI mode) + python HeaderUpdater.py --check src/ test/ # Check specific directories + python HeaderUpdater.py src/ test/ # Update specific directories +""" + +import argparse import datetime +import os import re +import sys +from pathlib import Path -# Define the file extensions you want to target -TARGET_EXTENSIONS = [".cpp", ".cc", ".h", ".qml"] # Add other extensions as needed +# File extensions to process +TARGET_EXTENSIONS = {".cpp", ".cc", ".h", ".qml"} -# Define the new header with a placeholder for the updated year +# License header template HEADER_TEMPLATE = """/**************************************************************************** * * (c) 2009-{year} QGROUNDCONTROL PROJECT @@ -16,45 +32,199 @@ ****************************************************************************/ """ +# Pattern to match QGC license header (with flexible year format) +HEADER_PATTERN = re.compile(r"\(c\)\s*(2009-\d{4}|\d{4})\s+QGROUNDCONTROL") + +# Directories to skip +SKIP_DIRS = { + "build", + "libs", + "deploy", + ".git", + "node_modules", + "__pycache__", + "cpm_modules", +} + + +def find_source_files(directories: list[str]) -> list[Path]: + """Find all source files in the given directories.""" + files = [] + for directory in directories: + path = Path(directory) + if not path.exists(): + print(f"Warning: Directory not found: {directory}", file=sys.stderr) + continue -def update_header_in_file(file_path, header_template): - # Get the current year + for root, dirs, filenames in os.walk(path): + # Skip certain directories + dirs[:] = [d for d in dirs if d not in SKIP_DIRS] + + for filename in filenames: + if any(filename.endswith(ext) for ext in TARGET_EXTENSIONS): + files.append(Path(root) / filename) + + return sorted(files) + + +def check_header(file_path: Path) -> tuple[bool, str]: + """ + Check if a file has a valid QGC license header. + + Returns: + (is_valid, message) + """ + try: + content = file_path.read_text(encoding="utf-8", errors="replace") + except Exception as e: + return False, f"Error reading file: {e}" + + # Check if header exists + match = HEADER_PATTERN.search(content[:1000]) # Only check first 1000 chars + + if not match: + return False, "Missing QGC license header" + + # Check if year is current current_year = datetime.datetime.now().year - new_header = header_template.format(year=current_year) + year_match = match.group(1) + + if year_match == str(current_year): + return True, "Header OK (single year)" + elif year_match == f"2009-{current_year}": + return True, "Header OK" + elif year_match.startswith("2009-"): + return ( + False, + f"Header year outdated: {year_match} (should be 2009-{current_year})", + ) + else: + return False, f"Header year format incorrect: {year_match}" + + +def update_header(file_path: Path, current_year: int) -> tuple[bool, str]: + """ + Update the license header in a file. - with open(file_path, "r") as file: - content = file.read() + Returns: + (was_modified, message) + """ + try: + content = file_path.read_text(encoding="utf-8", errors="replace") + except Exception as e: + return False, f"Error reading file: {e}" - # Regex pattern to match both "2009-XXXX" and just "XXXX" - header_pattern = re.compile(r"\(c\)\s*(2009-\d{4}|\d{4}) QGROUNDCONTROL") + original_content = content - # Check if the header already exists - match = header_pattern.search(content) + # Check if header exists + match = HEADER_PATTERN.search(content) if match: - # Update the header to include "2009-current_year" if it is in the wrong format - updated_content = header_pattern.sub( - f"(c) 2009-{current_year} QGROUNDCONTROL", content - ) + # Update existing header + content = HEADER_PATTERN.sub(f"(c) 2009-{current_year} QGROUNDCONTROL", content) else: - # Prepend the new header if it's missing - updated_content = new_header + "\n" + content + # Prepend new header + new_header = HEADER_TEMPLATE.format(year=current_year) + content = new_header + "\n" + content + + if content != original_content: + try: + file_path.write_text(content, encoding="utf-8") + return True, "Header updated" + except Exception as e: + return False, f"Error writing file: {e}" + + return False, "Header already up to date" + + +def main(): + parser = argparse.ArgumentParser( + description="Update or check QGC license headers in source files.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "directories", + nargs="*", + default=["src"], + help="Directories to process (default: src)", + ) + parser.add_argument( + "--check", + action="store_true", + help="Check headers without modifying files (exit 1 if issues found)", + ) + parser.add_argument( + "--verbose", + "-v", + action="store_true", + help="Show all files, not just those with issues", + ) + + args = parser.parse_args() + + # Find repo root (look for COPYING.md) + script_dir = Path(__file__).parent + repo_root = script_dir.parent - with open(file_path, "w") as file: - file.write(updated_content) + if not (repo_root / "COPYING.md").exists(): + print("Warning: COPYING.md not found in repo root", file=sys.stderr) + # Resolve directories relative to repo root + directories = [ + str(repo_root / d) if not os.path.isabs(d) else d for d in args.directories + ] -def process_directory(directory): - for root, _, files in os.walk(directory): - for file in files: - if any(file.endswith(ext) for ext in TARGET_EXTENSIONS): - file_path = os.path.join(root, file) - update_header_in_file(file_path, HEADER_TEMPLATE) + files = find_source_files(directories) + current_year = datetime.datetime.now().year + + if not files: + print("No source files found.") + return 0 + + print(f"Processing {len(files)} files...") + + issues = [] + updated = [] + + for file_path in files: + rel_path = ( + file_path.relative_to(repo_root) + if file_path.is_relative_to(repo_root) + else file_path + ) + + if args.check: + is_valid, message = check_header(file_path) + if not is_valid: + issues.append((rel_path, message)) + print(f" FAIL: {rel_path}: {message}") + elif args.verbose: + print(f" OK: {rel_path}") + else: + was_modified, message = update_header(file_path, current_year) + if was_modified: + updated.append(rel_path) + print(f" Updated: {rel_path}") + elif args.verbose: + print(f" Skipped: {rel_path}: {message}") + + print() + + if args.check: + if issues: + print(f"Found {len(issues)} file(s) with header issues.") + return 1 + else: + print("All headers are valid.") + return 0 + else: + if updated: + print(f"Updated {len(updated)} file(s).") + else: + print("No files needed updating.") + return 0 if __name__ == "__main__": - current_directory = os.path.dirname(os.path.abspath(__file__)) - source_directory = current_directory + "/../src" - print(source_directory) - process_directory(source_directory) - print("Headers updated.") + sys.exit(main()) diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 000000000000..d8a5c2921234 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,311 @@ +# QGroundControl Tools + +This directory contains development tools, scripts, and configuration files for QGroundControl. + +## Directory Structure + +``` +tools/ +├── analyze.sh # Static analysis (clang-tidy, cppcheck) +├── check-deps.sh # Check for outdated dependencies +├── clean.sh # Clean build artifacts and caches +├── coverage.sh # Code coverage reports +├── format-check.sh # Check/apply clang-format +├── generate-docs.sh # Generate API docs (Doxygen) +├── param-docs.py # Generate parameter documentation +├── profile.sh # Profiling (valgrind, perf) +├── ccache.conf # ccache configuration +├── HeaderUpdater.py # License header management +├── qt6.natvis # Visual Studio debugger visualizers +├── coding-style/ # Code style examples +├── gdb-pretty-printers/ # GDB/LLDB Qt type formatters +├── log-analyzer/ # QGC log analysis tools +├── mock-mavlink/ # MAVLink vehicle simulator +├── setup/ # Environment setup scripts +└── translations/ # Translation tools +``` + +## Quick Start + +```bash +# Format changed files +./tools/format-check.sh + +# Run static analysis +./tools/analyze.sh + +# Clean build +./tools/clean.sh +``` + +## Development Scripts + +### format-check.sh + +Check or apply clang-format to source files. + +```bash +./tools/format-check.sh # Format changed files (vs master) +./tools/format-check.sh --check # Check only (for CI) +./tools/format-check.sh --all # Format all source files +./tools/format-check.sh src/Vehicle/ # Format specific directory +``` + +### analyze.sh + +Run static analysis on source code. + +```bash +./tools/analyze.sh # Analyze changed files +./tools/analyze.sh --all # Analyze all files +./tools/analyze.sh --tool cppcheck # Use cppcheck instead of clang-tidy +./tools/analyze.sh src/Vehicle/ # Analyze specific directory +``` + +### clean.sh + +Clean build artifacts and caches. + +```bash +./tools/clean.sh # Clean build directory +./tools/clean.sh --all # Clean everything (build, caches) +./tools/clean.sh --cache # Clean only caches +./tools/clean.sh --dry-run # Show what would be removed +``` + +### coverage.sh + +Generate code coverage reports. + +```bash +./tools/coverage.sh # Build with coverage, run tests, generate report +./tools/coverage.sh --report # Generate report only (after tests) +./tools/coverage.sh --open # Generate and open in browser +./tools/coverage.sh --clean # Clean coverage data +``` + +Requires: `lcov`, `genhtml` + +### profile.sh + +Profile QGC for performance and memory issues. + +```bash +./tools/profile.sh # CPU profiling with perf +./tools/profile.sh --memcheck # Memory leak detection (valgrind) +./tools/profile.sh --callgrind # CPU profiling (valgrind) +./tools/profile.sh --massif # Heap profiling (valgrind) +./tools/profile.sh --heaptrack # Heap profiling (heaptrack) +./tools/profile.sh --sanitize # Build with AddressSanitizer +``` + +### check-deps.sh + +Check for outdated dependencies and submodules. + +```bash +./tools/check-deps.sh # Check all dependencies +./tools/check-deps.sh --submodules # Check git submodules only +./tools/check-deps.sh --qt # Check Qt version +./tools/check-deps.sh --update # Update submodules to latest +``` + +### generate-docs.sh + +Generate API documentation using Doxygen. + +```bash +./tools/generate-docs.sh # Generate HTML docs +./tools/generate-docs.sh --open # Generate and open in browser +./tools/generate-docs.sh --pdf # Generate PDF (requires LaTeX) +./tools/generate-docs.sh --clean # Clean generated docs +``` + +Requires: `doxygen`, `graphviz` + +### param-docs.py + +Generate parameter documentation from FactMetaData JSON files. + +```bash +./tools/param-docs.py # Generate markdown +./tools/param-docs.py --format html # Generate HTML +./tools/param-docs.py --format json # Generate JSON +./tools/param-docs.py --group "Battery" # Filter by group +./tools/param-docs.py --output params.md # Custom output file +``` + +## Setup Scripts + +Scripts in `setup/` help configure development environments. They read configuration from `.github/build-config.json` for consistent versioning. + +| Script | Platform | Description | +|--------|----------|-------------| +| `install-dependencies-debian.sh` | Linux | Install build dependencies via apt | +| `install-dependencies-osx.sh` | macOS | Install dependencies via Homebrew + GStreamer | +| `install-dependencies-windows.ps1` | Windows | Install GStreamer (Vulkan SDK optional) | +| `install-qt-debian.sh` | Linux | Install Qt via aqtinstall | +| `install-qt-macos.sh` | macOS | Install Qt via aqtinstall | +| `install-qt-windows.ps1` | Windows | Install Qt via aqtinstall | +| `build-gstreamer.sh` | Linux | Build GStreamer from source (optional) | +| `read-config.sh` | All | Helper to read `.github/build-config.json` | + +### Usage Examples + +```bash +# Linux: Install all dependencies +sudo ./tools/setup/install-dependencies-debian.sh +./tools/setup/install-qt-debian.sh + +# macOS: Install all dependencies +./tools/setup/install-dependencies-osx.sh +./tools/setup/install-qt-macos.sh + +# Windows (PowerShell as Admin): +.\tools\setup\install-dependencies-windows.ps1 +.\tools\setup\install-qt-windows.ps1 + +# Build GStreamer from source (Linux, optional) +./tools/setup/build-gstreamer.sh -p /opt/gstreamer +``` + +## Debugging Tools + +### gdb-pretty-printers/ + +GDB pretty printers for Qt 6 types. Makes debugging Qt containers and strings readable. + +```bash +# In GDB: +source tools/gdb-pretty-printers/qt6.py + +# Then: +(gdb) print myQString +$1 = "Hello, World!" +``` + +See [gdb-pretty-printers/README.md](gdb-pretty-printers/README.md) for setup instructions. + +### qt6.natvis + +Visual Studio debugger visualizers for Qt6 types. Automatically loaded by VS when debugging. + +## Testing Tools + +### mock-mavlink/ + +Simple MAVLink vehicle simulator for testing QGC without hardware. + +```bash +# Install dependency +pip install pymavlink + +# Run mock vehicle (QGC connects to UDP 14550) +./tools/mock-mavlink/mock_vehicle.py + +# Multiple vehicles +./tools/mock-mavlink/mock_vehicle.py --system-id 1 --port 14550 & +./tools/mock-mavlink/mock_vehicle.py --system-id 2 --port 14551 & +``` + +See [mock-mavlink/README.md](mock-mavlink/README.md) for details. + +### log-analyzer/ + +Analyze QGC application logs and telemetry files. + +```bash +# Analyze application log +./tools/log-analyzer/analyze_log.py ~/.local/share/QGroundControl/Logs/QGCConsole.log + +# Show only errors +./tools/log-analyzer/analyze_log.py --errors QGCConsole.log + +# Analyze MAVLink telemetry log +./tools/log-analyzer/analyze_log.py flight.tlog + +# Show statistics +./tools/log-analyzer/analyze_log.py --stats QGCConsole.log + +# Filter by component +./tools/log-analyzer/analyze_log.py --component Vehicle QGCConsole.log +``` + +See [log-analyzer/README.md](log-analyzer/README.md) for details. + +## Translation Tools + +Scripts in `translations/` manage internationalization. + +| Script | Description | +|--------|-------------| +| `qgc-lupdate.sh` | Update Qt translation files (runs lupdate + JSON extractor) | +| `qgc-lupdate-json.py` | Extract translatable strings from JSON files | + +```bash +# From repository root: +source tools/translations/qgc-lupdate.sh + +# Or run JSON extractor directly: +python3 tools/translations/qgc-lupdate-json.py --verbose +``` + +See [translations/README.md](translations/README.md) for Crowdin integration. + +## Code Quality Tools + +### HeaderUpdater.py + +Updates or validates license headers in source files. + +```bash +python3 tools/HeaderUpdater.py # Update headers +python3 tools/HeaderUpdater.py --check # Check only (for CI) +python3 tools/HeaderUpdater.py --check src/ # Check specific directory +``` + +### ccache.conf + +Configuration for [ccache](https://ccache.dev/) to speed up rebuilds. CMake automatically uses this when ccache is available. + +```bash +# Manual use: +export CCACHE_CONFIGPATH=/path/to/qgroundcontrol/tools/ccache.conf +``` + +### coding-style/ + +Example files demonstrating QGC coding conventions: + +- `CodingStyle.h` - Header file conventions +- `CodingStyle.cc` - Implementation file conventions +- `CodingStyle.qml` - QML file conventions + +See [CODING_STYLE.md](../CODING_STYLE.md) for the full style guide. + +## VS Code Integration + +The repository includes VS Code configuration in `.vscode/`: + +- **settings.json** - Editor settings, CMake integration +- **extensions.json** - Recommended extensions +- **tasks.json** - Build, test, format tasks +- **launch.json** - Debug configurations + +Open the repository in VS Code and install recommended extensions for the best experience. + +## Centralized Configuration + +Version numbers and build settings are centralized in `.github/build-config.json`: + +```json +{ + "qt_version": "6.10.1", + "qt_modules": "qtcharts qtlocation ...", + "gstreamer_version": "1.24.12", + "ndk_version": "r27c", + ... +} +``` + +Scripts read from this file to ensure consistent versions across local development and CI. diff --git a/tools/analyze.sh b/tools/analyze.sh new file mode 100755 index 000000000000..222317071f62 --- /dev/null +++ b/tools/analyze.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# +# Run static analysis on QGroundControl source code +# +# Usage: +# ./tools/analyze.sh # Analyze changed files (vs master) +# ./tools/analyze.sh --all # Analyze all source files +# ./tools/analyze.sh src/Vehicle/ # Analyze specific directory +# ./tools/analyze.sh --tool clang-tidy # Use specific tool +# +# Tools: +# clang-tidy - Clang static analyzer (default, requires compile_commands.json) +# cppcheck - Cppcheck static analyzer + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Defaults +TOOL="clang-tidy" +ANALYZE_ALL=false +TARGET_PATH="" +BUILD_DIR="$REPO_ROOT/build" +COMPILE_COMMANDS="$BUILD_DIR/compile_commands.json" + +show_help() { + head -15 "$0" | tail -13 + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -a|--all) + ANALYZE_ALL=true + shift + ;; + -t|--tool) + TOOL="$2" + shift 2 + ;; + -b|--build-dir) + BUILD_DIR="$2" + COMPILE_COMMANDS="$BUILD_DIR/compile_commands.json" + shift 2 + ;; + *) + TARGET_PATH="$1" + shift + ;; + esac +done + +# Get files to analyze +get_files() { + if [[ -n "$TARGET_PATH" ]]; then + find "$REPO_ROOT/$TARGET_PATH" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) 2>/dev/null + elif [[ "$ANALYZE_ALL" == true ]]; then + find "$REPO_ROOT/src" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) + else + # Only changed files vs master + git -C "$REPO_ROOT" diff --name-only master... -- '*.cc' '*.cpp' '*.h' '*.hpp' 2>/dev/null | \ + xargs -I{} echo "$REPO_ROOT/{}" | \ + while read -r f; do [[ -f "$f" ]] && echo "$f"; done + fi +} + +run_clang_tidy() { + if [[ ! -f "$COMPILE_COMMANDS" ]]; then + log_error "compile_commands.json not found at $COMPILE_COMMANDS" + log_info "Run: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON" + exit 1 + fi + + if ! command -v clang-tidy &> /dev/null; then + log_error "clang-tidy not found. Install with: sudo apt install clang-tidy" + exit 1 + fi + + local files + files=$(get_files) + + if [[ -z "$files" ]]; then + log_info "No files to analyze" + exit 0 + fi + + local file_count + file_count=$(echo "$files" | wc -l) + log_info "Running clang-tidy on $file_count files..." + + local exit_code=0 + echo "$files" | while read -r file; do + echo -n " Analyzing: ${file#"$REPO_ROOT"/}... " + if clang-tidy -p "$BUILD_DIR" "$file" 2>/dev/null; then + echo -e "${GREEN}OK${NC}" + else + echo -e "${RED}ISSUES${NC}" + exit_code=1 + fi + done + + return $exit_code +} + +run_cppcheck() { + if ! command -v cppcheck &> /dev/null; then + log_error "cppcheck not found. Install with: sudo apt install cppcheck" + exit 1 + fi + + local files + files=$(get_files) + + if [[ -z "$files" ]]; then + log_info "No files to analyze" + exit 0 + fi + + local file_count + file_count=$(echo "$files" | wc -l) + log_info "Running cppcheck on $file_count files..." + + # Create file list + local filelist + filelist=$(mktemp) + echo "$files" > "$filelist" + + cppcheck \ + --enable=warning,style,performance,portability \ + --std=c++20 \ + --suppress=missingIncludeSystem \ + --suppress=unmatchedSuppression \ + --inline-suppr \ + --file-list="$filelist" \ + --error-exitcode=1 \ + 2>&1 + + local exit_code=$? + rm -f "$filelist" + return $exit_code +} + +# Main +cd "$REPO_ROOT" + +case "$TOOL" in + clang-tidy) + run_clang_tidy + ;; + cppcheck) + run_cppcheck + ;; + *) + log_error "Unknown tool: $TOOL" + log_info "Available tools: clang-tidy, cppcheck" + exit 1 + ;; +esac + +log_ok "Static analysis complete" diff --git a/tools/ccache.conf b/tools/ccache.conf index e00b8710c6aa..30c1c95ae41a 100644 --- a/tools/ccache.conf +++ b/tools/ccache.conf @@ -1,2 +1,22 @@ +# ccache configuration for QGroundControl +# +# Note: CMake automatically configures ccache when building QGC. +# This file documents the settings and can be used standalone: +# export CCACHE_CONFIGPATH=/path/to/qgroundcontrol/tools/ccache.conf +# +# CMake sets CCACHE_DIR to .cache/ccache in the project root. + +# Maximum cache size +max_size = 2G + +# Cache directory (when using this config standalone) +# cache_dir = /path/to/qgroundcontrol/.cache/ccache + +# Compression level (1-19, higher = smaller but slower) compression_level = 5 + +# Allow ccache to work with precompiled headers and time-based macros sloppiness = pch_defines,time_macros,include_file_mtime,include_file_ctime + +# Useful for debugging cache misses (uncomment to enable) +# log_file = /tmp/ccache.log diff --git a/tools/check-deps.sh b/tools/check-deps.sh new file mode 100755 index 000000000000..d04e629170a1 --- /dev/null +++ b/tools/check-deps.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# +# Check for outdated dependencies and submodules +# +# Usage: +# ./tools/check-deps.sh # Check all dependencies +# ./tools/check-deps.sh --submodules # Check only git submodules +# ./tools/check-deps.sh --qt # Check Qt version +# ./tools/check-deps.sh --update # Update submodules to latest +# +# Checks: +# - Git submodules vs upstream +# - Qt version vs latest +# - GStreamer version vs latest +# - Python dependencies + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Defaults +CHECK_ALL=true +CHECK_SUBMODULES=false +CHECK_QT=false +UPDATE_DEPS=false + +show_help() { + head -15 "$0" | tail -13 + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + --submodules) + CHECK_ALL=false + CHECK_SUBMODULES=true + shift + ;; + --qt) + CHECK_ALL=false + CHECK_QT=true + shift + ;; + --update) + UPDATE_DEPS=true + shift + ;; + *) + log_error "Unknown option: $1" + exit 1 + ;; + esac +done + +check_submodules() { + log_info "Checking git submodules..." + + cd "$REPO_ROOT" + + # Initialize submodules if needed + git submodule update --init --recursive 2>/dev/null || true + + local outdated=0 + + while IFS= read -r line; do + if [[ -z "$line" ]]; then + continue + fi + + # Parse submodule status (status and hash reserved for future use) + local _status="${line:0:1}" + local _hash="${line:1:40}" + local path + path=$(echo "$line" | awk '{print $2}') + + if [[ ! -d "$REPO_ROOT/$path" ]]; then + continue + fi + + cd "$REPO_ROOT/$path" + + # Get current and remote info (reserved for future verbose output) + local _current_hash _branch + _current_hash=$(git rev-parse HEAD 2>/dev/null || echo "unknown") + _branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "detached") + + # Fetch updates quietly + git fetch --quiet 2>/dev/null || true + + # Check if behind + local behind=0 + # shellcheck disable=SC1083 # @{u} is valid git syntax for upstream + if git rev-parse '@{u}' &>/dev/null; then + behind=$(git rev-list --count 'HEAD..@{u}' 2>/dev/null || echo "0") + fi + + if [[ "$behind" -gt 0 ]]; then + log_warn "$path: $behind commits behind upstream" + ((outdated++)) || true + else + echo -e " ${GREEN}✓${NC} $path (up to date)" + fi + + cd "$REPO_ROOT" + done < <(git submodule status --recursive 2>/dev/null) + + if [[ "$outdated" -eq 0 ]]; then + log_ok "All submodules up to date" + else + log_warn "$outdated submodule(s) have updates available" + if [[ "$UPDATE_DEPS" == true ]]; then + log_info "Updating submodules..." + git submodule update --remote --merge + log_ok "Submodules updated" + else + log_info "Run with --update to update submodules" + fi + fi +} + +check_qt_version() { + log_info "Checking Qt version..." + + # Read current version from config + local config_file="$REPO_ROOT/.github/build-config.json" + if [[ ! -f "$config_file" ]]; then + log_warn "build-config.json not found" + return + fi + + local current_version + current_version=$(python3 -c "import json; print(json.load(open('$config_file')).get('qt_version', 'unknown'))") + + echo " Current: Qt $current_version" + + # Check latest Qt version (from qt.io) + local latest_info + if command -v curl &> /dev/null; then + # Try to get latest version info + latest_info=$(curl -s "https://download.qt.io/official_releases/qt/" 2>/dev/null | \ + grep -oP '6\.\d+' | sort -V | tail -1 || echo "") + + if [[ -n "$latest_info" ]]; then + echo " Latest minor: Qt $latest_info.x" + + local current_minor="${current_version%.*}" + if [[ "$current_minor" != "$latest_info" ]]; then + log_warn "Newer Qt minor version available: $latest_info" + else + log_ok "Using latest Qt minor version" + fi + fi + fi + + # Check installed Qt + if command -v qmake &> /dev/null; then + local installed + installed=$(qmake --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' || echo "not found") + echo " Installed: Qt $installed" + elif [[ -n "${QT_ROOT_DIR:-}" ]]; then + echo " QT_ROOT_DIR: $QT_ROOT_DIR" + fi +} + +check_gstreamer_version() { + log_info "Checking GStreamer version..." + + # Read current version from config + local config_file="$REPO_ROOT/.github/build-config.json" + if [[ ! -f "$config_file" ]]; then + return + fi + + local current_version + current_version=$(python3 -c "import json; print(json.load(open('$config_file')).get('gstreamer_version', 'unknown'))") + + echo " Configured: GStreamer $current_version" + + # Check installed version + if command -v gst-launch-1.0 &> /dev/null; then + local installed + installed=$(gst-launch-1.0 --version 2>/dev/null | head -1 | grep -oP '\d+\.\d+\.\d+' || echo "not found") + echo " Installed: GStreamer $installed" + fi +} + +check_python_deps() { + log_info "Checking Python dependencies..." + + local req_files=("requirements.txt" "docs/requirements.txt") + + for req in "${req_files[@]}"; do + if [[ -f "$REPO_ROOT/$req" ]]; then + echo " Checking $req..." + if command -v pip &> /dev/null; then + # Check for outdated packages + local outdated + outdated=$(pip list --outdated --format=columns 2>/dev/null | tail -n +3 || echo "") + if [[ -n "$outdated" ]]; then + echo "$outdated" | while read -r line; do + echo " $line" + done + fi + fi + fi + done +} + +check_build_tools() { + log_info "Checking build tools..." + + local tools=("cmake" "ninja" "ccache" "clang-format" "clang-tidy") + + for tool in "${tools[@]}"; do + if command -v "$tool" &> /dev/null; then + local version + version=$("$tool" --version 2>/dev/null | head -1 || echo "unknown") + echo -e " ${GREEN}✓${NC} $tool: $version" + else + echo -e " ${YELLOW}✗${NC} $tool: not installed" + fi + done +} + +# Main +cd "$REPO_ROOT" + +if [[ "$CHECK_ALL" == true ]]; then + check_submodules + echo "" + check_qt_version + echo "" + check_gstreamer_version + echo "" + check_build_tools +elif [[ "$CHECK_SUBMODULES" == true ]]; then + check_submodules +elif [[ "$CHECK_QT" == true ]]; then + check_qt_version +fi + +echo "" +log_ok "Dependency check complete" diff --git a/tools/clean.sh b/tools/clean.sh new file mode 100755 index 000000000000..bf0a789b727d --- /dev/null +++ b/tools/clean.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +# +# Clean build artifacts and caches +# +# Usage: +# ./tools/clean.sh # Clean build directory +# ./tools/clean.sh --all # Clean everything (build, caches, generated files) +# ./tools/clean.sh --cache # Clean only caches (ccache, pip, etc.) +# +# This script removes: +# - build/ CMake build directory +# - .cache/ Local caches (ccache, clangd index) +# - *.user Qt Creator user files +# - CMakeUserPresets.json + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*"; } + +# Defaults +CLEAN_ALL=false +CLEAN_CACHE_ONLY=false +DRY_RUN=false + +show_help() { + head -14 "$0" | tail -12 + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -a|--all) + CLEAN_ALL=true + shift + ;; + -c|--cache) + CLEAN_CACHE_ONLY=true + shift + ;; + -n|--dry-run) + DRY_RUN=true + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +cd "$REPO_ROOT" + +remove_if_exists() { + local path="$1" + local desc="${2:-$path}" + + if [[ -e "$path" ]]; then + if [[ "$DRY_RUN" == true ]]; then + log_info "Would remove: $desc" + else + log_info "Removing: $desc" + rm -rf "$path" + fi + fi +} + +clean_build() { + remove_if_exists "build" "build directory" + remove_if_exists "CMakeUserPresets.json" "CMake user presets" + + # Qt Creator user files + find . -maxdepth 1 -name "*.user" -type f 2>/dev/null | while read -r f; do + remove_if_exists "$f" "Qt Creator user file: $f" + done + + # CMake generated files in source + find . -maxdepth 1 -name "CMakeFiles" -type d 2>/dev/null | while read -r d; do + remove_if_exists "$d" "CMake files: $d" + done +} + +clean_cache() { + remove_if_exists ".cache" "local cache directory" + + # clangd index + remove_if_exists ".clangd" "clangd index" + + # ccache stats (not the cache itself, just local stats) + if command -v ccache &> /dev/null; then + if [[ "$DRY_RUN" == true ]]; then + log_info "Would clear ccache statistics" + else + log_info "Clearing ccache statistics" + ccache --zero-stats 2>/dev/null || true + fi + fi +} + +clean_generated() { + # Compiled Python files + find . -type d -name "__pycache__" 2>/dev/null | while read -r d; do + remove_if_exists "$d" "Python cache: $d" + done + + find . -name "*.pyc" -type f 2>/dev/null | while read -r f; do + remove_if_exists "$f" "Python bytecode: $f" + done + + # Generated translation files (keep source .ts files) + # remove_if_exists "translations/*.qm" "compiled translation files" +} + +# Main +if [[ "$DRY_RUN" == true ]]; then + log_warn "Dry run mode - no files will be removed" +fi + +if [[ "$CLEAN_CACHE_ONLY" == true ]]; then + clean_cache +elif [[ "$CLEAN_ALL" == true ]]; then + clean_build + clean_cache + clean_generated +else + clean_build +fi + +log_ok "Clean complete" + +# Show disk space recovered +if [[ "$DRY_RUN" != true ]]; then + echo "" + log_info "Disk usage: $(du -sh "$REPO_ROOT" 2>/dev/null | cut -f1)" +fi diff --git a/CodingStyle.cc b/tools/coding-style/CodingStyle.cc similarity index 66% rename from CodingStyle.cc rename to tools/coding-style/CodingStyle.cc index 762d255a8745..ff6989bc8f00 100644 --- a/CodingStyle.cc +++ b/tools/coding-style/CodingStyle.cc @@ -1,6 +1,6 @@ /**************************************************************************** * - * (c) 2009-2024 QGROUNDCONTROL PROJECT + * (c) 2009-2025 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. @@ -10,10 +10,14 @@ // This is an example class c++ file which is used to describe the QGroundControl // coding style. In general almost everything in here has some coding style meaning. // Not all style choices are explained. +// +// QGroundControl requires C++20. Use modern C++ features where appropriate. #include "CodingStyle.h" -#include +#include +#include +#include #include #include @@ -162,3 +166,77 @@ void CodingStyle::_methodWithManyArguments( // This makes it clear the parameter is intentionally unused // Implementation here... } + +// ============================================================================= +// C++20 Features Examples +// ============================================================================= + +bool CodingStyle::validateInput(std::string_view input) const +{ + // C++20: std::string_view avoids allocations for read-only string operations + // Use when interfacing with non-Qt code or performance-critical paths + if (input.empty()) { + return false; + } + + // C++20: Use std::ranges algorithms for cleaner code + return std::ranges::all_of(input, [](char c) { + return std::isalnum(static_cast(c)) || c == '_'; + }); +} + +void CodingStyle::processData(std::span data) +{ + // C++20: std::span provides safe, bounds-checked view of contiguous data + // Replaces (int* ptr, size_t size) parameter pairs + if (data.empty()) { + qCDebug(CodingStyleLog) << "No data to process"; + return; + } + + // C++20: Range-based for with init-statement + for (int sum = 0; const int value : data) { + sum += value; + qCDebug(CodingStyleLog) << "Running sum:" << sum; + } + + // C++20: Use ranges for transformations + // Example: filter and transform in a pipeline + auto positiveDoubled = data + | std::views::filter([](int n) { return n > 0; }) + | std::views::transform([](int n) { return n * 2; }); + + for (const int value : positiveDoubled) { + qCDebug(CodingStyleLog) << "Positive doubled:" << value; + } +} + +// C++20: Use designated initializers for aggregate types (defined in header or locally) +namespace { + struct ConfigOptions { + int timeout = 30; + bool enabled = true; + int retryCount = 3; + }; + + void exampleDesignatedInitializers() + { + // C++20: Designated initializers make struct initialization clear + const ConfigOptions config{ + .timeout = 60, + .enabled = true, + .retryCount = 5 + }; + Q_UNUSED(config); + } + + // C++20: Concepts can constrain template parameters (use sparingly, prefer concrete types) + template + concept Numeric = std::integral || std::floating_point; + + template + T clampValue(T value, T min, T max) + { + return std::clamp(value, min, max); + } +} // anonymous namespace diff --git a/CodingStyle.h b/tools/coding-style/CodingStyle.h similarity index 84% rename from CodingStyle.h rename to tools/coding-style/CodingStyle.h index c06255c0340e..dbd816443426 100644 --- a/CodingStyle.h +++ b/tools/coding-style/CodingStyle.h @@ -1,6 +1,6 @@ /**************************************************************************** * - * (c) 2009-2024 QGROUNDCONTROL PROJECT + * (c) 2009-2025 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. @@ -10,10 +10,14 @@ // This is an example class header file which is used to describe the QGroundControl // coding style. In general almost everything in here has some coding style meaning. // Not all style choices are explained. +// +// QGroundControl requires C++20. Use modern C++ features where appropriate. #pragma once #include +#include +#include #include #include @@ -68,14 +72,21 @@ class CodingStyle : public QObject /// Document public methods which are non-obvious in the header file /// Use Q_INVOKABLE for methods callable from QML - Q_INVOKABLE bool publicMethod1(); + /// Use [[nodiscard]] for functions whose return value should not be ignored + [[nodiscard]] Q_INVOKABLE bool publicMethod1(); Q_INVOKABLE void performAction(const QString& param); - // Public getters/setters - int exampleProperty() const { return _exampleProperty; } + // C++20: Use std::string_view for read-only string parameters when Qt types aren't needed + [[nodiscard]] bool validateInput(std::string_view input) const; + + // C++20: Use std::span for array-like parameters instead of pointer + size + void processData(std::span data); + + // Public getters/setters - use [[nodiscard]] for getters + [[nodiscard]] int exampleProperty() const { return _exampleProperty; } void setExampleProperty(int value); - Vehicle* vehicle() const { return _vehicle; } - bool readOnlyProperty() const { return _readOnlyProperty; } + [[nodiscard]] Vehicle* vehicle() const { return _vehicle; } + [[nodiscard]] bool readOnlyProperty() const { return _readOnlyProperty; } signals: /// Document signals which are non-obvious in the header file diff --git a/CodingStyle.qml b/tools/coding-style/CodingStyle.qml similarity index 99% rename from CodingStyle.qml rename to tools/coding-style/CodingStyle.qml index cb34b1402888..8f904b88b894 100644 --- a/CodingStyle.qml +++ b/tools/coding-style/CodingStyle.qml @@ -1,6 +1,6 @@ /**************************************************************************** * - * (c) 2009-2024 QGROUNDCONTROL PROJECT + * (c) 2009-2025 QGROUNDCONTROL PROJECT * * QGroundControl is licensed according to the terms in the file * COPYING.md in the root of the source code directory. diff --git a/tools/coverage.sh b/tools/coverage.sh new file mode 100755 index 000000000000..48ef3549ccc0 --- /dev/null +++ b/tools/coverage.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +# +# Generate code coverage reports for QGroundControl +# +# Usage: +# ./tools/coverage.sh # Build with coverage and run tests +# ./tools/coverage.sh --report # Generate HTML report only (after tests) +# ./tools/coverage.sh --open # Generate and open report in browser +# ./tools/coverage.sh --clean # Clean coverage data +# +# Requirements: +# - gcov (from GCC) or llvm-cov +# - lcov and genhtml (for HTML reports) +# - Optional: gcovr (for Cobertura XML output) +# +# The script configures CMake with coverage flags, runs tests, and generates reports. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Defaults +BUILD_DIR="$REPO_ROOT/build-coverage" +COVERAGE_DIR="$REPO_ROOT/coverage" +REPORT_ONLY=false +OPEN_REPORT=false +CLEAN_ONLY=false +TEST_FILTER="" + +show_help() { + head -16 "$0" | tail -14 + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -r|--report) + REPORT_ONLY=true + shift + ;; + -o|--open) + OPEN_REPORT=true + shift + ;; + -c|--clean) + CLEAN_ONLY=true + shift + ;; + -t|--test) + TEST_FILTER="$2" + shift 2 + ;; + -b|--build-dir) + BUILD_DIR="$2" + shift 2 + ;; + *) + log_error "Unknown option: $1" + exit 1 + ;; + esac +done + +check_dependencies() { + local missing=() + + if ! command -v lcov &> /dev/null; then + missing+=("lcov") + fi + if ! command -v genhtml &> /dev/null; then + missing+=("genhtml (part of lcov)") + fi + + if [[ ${#missing[@]} -gt 0 ]]; then + log_error "Missing dependencies: ${missing[*]}" + log_info "Install with: sudo apt install lcov" + exit 1 + fi +} + +clean_coverage() { + log_info "Cleaning coverage data..." + rm -rf "$BUILD_DIR" + rm -rf "$COVERAGE_DIR" + find "$REPO_ROOT" -name "*.gcda" -delete 2>/dev/null || true + find "$REPO_ROOT" -name "*.gcno" -delete 2>/dev/null || true + log_ok "Coverage data cleaned" +} + +configure_build() { + log_info "Configuring build with coverage..." + + cmake -B "$BUILD_DIR" -S "$REPO_ROOT" \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_FLAGS="--coverage -fprofile-arcs -ftest-coverage" \ + -DCMAKE_CXX_FLAGS="--coverage -fprofile-arcs -ftest-coverage" \ + -DCMAKE_EXE_LINKER_FLAGS="--coverage" \ + -DQGC_BUILD_TESTING=ON \ + -G Ninja + + log_ok "Build configured" +} + +build_project() { + log_info "Building project..." + cmake --build "$BUILD_DIR" --parallel + log_ok "Build complete" +} + +run_tests() { + log_info "Running tests..." + + local test_args="--unittest" + if [[ -n "$TEST_FILTER" ]]; then + test_args="--unittest:$TEST_FILTER" + fi + + # Run tests (continue even if some fail) + # shellcheck disable=SC2086 # Intentional word splitting for test args + "$BUILD_DIR/QGroundControl" $test_args || true + + log_ok "Tests complete" +} + +generate_report() { + log_info "Generating coverage report..." + + mkdir -p "$COVERAGE_DIR" + + # Capture coverage data + lcov --capture \ + --directory "$BUILD_DIR" \ + --output-file "$COVERAGE_DIR/coverage.info" \ + --ignore-errors mismatch \ + --rc lcov_branch_coverage=1 + + # Remove system headers and test files from coverage + lcov --remove "$COVERAGE_DIR/coverage.info" \ + '/usr/*' \ + '*/build/*' \ + '*/test/*' \ + '*/libs/*' \ + '*/_deps/*' \ + '*/Qt/*' \ + --output-file "$COVERAGE_DIR/coverage.filtered.info" \ + --rc lcov_branch_coverage=1 + + # Generate HTML report + genhtml "$COVERAGE_DIR/coverage.filtered.info" \ + --output-directory "$COVERAGE_DIR/html" \ + --title "QGroundControl Coverage Report" \ + --legend \ + --branch-coverage \ + --highlight + + # Print summary + echo "" + log_ok "Coverage report generated: $COVERAGE_DIR/html/index.html" + + # Show summary statistics + lcov --summary "$COVERAGE_DIR/coverage.filtered.info" 2>&1 | grep -E "(lines|functions|branches)" +} + +open_report() { + local report="$COVERAGE_DIR/html/index.html" + if [[ ! -f "$report" ]]; then + log_error "Report not found. Run coverage first." + exit 1 + fi + + log_info "Opening coverage report..." + + if command -v xdg-open &> /dev/null; then + xdg-open "$report" + elif command -v open &> /dev/null; then + open "$report" + else + log_warn "Could not open browser. Report at: $report" + fi +} + +# Main +cd "$REPO_ROOT" +check_dependencies + +if [[ "$CLEAN_ONLY" == true ]]; then + clean_coverage + exit 0 +fi + +if [[ "$REPORT_ONLY" == true ]]; then + generate_report + if [[ "$OPEN_REPORT" == true ]]; then + open_report + fi + exit 0 +fi + +# Full coverage workflow +configure_build +build_project +run_tests +generate_report + +if [[ "$OPEN_REPORT" == true ]]; then + open_report +fi + +log_ok "Coverage complete!" diff --git a/tools/format-check.sh b/tools/format-check.sh new file mode 100755 index 000000000000..3dcbac0310bf --- /dev/null +++ b/tools/format-check.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# Check or apply clang-format to source files +# +# Usage: +# ./tools/format-check.sh # Format changed files (vs master) +# ./tools/format-check.sh --check # Check only, don't modify (for CI) +# ./tools/format-check.sh --all # Format all source files +# ./tools/format-check.sh src/Vehicle/ # Format specific directory +# +# Requires: clang-format (version 17+ recommended) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Defaults +CHECK_ONLY=false +FORMAT_ALL=false +TARGET_PATH="" + +show_help() { + head -12 "$0" | tail -10 + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -c|--check) + CHECK_ONLY=true + shift + ;; + -a|--all) + FORMAT_ALL=true + shift + ;; + *) + TARGET_PATH="$1" + shift + ;; + esac +done + +# Check for clang-format +if ! command -v clang-format &> /dev/null; then + log_error "clang-format not found" + log_info "Install with: sudo apt install clang-format" + exit 1 +fi + +CLANG_FORMAT_VERSION=$(clang-format --version | grep -oP '\d+' | head -1) +log_info "Using clang-format version $CLANG_FORMAT_VERSION" + +# Get files to format +get_files() { + if [[ -n "$TARGET_PATH" ]]; then + find "$REPO_ROOT/$TARGET_PATH" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) 2>/dev/null + elif [[ "$FORMAT_ALL" == true ]]; then + find "$REPO_ROOT/src" "$REPO_ROOT/test" -type f \( -name "*.cc" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) 2>/dev/null + else + # Only changed files vs master + git -C "$REPO_ROOT" diff --name-only master... -- '*.cc' '*.cpp' '*.h' '*.hpp' 2>/dev/null | \ + xargs -I{} echo "$REPO_ROOT/{}" | \ + while read -r f; do [[ -f "$f" ]] && echo "$f"; done + fi +} + +cd "$REPO_ROOT" + +files=$(get_files) + +if [[ -z "$files" ]]; then + log_info "No files to format" + exit 0 +fi + +file_count=$(echo "$files" | wc -l) +log_info "Processing $file_count files..." + +if [[ "$CHECK_ONLY" == true ]]; then + # Check mode - verify formatting without modifying + needs_format=() + + while read -r file; do + if ! clang-format --dry-run --Werror "$file" 2>/dev/null; then + needs_format+=("${file#"$REPO_ROOT"/}") + fi + done <<< "$files" + + if [[ ${#needs_format[@]} -gt 0 ]]; then + log_error "The following files need formatting:" + for f in "${needs_format[@]}"; do + echo " $f" + done + echo "" + log_info "Run: ./tools/format-check.sh" + exit 1 + fi + + log_ok "All files properly formatted" +else + # Format mode - modify files in place + formatted=0 + + while read -r file; do + if clang-format -i "$file"; then + ((formatted++)) + fi + done <<< "$files" + + log_ok "Formatted $formatted files" + + # Show what changed + if git -C "$REPO_ROOT" diff --quiet; then + log_info "No formatting changes needed" + else + log_info "Files modified:" + git -C "$REPO_ROOT" diff --name-only + fi +fi diff --git a/tools/gdb-pretty-printers/README.md b/tools/gdb-pretty-printers/README.md new file mode 100644 index 000000000000..8a61036d71da --- /dev/null +++ b/tools/gdb-pretty-printers/README.md @@ -0,0 +1,65 @@ +# GDB Pretty Printers for Qt 6 + +This directory contains GDB pretty printers for displaying Qt types in a human-readable format. + +## Setup + +### Quick Setup (per session) + +In GDB: +```gdb +source /path/to/qgroundcontrol/tools/gdb-pretty-printers/qt6.py +``` + +### Permanent Setup + +Add to your `~/.gdbinit`: +```gdb +python +import sys +sys.path.insert(0, '/path/to/qgroundcontrol/tools/gdb-pretty-printers') +from qt6 import register_qt_printers +register_qt_printers(None) +end +``` + +### VS Code Setup + +The `.vscode/launch.json` in this repository is already configured to load these printers automatically. + +## Supported Types + +- **Strings**: QString, QByteArray +- **Containers**: QList, QVector, QMap, QHash +- **Geometry**: QPoint, QPointF, QSize, QSizeF, QRect, QRectF +- **Other**: QVariant, QUrl, QSharedPointer + +## Example Output + +``` +(gdb) print myString +$1 = "Hello, World!" + +(gdb) print myList +$2 = QList (size=3) + [0] = 1 + [1] = 2 + [2] = 3 + +(gdb) print myPoint +$3 = (100, 200) + +(gdb) print myRect +$4 = [(0, 0) 800x600] +``` + +## LLDB Support + +For LLDB (macOS), Qt provides built-in formatters. You can also use: + +```bash +# In ~/.lldbinit +command script import /path/to/qt6/plugins/lldb/qt6lldb.py +``` + +Or check if your Qt installation includes LLDB formatters in `$QT_ROOT/plugins/lldb/`. diff --git a/tools/gdb-pretty-printers/qt6.py b/tools/gdb-pretty-printers/qt6.py new file mode 100644 index 000000000000..7818b295ae44 --- /dev/null +++ b/tools/gdb-pretty-printers/qt6.py @@ -0,0 +1,357 @@ +""" +GDB Pretty Printers for Qt 6 Types + +This module provides human-readable display of Qt types in GDB. + +Usage in GDB: + source /path/to/qgroundcontrol/tools/gdb-pretty-printers/qt6.py + +Usage in .gdbinit: + python + import sys + sys.path.insert(0, '/path/to/qgroundcontrol/tools/gdb-pretty-printers') + from qt6 import register_qt_printers + register_qt_printers(None) + end + +Based on KDE's Qt pretty printers, adapted for Qt 6. +""" + +import gdb + + +def _qstring_to_str(val): + """Convert a QString to a Python string.""" + try: + d = val["d"] + if d == 0: + return '""' + + # Qt 6 QString internal structure + size = int(d["size"]) + if size == 0: + return '""' + + # Data is stored inline after QStringData header in Qt 6 + data_ptr = d["ptr"] + if data_ptr == 0: + return '""' + + # Read UTF-16 data + inferior = gdb.selected_inferior() + mem = inferior.read_memory(int(data_ptr), size * 2) + return '"' + mem.tobytes().decode("utf-16-le", errors="replace") + '"' + except Exception as e: + return f"" + + +def _qbytearray_to_str(val): + """Convert a QByteArray to a Python string.""" + try: + d = val["d"] + if d == 0: + return '""' + + size = int(d["size"]) + if size == 0: + return '""' + + data_ptr = d["ptr"] + if data_ptr == 0: + return '""' + + inferior = gdb.selected_inferior() + mem = inferior.read_memory(int(data_ptr), size) + return '"' + mem.tobytes().decode("utf-8", errors="replace") + '"' + except Exception as e: + return f"" + + +class QStringPrinter: + """Pretty printer for QString.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + return _qstring_to_str(self.val) + + def display_hint(self): + return "string" + + +class QByteArrayPrinter: + """Pretty printer for QByteArray.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + return _qbytearray_to_str(self.val) + + def display_hint(self): + return "string" + + +class QListPrinter: + """Pretty printer for QList.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + d = self.val["d"] + size = int(d["size"]) + return f"QList<...> (size={size})" + except Exception: + return "QList<...>" + + def children(self): + try: + d = self.val["d"] + size = int(d["size"]) + ptr = d["ptr"] + + # Get the element type + val_type = self.val.type.template_argument(0) + + for i in range(min(size, 100)): # Limit to 100 elements + element = (ptr + i).dereference().cast(val_type) + yield f"[{i}]", element + except Exception: + pass + + def display_hint(self): + return "array" + + +class QVectorPrinter(QListPrinter): + """Pretty printer for QVector (alias of QList in Qt 6).""" + + pass + + +class QMapPrinter: + """Pretty printer for QMap.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + d = self.val["d"] + if d == 0: + return "QMap (empty)" + size = int(d["size"]) + return f"QMap (size={size})" + except Exception: + return "QMap" + + def display_hint(self): + return "map" + + +class QHashPrinter: + """Pretty printer for QHash.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + d = self.val["d"] + if d == 0: + return "QHash (empty)" + size = int(d["size"]) + return f"QHash (size={size})" + except Exception: + return "QHash" + + def display_hint(self): + return "map" + + +class QVariantPrinter: + """Pretty printer for QVariant.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + d = self.val["d"] + type_id = int(d["typeId"]) + + # Common type IDs in Qt 6 + type_names = { + 0: "Invalid", + 1: "Bool", + 2: "Int", + 3: "UInt", + 4: "LongLong", + 5: "ULongLong", + 6: "Double", + 7: "Char", + 10: "QString", + 11: "QStringList", + 12: "QByteArray", + 13: "QBitArray", + 14: "QDate", + 15: "QTime", + 16: "QDateTime", + 17: "QUrl", + } + + type_name = type_names.get(type_id, f"type={type_id}") + return f"QVariant({type_name})" + except Exception: + return "QVariant" + + +class QPointPrinter: + """Pretty printer for QPoint/QPointF.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + x = self.val["xp"] + y = self.val["yp"] + return f"({x}, {y})" + except Exception: + return "QPoint" + + +class QSizePrinter: + """Pretty printer for QSize/QSizeF.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + w = self.val["wd"] + h = self.val["ht"] + return f"{w}x{h}" + except Exception: + return "QSize" + + +class QRectPrinter: + """Pretty printer for QRect/QRectF.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + x1 = self.val["x1"] + y1 = self.val["y1"] + x2 = self.val["x2"] + y2 = self.val["y2"] + return f"[({x1}, {y1}) - ({x2}, {y2})]" + except Exception: + try: + x = self.val["xp"] + y = self.val["yp"] + w = self.val["w"] + h = self.val["h"] + return f"[({x}, {y}) {w}x{h}]" + except Exception: + return "QRect" + + +class QUrlPrinter: + """Pretty printer for QUrl.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + d = self.val["d"] + if d == 0: + return "QUrl(empty)" + + # Try to get the full URL string + scheme = d["scheme"] + host = d["host"] + path = d["path"] + + parts = [] + if scheme: + parts.append(_qstring_to_str(scheme).strip('"')) + if host: + parts.append("://" + _qstring_to_str(host).strip('"')) + if path: + parts.append(_qstring_to_str(path).strip('"')) + + return f"QUrl({(''.join(parts))})" + except Exception: + return "QUrl" + + +class QSharedPointerPrinter: + """Pretty printer for QSharedPointer.""" + + def __init__(self, val): + self.val = val + + def to_string(self): + try: + d = self.val["d"] + value = self.val["value"] + + if value == 0: + return "QSharedPointer(nullptr)" + + if d != 0: + strong = int(d["strongref"]["_q_value"]) + weak = int(d["weakref"]["_q_value"]) + return f"QSharedPointer(strong={strong}, weak={weak}) = {value}" + + return f"QSharedPointer = {value}" + except Exception: + return "QSharedPointer" + + +def build_qt_printer(): + """Build and return the Qt pretty printer collection.""" + pp = gdb.printing.RegexpCollectionPrettyPrinter("Qt6") + + # Core types + pp.add_printer("QString", "^QString$", QStringPrinter) + pp.add_printer("QByteArray", "^QByteArray$", QByteArrayPrinter) + pp.add_printer("QVariant", "^QVariant$", QVariantPrinter) + pp.add_printer("QUrl", "^QUrl$", QUrlPrinter) + + # Containers + pp.add_printer("QList", "^QList<.*>$", QListPrinter) + pp.add_printer("QVector", "^QVector<.*>$", QVectorPrinter) + pp.add_printer("QMap", "^QMap<.*>$", QMapPrinter) + pp.add_printer("QHash", "^QHash<.*>$", QHashPrinter) + + # Geometry + pp.add_printer("QPoint", "^QPoint$", QPointPrinter) + pp.add_printer("QPointF", "^QPointF$", QPointPrinter) + pp.add_printer("QSize", "^QSize$", QSizePrinter) + pp.add_printer("QSizeF", "^QSizeF$", QSizePrinter) + pp.add_printer("QRect", "^QRect$", QRectPrinter) + pp.add_printer("QRectF", "^QRectF$", QRectPrinter) + + # Smart pointers + pp.add_printer("QSharedPointer", "^QSharedPointer<.*>$", QSharedPointerPrinter) + + return pp + + +def register_qt_printers(obj): + """Register Qt pretty printers with GDB.""" + gdb.printing.register_pretty_printer(obj, build_qt_printer(), replace=True) + + +# Auto-register when loaded +register_qt_printers(None) +print("Qt 6 pretty printers loaded") diff --git a/tools/generate-docs.sh b/tools/generate-docs.sh new file mode 100755 index 000000000000..a879d9f8493a --- /dev/null +++ b/tools/generate-docs.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# +# Generate API documentation using Doxygen +# +# Usage: +# ./tools/generate-docs.sh # Generate HTML docs +# ./tools/generate-docs.sh --open # Generate and open in browser +# ./tools/generate-docs.sh --pdf # Generate PDF (requires LaTeX) +# ./tools/generate-docs.sh --clean # Clean generated docs +# +# Requirements: +# - doxygen +# - graphviz (for diagrams) +# - Optional: texlive (for PDF output) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } +log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Defaults +OUTPUT_DIR="$REPO_ROOT/docs/api" +DOXYFILE="$REPO_ROOT/Doxyfile" +GENERATE_PDF=false +OPEN_DOCS=false +CLEAN_ONLY=false + +show_help() { + head -14 "$0" | tail -12 + exit 0 +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + show_help + ;; + -o|--open) + OPEN_DOCS=true + shift + ;; + --pdf) + GENERATE_PDF=true + shift + ;; + -c|--clean) + CLEAN_ONLY=true + shift + ;; + *) + log_error "Unknown option: $1" + exit 1 + ;; + esac +done + +check_dependencies() { + if ! command -v doxygen &> /dev/null; then + log_error "doxygen not found. Install with: sudo apt install doxygen" + exit 1 + fi + + if ! command -v dot &> /dev/null; then + log_warn "graphviz not found. Diagrams will be disabled." + log_info "Install with: sudo apt install graphviz" + fi + + if [[ "$GENERATE_PDF" == true ]] && ! command -v pdflatex &> /dev/null; then + log_error "pdflatex not found. Install with: sudo apt install texlive-latex-base" + exit 1 + fi +} + +clean_docs() { + log_info "Cleaning generated documentation..." + rm -rf "$OUTPUT_DIR" + log_ok "Documentation cleaned" +} + +generate_doxyfile() { + log_info "Generating Doxyfile..." + + cat > "$DOXYFILE" << 'DOXYFILE_CONTENT' +# Doxyfile for QGroundControl + +PROJECT_NAME = "QGroundControl" +PROJECT_BRIEF = "Ground Control Station for MAVLink Drones" +PROJECT_NUMBER = +OUTPUT_DIRECTORY = docs/api + +# Input +INPUT = src +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h *.cc *.cpp *.hpp *.qml *.md +RECURSIVE = YES +EXCLUDE_PATTERNS = */test/* */libs/* */_deps/* */build/* + +# Output formats +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_LATEX = NO +GENERATE_XML = NO + +# HTML settings +HTML_COLORSTYLE_HUE = 220 +HTML_COLORSTYLE_SAT = 100 +HTML_COLORSTYLE_GAMMA = 80 +HTML_DYNAMIC_SECTIONS = YES +DISABLE_INDEX = NO +GENERATE_TREEVIEW = YES +FULL_SIDEBAR = YES + +# Extraction settings +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = YES +EXTRACT_LOCAL_CLASSES = YES + +# Source browser +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +STRIP_CODE_COMMENTS = YES +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES + +# Diagrams (requires graphviz) +HAVE_DOT = YES +DOT_NUM_THREADS = 0 +CLASS_DIAGRAMS = YES +COLLABORATION_GRAPH = YES +GROUP_GRAPHS = YES +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +GRAPHICAL_HIERARCHY = YES +DIRECTORY_GRAPH = YES +DOT_IMAGE_FORMAT = svg +INTERACTIVE_SVG = YES + +# Preprocessing +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = NO +PREDEFINED = Q_OBJECT Q_PROPERTY Q_INVOKABLE Q_SIGNAL Q_SLOT + +# Warnings +QUIET = NO +WARNINGS = YES +WARN_IF_UNDOCUMENTED = NO +WARN_IF_DOC_ERROR = YES + +# Other +ALPHABETICAL_INDEX = YES +GENERATE_TODOLIST = YES +GENERATE_BUGLIST = YES +SHOW_USED_FILES = YES +SHOW_FILES = YES +SHOW_NAMESPACES = YES +DOXYFILE_CONTENT + + log_ok "Doxyfile generated" +} + +generate_docs() { + log_info "Generating documentation..." + + mkdir -p "$OUTPUT_DIR" + + # Generate Doxyfile if it doesn't exist + if [[ ! -f "$DOXYFILE" ]]; then + generate_doxyfile + fi + + # Enable LaTeX if PDF requested + if [[ "$GENERATE_PDF" == true ]]; then + sed -i 's/GENERATE_LATEX.*= NO/GENERATE_LATEX = YES/' "$DOXYFILE" + fi + + # Run doxygen + cd "$REPO_ROOT" + doxygen "$DOXYFILE" + + log_ok "Documentation generated: $OUTPUT_DIR/html/index.html" + + # Generate PDF if requested + if [[ "$GENERATE_PDF" == true ]]; then + log_info "Generating PDF..." + cd "$OUTPUT_DIR/latex" + make pdf + log_ok "PDF generated: $OUTPUT_DIR/latex/refman.pdf" + fi +} + +open_docs() { + local index="$OUTPUT_DIR/html/index.html" + if [[ ! -f "$index" ]]; then + log_error "Documentation not found. Generate first." + exit 1 + fi + + log_info "Opening documentation..." + + if command -v xdg-open &> /dev/null; then + xdg-open "$index" + elif command -v open &> /dev/null; then + open "$index" + else + log_warn "Could not open browser. Docs at: $index" + fi +} + +# Main +cd "$REPO_ROOT" + +if [[ "$CLEAN_ONLY" == true ]]; then + clean_docs + exit 0 +fi + +check_dependencies +generate_docs + +if [[ "$OPEN_DOCS" == true ]]; then + open_docs +fi diff --git a/tools/log-analyzer/README.md b/tools/log-analyzer/README.md new file mode 100644 index 000000000000..85332858de13 --- /dev/null +++ b/tools/log-analyzer/README.md @@ -0,0 +1,118 @@ +# QGC Log Analyzer + +Tools for analyzing QGroundControl logs and telemetry data. + +## Installation + +```bash +# For basic log analysis +# No dependencies required + +# For MAVLink telemetry logs (.tlog) +pip install pymavlink +``` + +## Usage + +### Analyze Application Logs + +```bash +# Show all entries +./analyze_log.py ~/.local/share/QGroundControl/Logs/QGCConsole.log + +# Show only errors +./analyze_log.py --errors QGCConsole.log + +# Show errors and warnings +./analyze_log.py --warnings QGCConsole.log + +# Filter by component +./analyze_log.py --component Vehicle QGCConsole.log + +# Filter by message pattern +./analyze_log.py --message "parameter" QGCConsole.log + +# Show statistics +./analyze_log.py --stats QGCConsole.log + +# Show timeline +./analyze_log.py --timeline QGCConsole.log +``` + +### Analyze Telemetry Logs + +```bash +# Analyze MAVLink telemetry log +./analyze_log.py flight.tlog + +# Filter by MAVLink message type +./analyze_log.py --message "HEARTBEAT" flight.tlog + +# Show statistics +./analyze_log.py --stats flight.tlog +``` + +## Log Locations + +| Platform | Application Logs | +|----------|-----------------| +| Linux | `~/.local/share/QGroundControl/Logs/` | +| macOS | `~/Library/Application Support/QGroundControl/Logs/` | +| Windows | `%APPDATA%\QGroundControl\Logs\` | + +Telemetry logs (`.tlog`) are saved in the same directory. + +## Examples + +### Find Connection Issues + +```bash +./analyze_log.py --message "connect|disconnect|timeout" QGCConsole.log +``` + +### Find Parameter Errors + +```bash +./analyze_log.py --component parameter --errors QGCConsole.log +``` + +### Analyze Flight Session + +```bash +# Get overview +./analyze_log.py --stats flight.tlog + +# Find GPS issues +./analyze_log.py --message "GPS|satellite" --warnings QGCConsole.log +``` + +## Output Formats + +### Default Output + +``` +[12:34:56.789] [INFO ] [qgc.vehicle] Connected to vehicle 1 +[12:34:57.123] [WARN ] [qgc.param] Parameter timeout +[12:34:58.456] [ERROR] [qgc.link] Connection lost +``` + +### Statistics Output + +``` +===================================== +LOG STATISTICS +===================================== + +Total entries: 1234 + +By level: + DEBUG: 500 + INFO: 600 + WARN: 100 + ERROR: 34 + +Top components: + qgc.vehicle: 300 + qgc.mavlink: 250 + qgc.param: 150 +``` diff --git a/tools/log-analyzer/analyze_log.py b/tools/log-analyzer/analyze_log.py new file mode 100755 index 000000000000..1f3f689ffb47 --- /dev/null +++ b/tools/log-analyzer/analyze_log.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +""" +QGroundControl Log Analyzer + +Analyzes QGC application logs and telemetry logs for debugging and diagnostics. + +Usage: + ./analyze_log.py # Analyze a log file + ./analyze_log.py --errors # Show only errors + ./analyze_log.py --warnings # Show errors and warnings + ./analyze_log.py --component Vehicle # Filter by component + ./analyze_log.py --timeline # Show timeline of events + ./analyze_log.py --stats # Show statistics + +Supports: + - QGC application logs + - MAVLink telemetry logs (.tlog) + - Console output logs + +Requirements: + pip install pymavlink # For .tlog files +""" + +import argparse +import os +import re +import sys +from collections import Counter, defaultdict +from datetime import datetime +from pathlib import Path + +# Try to import pymavlink for tlog support +try: + from pymavlink import mavutil + + HAS_PYMAVLINK = True +except ImportError: + HAS_PYMAVLINK = False + + +class LogEntry: + """Represents a single log entry.""" + + def __init__( + self, timestamp=None, level=None, component=None, message=None, raw=None + ): + self.timestamp = timestamp + self.level = level or "INFO" + self.component = component or "unknown" + self.message = message or "" + self.raw = raw or "" + + def __str__(self): + ts = ( + self.timestamp.strftime("%H:%M:%S.%f")[:-3] + if self.timestamp + else "??:??:??" + ) + return f"[{ts}] [{self.level:5}] [{self.component}] {self.message}" + + +class QGCLogParser: + """Parser for QGC application logs.""" + + # Common QGC log patterns + PATTERNS = [ + # Qt debug format: "qgc.component: message" + re.compile(r"^(?Pqgc\.[a-z.]+):\s*(?P.*)$", re.IGNORECASE), + # Timestamped format: "[HH:MM:SS.mmm] message" + re.compile(r"^\[(?P