Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions .github/actions/install-dependencies/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,49 @@ runs:
shell: bash
run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" print-packages

- name: Normalize Ubuntu apt mirrors (Linux ARM64)
if: runner.os == 'Linux' && runner.arch == 'ARM64'
shell: bash
run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" normalize-apt-sources

- name: Enable universe repository (Linux)
if: runner.os == 'Linux'
shell: bash
run: python3 "${GITHUB_WORKSPACE}/.github/scripts/install_dependencies_helper.py" enable-universe

- name: Install apt packages (Linux)
if: runner.os == 'Linux'
# Retry: a slow apt mirror can stall the download past the job timeout; fast-fail and retry.
uses: nick-fields/retry@v4
shell: bash
env:
APT_PACKAGES: ${{ steps.linux-deps.outputs.packages }}
with:
timeout_minutes: 20
max_attempts: 3
shell: bash
# Acquire::*::Timeout aborts a stalled mirror connection so retries can fire.
command: |
APT_OPTS="-o Acquire::Retries=3 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30"
# shellcheck disable=SC2086
sudo apt-get $APT_OPTS update -qq
# shellcheck disable=SC2086
sudo apt-get $APT_OPTS install -y --no-install-recommends $APT_PACKAGES
run: |
# shellcheck disable=SC2153 # Supplied through the step environment.
read -r -a apt_packages <<< "$APT_PACKAGES"

for attempt in 1 2 3; do
# Run timeout as root so it can terminate the privileged apt process tree.
if sudo timeout --signal=TERM --kill-after=30s 20m bash -c '
set -euo pipefail
apt_opts=(
-o Acquire::Retries=3
-o Acquire::ForceIPv4=true
-o Acquire::http::Timeout=30
-o Acquire::https::Timeout=30
)
apt-get "${apt_opts[@]}" update -qq
apt-get "${apt_opts[@]}" install -y --no-install-recommends "$@"
' -- "${apt_packages[@]}"; then
exit 0
fi

if [[ "$attempt" -eq 3 ]]; then
echo "::error::apt package installation failed after $attempt attempts"
exit 1
fi

echo "::warning::apt package installation attempt $attempt failed; retrying in 10 seconds"
sleep 10
done

- name: Fix apt alternatives (Linux)
if: runner.os == 'Linux'
Expand Down Expand Up @@ -134,4 +155,3 @@ runs:
max_attempts: 3
command: |
python3 tools/setup/install_dependencies --platform macos

64 changes: 64 additions & 0 deletions .github/scripts/install_dependencies_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Post-install fixups for CI dependency caching on Linux.

Handles:
- Normalizing official Ubuntu apt mirrors to reliable HTTPS endpoints
- Enabling the Ubuntu universe repository
- Repairing apt alternatives after cache-apt-pkgs-action restore
- Installing optional packages
Expand All @@ -23,6 +24,10 @@
from common.proc import run_captured

APT_OPTS = ["-o", "DPkg::Lock::Timeout=300", "-o", "Acquire::Retries=3"]
UBUNTU_APT_URL_PATTERN = re.compile(
r"http://(?P<host>[a-z0-9.-]+\.ubuntu\.com)(?P<suffix>(?::\d+)?(?:/[^\s#]*)?)",
re.IGNORECASE,
)


def _sudo(cmd: list[str], *, check: bool = True) -> subprocess.CompletedProcess:
Expand All @@ -40,6 +45,63 @@ def _get_multiarch() -> str:
return result.stdout.strip() if result.returncode == 0 else ""


def _normalize_ubuntu_apt_urls(text: str) -> str:
"""Use canonical HTTPS endpoints for official Ubuntu apt repositories."""

def replace_url(match: re.Match[str]) -> str:
host = match.group("host").lower()
suffix = match.group("suffix")

if host.endswith(".ec2.ports.ubuntu.com"):
host = "ports.ubuntu.com"
elif host.endswith(".ec2.archive.ubuntu.com"):
host = "archive.ubuntu.com"
elif host not in {"archive.ubuntu.com", "ports.ubuntu.com", "security.ubuntu.com"}:
return match.group(0)

return f"https://{host}{suffix}"

return UBUNTU_APT_URL_PATTERN.sub(replace_url, text)


def normalize_apt_sources(apt_root: Path = Path("/etc/apt")) -> None:
"""Normalize Ubuntu URLs in legacy and deb822 apt source files."""
sources = [apt_root / "sources.list"]
sources_dir = apt_root / "sources.list.d"
if sources_dir.is_dir():
sources.extend(sorted(sources_dir.glob("*.list")))
sources.extend(sorted(sources_dir.glob("*.sources")))

changed_sources = 0
for source in sources:
if not source.is_file():
continue

try:
original = source.read_text(errors="replace")
except OSError as error:
gh_warning(f"Unable to read apt source {source}: {error}")
continue

normalized = _normalize_ubuntu_apt_urls(original)
if normalized == original:
continue

try:
source.write_text(normalized)
except PermissionError:
subprocess.run(
["sudo", "tee", str(source)],
check=True,
input=normalized,
stdout=subprocess.DEVNULL,
text=True,
)
changed_sources += 1

print(f"Normalized {changed_sources} apt source file(s)")


def enable_universe() -> None:
"""Enable the Ubuntu universe repository if not already present."""
sources_dirs = [Path("/etc/apt/sources.list"), Path("/etc/apt/sources.list.d")]
Expand Down Expand Up @@ -150,6 +212,7 @@ def main() -> None:

parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("normalize-apt-sources")
sub.add_parser("enable-universe")
sub.add_parser("fix-apt-alternatives")
sub.add_parser("install-optional")
Expand All @@ -158,6 +221,7 @@ def main() -> None:

args = parser.parse_args()
commands = {
"normalize-apt-sources": normalize_apt_sources,
"enable-universe": enable_universe,
"fix-apt-alternatives": fix_apt_alternatives,
"install-optional": install_optional_packages,
Expand Down
55 changes: 55 additions & 0 deletions .github/scripts/tests/test_install_dependencies_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,61 @@
import install_dependencies_helper


class TestNormalizeAptSources:
"""Tests for Ubuntu apt mirror normalization."""

def test_normalizes_legacy_ec2_ports_mirror(self):
"""Remaps the ARM64 EC2 mirror to the canonical HTTPS endpoint."""
source = "deb http://us-west-2.ec2.ports.ubuntu.com/ubuntu-ports noble main universe\n"

assert install_dependencies_helper._normalize_ubuntu_apt_urls(source) == (
"deb https://ports.ubuntu.com/ubuntu-ports noble main universe\n"
)

def test_normalizes_deb822_official_mirrors(self):
"""Normalizes official archive and security URLs in deb822 sources."""
source = (
"Types: deb\n"
"URIs: http://archive.ubuntu.com/ubuntu http://security.ubuntu.com/ubuntu\n"
"Suites: noble noble-updates noble-security\n"
)

assert install_dependencies_helper._normalize_ubuntu_apt_urls(source) == (
"Types: deb\n"
"URIs: https://archive.ubuntu.com/ubuntu https://security.ubuntu.com/ubuntu\n"
"Suites: noble noble-updates noble-security\n"
)

def test_preserves_third_party_and_unknown_ubuntu_mirrors(self):
"""Does not rewrite repositories whose HTTPS support is unknown."""
source = (
"deb http://packages.example.com/ubuntu noble main\n"
"deb http://azure.archive.ubuntu.com/ubuntu noble main\n"
)

assert install_dependencies_helper._normalize_ubuntu_apt_urls(source) == source

def test_updates_legacy_and_deb822_files(self, tmp_path, capsys):
"""Discovers and updates both supported apt source file formats."""
sources_dir = tmp_path / "sources.list.d"
sources_dir.mkdir()
legacy_source = tmp_path / "sources.list"
deb822_source = sources_dir / "ubuntu.sources"
ignored_source = sources_dir / "ignored.conf"
legacy_source.write_text("deb http://ports.ubuntu.com/ubuntu-ports noble main\n")
deb822_source.write_text("URIs: http://eu-west-1.ec2.archive.ubuntu.com/ubuntu\n")
ignored_source.write_text("deb http://archive.ubuntu.com/ubuntu noble main\n")

install_dependencies_helper.normalize_apt_sources(tmp_path)

assert legacy_source.read_text() == (
"deb https://ports.ubuntu.com/ubuntu-ports noble main\n"
)
assert deb822_source.read_text() == "URIs: https://archive.ubuntu.com/ubuntu\n"
assert ignored_source.read_text() == "deb http://archive.ubuntu.com/ubuntu noble main\n"
assert capsys.readouterr().out == "Normalized 2 apt source file(s)\n"


class TestDetectPythonVersion:
"""Tests for detect_python_version function."""

Expand Down
11 changes: 1 addition & 10 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ find_package(Qt6
Core
Gui
HttpServer
LabsStyleKit
LinguistTools
Location
LocationPrivate
Expand Down Expand Up @@ -373,16 +374,6 @@ qt_add_translations(${CMAKE_PROJECT_NAME}
MERGE_QT_TRANSLATIONS
)

# ----------------------------------------------------------------------------
# Qt Quick Controls Configuration
# ----------------------------------------------------------------------------
qgc_set_qt_resource_alias("${CMAKE_SOURCE_DIR}/resources/qtquickcontrols2.conf")

qt_add_resources(${CMAKE_PROJECT_NAME} "qgcresources_cmake"
PREFIX "/"
FILES "${CMAKE_SOURCE_DIR}/resources/qtquickcontrols2.conf"
)

# ----------------------------------------------------------------------------
# Qt Plugin Configuration
# ----------------------------------------------------------------------------
Expand Down
10 changes: 8 additions & 2 deletions deploy/docker/install-sysroot-aarch64.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ set -euo pipefail

SYSROOT="${SYSROOT:-/opt/sysroot}"
UBUNTU_SUITE="${UBUNTU_SUITE:-noble}"
MIRROR="${MIRROR:-http://ports.ubuntu.com/ubuntu-ports}"
MIRROR="${MIRROR:-https://ports.ubuntu.com/ubuntu-ports}"

# Reject newlines/junk in overrides — they land verbatim in the apt sources heredoc.
if [[ ! "$UBUNTU_SUITE" =~ ^[a-z][-a-z0-9]*$ ]]; then
Expand Down Expand Up @@ -96,7 +96,13 @@ EOF
}

configure_apt
apt_retry apt-get -o Acquire::Retries=3 update -y --quiet
# apt-get update normally exits successfully when an index download fails. Make
# partial updates fatal so apt_retry can recover instead of continuing with no
# arm64 package metadata and producing an empty dependency closure.
apt_retry apt-get \
-o APT::Update::Error-Mode=any \
-o Acquire::Retries=3 \
update -y --quiet

# Target-side libraries are single-sourced from install_dependencies
# (DEBIAN_PACKAGES["cross_arm64"]) so they can't drift from the native build;
Expand Down
2 changes: 0 additions & 2 deletions resources/qtquickcontrols2.conf

This file was deleted.

2 changes: 1 addition & 1 deletion src/AppSettings/LinkConfigurationManager.qml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ SettingsGroupLayout {
buttonText: qsTr("Add")

onClicked: {
var editingConfig = _linkManager.createConfiguration(ScreenTools.isSerialAvailable ? LinkConfiguration.TypeSerial : LinkConfiguration.TypeUdp, "")
const editingConfig = _root._linkManager.createConfiguration()
linkDialogFactory.open({ editingConfig: editingConfig, originalConfig: null })
}
Comment on lines 94 to 97
}
Expand Down
1 change: 0 additions & 1 deletion src/AppSettings/QmlTest.qml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import QtQuick.Layouts

import QGroundControl
import QGroundControl.Controls

Rectangle {
id: _root
anchors.fill: parent
Expand Down
12 changes: 11 additions & 1 deletion src/AppSettings/pages/General.SettingsUI.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"groups": [
{
"heading": "General",
"keywords": ["language", "locale", "color scheme", "dark mode", "theme", "palette", "follow me", "audio", "volume", "sound", "gstreamer", "debug level", "ui scale", "font size", "zoom", "save path", "storage", "reset", "clear settings", "factory reset"],
"keywords": ["language", "locale", "color scheme", "dark mode", "theme", "palette", "follow me", "audio", "volume", "sound", "gstreamer", "debug level", "ui scale", "font size", "zoom", "accessibility", "contrast", "motion", "touch", "pointer", "density", "save path", "storage", "reset", "clear settings", "factory reset"],
"controls": [
{
"setting": "appSettings.qLocaleLanguage",
Expand Down Expand Up @@ -40,6 +40,16 @@
"setting": "appSettings.uiScalePercent",
"control": "scaler"
},
{
"setting": "appSettings.forceHighContrast"
},
{
"setting": "appSettings.reducedMotion"
},
{
"setting": "appSettings.touchModeOverride",
"control": "combobox"
},
{
"setting": "appSettings.savePath",
"control": "browse",
Expand Down
6 changes: 3 additions & 3 deletions src/AppSettings/pages/SettingsPages.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,19 @@
"name": "Mock Link",
"qml": "MockLink.qml",
"icon": "qrc:/InstrumentValueIcons/drone.svg",
"visible": "ScreenTools.isDebug"
"visible": "QGroundControl.isDebugBuild"
},
{
"name": "Debug",
"qml": "DebugWindow.qml",
"icon": "qrc:/InstrumentValueIcons/bug.svg",
"visible": "ScreenTools.isDebug"
"visible": "QGroundControl.isDebugBuild"
},
{
"name": "Palette Test",
"qml": "QmlTest.qml",
"icon": "qrc:/InstrumentValueIcons/photo.svg",
"visible": "ScreenTools.isDebug"
"visible": "QGroundControl.isDebugBuild"
}
]
}
3 changes: 3 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
Qt6::Gui
Qt6::GuiPrivate
Qt6::HttpServer
Qt6::LabsStyleKit
Qt6::Network
Qt6::StateMachine
Qt6::Svg
Expand Down Expand Up @@ -140,6 +141,8 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
FlightMapModule
PlanViewModule
QGroundControlControlsModule
QGCStyle
QGCStyleplugin
QGroundControlModule
ToolbarModule
VehicleSetupModule
Expand Down
3 changes: 2 additions & 1 deletion src/Comms/LinkManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ class LinkManager : public QObject
void init();

/// Create/Edit Link Configuration
Q_INVOKABLE LinkConfiguration *createConfiguration(int type, const QString &name);
/// Default type is the first LinkType enum entry (serial when available, otherwise UDP)
Q_INVOKABLE LinkConfiguration *createConfiguration(int type = 0, const QString &name = QString());
Q_INVOKABLE LinkConfiguration *startConfigurationEditing(LinkConfiguration *config);
Q_INVOKABLE void cancelConfigurationEditing(LinkConfiguration *config) const { delete config; }
Q_INVOKABLE void endConfigurationEditing(LinkConfiguration *config, LinkConfiguration *editedConfig);
Expand Down
2 changes: 1 addition & 1 deletion src/FlyView/FlyViewGripperDropPanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import QGroundControl
import QGroundControl.Controls

ColumnLayout {
spacing: ScreenTools.defaultFontHeight / 2
spacing: ScreenTools.defaultFontPixelHeight / 2

property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
property var _buttonTitles: [qsTr("Release"), qsTr("Grab"), qsTr("Hold")]
Expand Down
2 changes: 1 addition & 1 deletion src/FlyView/GuidedActionsController.qml
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ Item {

property var _corePlugin: QGroundControl.corePlugin
property var _corePluginOptions: QGroundControl.corePlugin.options
property bool _guidedActionsEnabled: (!ScreenTools.isDebug && _corePluginOptions.guidedActionsRequireRCRSSI && _activeVehicle) ? _rcRSSIAvailable : _activeVehicle
property bool _guidedActionsEnabled: _activeVehicle && (!_corePluginOptions.guidedActionsRequireRCRSSI || _rcRSSIAvailable)
property string _flightMode: _activeVehicle ? _activeVehicle.flightMode : ""
property bool _missionAvailable: missionController.containsItems
property bool _missionActive: _activeVehicle ? _vehicleArmed && (_vehicleInLandMode || _vehicleInRTLMode || _vehicleInMissionMode) : false
Expand Down
Loading
Loading