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
75 changes: 52 additions & 23 deletions .github/build-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,66 @@
"android_min_sdk": "28",
"android_platform": "35",
"cmake_minimum_version": "3.25",
"gstreamer_android_version": "1.28.1",
"gstreamer_ios_version": "1.28.1",
"gstreamer_macos_version": "1.28.1",
"gstreamer_minimum_version": "1.20.0",
"gstreamer_default_version": "1.28.1",
"gstreamer_windows_version": "1.28.1",
"gstreamer": {
"version": {
"default": "1.28.3",
"minimum": "1.20.0",
"android": "1.28.3",
"ios": "1.28.3",
"macos": "1.28.3",
"windows": "1.28.3"
},
"plugins": {
"common": [
"app",
"coreelements",
"isomp4",
"libav",
"matroska",
"mpegtsdemux",
"opengl",
"openh264",
"playback",
"rtp",
"rtpmanager",
"rtsp",
"sdpelem",
"tcp",
"typefindfunctions",
"udp",
"videoparsersbad",
"vpx",
"videoconvertscale",
"videoconvert",
"videoscale"
],
"android": ["androidmedia", "dav1d"],
"apple": ["applemedia", "dav1d"],
"windows": ["d3d", "d3d11", "d3d12", "dav1d", "nvcodec"],
"linux": ["nvcodec", "qsv", "va", "vulkan"]
},
"checksums": {
"1.28.3": {
"android": "4c8932344f43735df5fcdb709f72f11af38c967a88a89e3efc8d31cbb2d7b98f",
"windows_msvc_x64": "f01b4fb30b3aefdcdf4251d4a7464fb68197d15a763a2a94b667b93f88cc6216",
"windows_msvc_arm64": "bf82db1f8320358212b73483be9a671a88f3768d471d29b5819bc9b6d64f47ab",
"macos": "d2940ed843baae348f51e6c014ef4b39502e8dc419e99aac441f5b8d96d02456",
"macos_devel": "12c01972acb5668416248606bac6148c8d6f0395969650d05a18be9a08de0260",
"ios": "e815dd14338eb01702a1d9891dd379002daf07613412f080abb0715e07bedb1b"
}
},
"ca_bundle_sha256": "86a1f3366afac7c6f8ae9f3c779ac221129328c43f0ab2b8817eb2f362a5025c"
},
"ios_deployment_target": "14.0",
"java_version": "17",
"macos_deployment_target": "13.0",
"ndk_full_version": "27.2.12479018",
"platform_workflows": "Linux,Windows,MacOS,Android",
"ndk_version": "r27c",
"platform_workflows": "Linux,Windows,MacOS,Android",
"qt_minimum_version": "6.10.0",
"qt_modules": "qtgraphs qtlocation qtpositioning qtspeech qtmultimedia qtserialport qtimageformats qtshadertools qtconnectivity qtquick3d qtsensors qtscxml qtwebsockets qthttpserver",
"qt_version": "6.10.3",
"vulkan_sdk_version": "1.4.304.1",
"xcode_ios_version": "latest-stable",
"xcode_version": "16.x",
"gstreamer_checksums": {
"1.28.1": {
"android": "7b3f7cfd289aa1ac237899220b3d8fbfa722337c23175c047e06ec881c505481",
"windows_msvc_x64": "2ec50356d2d0937a9ead0f99d322f81d8413b9514c9d58ed41ca58fbcf25bfde",
"windows_msvc_arm64": "0a1938b7a8568ee5695c4c1755743cacc4a1643538cacdfc5be3c82426c0e193",
"macos": "02803f73435daabe8fb12b79c38c6775d0efb83af001474558ba25c4f874d305",
"macos_devel": "df167b41559afbcd743276c6b068cba2ada8f5b69eb68095415a7a5a7515e52c",
"ios": "3255cd01f8c4d92322be0c5825a192b998fef05989a161dcae3cef22c517ae71",
"good_plugins": "738e26aee41b7a62050e40b81adc017a110a7f32d1ec49fa6a0300846c44368d"
},
"1.22.12": {
"android": "be92cf477d140c270b480bd8ba0e26b1e01c8db042c46b9e234d87352112e485",
"windows_msvc_x64": "e5cbc6fb9f40fc2850806163df4b9d92012f967c842dc000a2b254cbcd7901d6"
}
}
"xcode_version": "16.x"
}
84 changes: 52 additions & 32 deletions .github/build-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
"android_min_sdk",
"android_build_tools",
"java_version",
"gstreamer_default_version",
"gstreamer_minimum_version",
"gstreamer",
"platform_workflows"
],
"properties": {
Expand Down Expand Up @@ -76,39 +75,60 @@
"pattern": "^[0-9]+$",
"description": "Java version for Android builds"
},
"gstreamer_default_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"gstreamer_minimum_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"gstreamer_android_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"gstreamer_ios_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"gstreamer_macos_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"gstreamer_windows_version": {
"type": "string",
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
},
"gstreamer_checksums": {
"gstreamer": {
"type": "object",
"description": "Per-version checksums for GStreamer SDK downloads",
"patternProperties": {
"^[0-9]+\\.[0-9]+\\.[0-9]+$": {
"required": ["version"],
"additionalProperties": false,
"properties": {
"version": {
"type": "object",
"patternProperties": {
".*": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
"required": ["default", "minimum"],
"additionalProperties": false,
"properties": {
"default": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
"minimum": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
"android": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
"ios": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
"macos": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" },
"windows": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }
}
},
"plugins": {
"type": "object",
"description": "GStreamer plugin allow-list. 'common' applies to every platform; per-platform keys are appended.",
"required": ["common"],
"additionalProperties": false,
"properties": {
"common": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_]+$" } },
"android": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_]+$" } },
"apple": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_]+$" } },
"windows": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_]+$" } },
"linux": { "type": "array", "items": { "type": "string", "pattern": "^[a-z0-9_]+$" } }
}
},
"checksums": {
"type": "object",
"description": "Per-version SHA256 checksums for SDK downloads",
"patternProperties": {
"^[0-9]+\\.[0-9]+\\.[0-9]+$": {
"type": "object",
"properties": {
"android": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"ios": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"macos": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"macos_devel": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"windows_msvc_x64": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
"windows_msvc_arm64": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"ca_bundle_sha256": {
"type": "string",
"description": "SHA256 of the curl.se Mozilla CA bundle baked into the iOS app; refresh on intentional bundle bumps (https://curl.se/ca/cacert.pem.sha256)",
"pattern": "^[a-f0-9]{64}$"
}
}
},
Expand Down
186 changes: 186 additions & 0 deletions .github/scripts/mirror_gstreamer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""Mirror official upstream GStreamer release artifacts to the QGC S3 bucket.

Downloads the prebuilt SDK packages published at gstreamer.freedesktop.org for a
given version, verifies each against its upstream ``.sha256sum`` sidecar, then
uploads them to ``s3://<bucket>/dependencies/gstreamer/<platform>/<filename>`` —
the layout the build's S3 fallback mirror reads (see
``cmake/GStreamer/Download.cmake::gstreamer_get_s3_mirror_url``).

The artifact set per platform mirrors ``gstreamer_get_package_url`` exactly so the
mirror and the primary download can't drift on filename.
"""

from __future__ import annotations

import argparse
import hashlib
import sys
import urllib.request
from dataclasses import dataclass
from pathlib import Path

from ci_bootstrap import ensure_tools_dir

ensure_tools_dir(__file__)

from common.gh_actions import write_step_summary
from common.proc import run_captured

PKG_BASE = "https://gstreamer.freedesktop.org/data/pkg"
ALLOWED_BUCKETS = frozenset({"qgroundcontrol"})
PLATFORMS = ("android", "ios", "macos", "windows")
WINDOWS_MIN_VERSION = (1, 28, 0)


@dataclass(frozen=True)
class Artifact:
"""One mirrored file: its upstream URL and target S3 directory."""

url: str
s3_dir: str

@property
def filename(self) -> str:
return self.url.rsplit("/", 1)[-1]

def s3_key(self) -> str:
return f"dependencies/gstreamer/{self.s3_dir}/{self.filename}"


def _version_tuple(version: str) -> tuple[int, ...]:
return tuple(int(part) for part in version.split(".") if part.isdigit())


def artifacts_for(platform: str, version: str) -> list[Artifact]:
"""Return the upstream artifact(s) to mirror for *platform* at *version*."""
if platform == "android":
return [Artifact(f"{PKG_BASE}/android/{version}/gstreamer-1.0-android-universal-{version}.tar.xz", "android")]
if platform == "ios":
return [Artifact(f"{PKG_BASE}/ios/{version}/gstreamer-1.0-devel-{version}-ios-universal.pkg", "ios")]
if platform == "macos":
return [
Artifact(f"{PKG_BASE}/macos/{version}/gstreamer-1.0-{version}-universal.pkg", "macos"),
Artifact(f"{PKG_BASE}/macos/{version}/gstreamer-1.0-devel-{version}-universal.pkg", "macos"),
]
if platform == "windows":
if _version_tuple(version) < WINDOWS_MIN_VERSION:
raise ValueError(f"Windows GStreamer SDK requires version >= 1.28.0 (got {version!r})")
return [
Artifact(f"{PKG_BASE}/windows/{version}/msvc/gstreamer-1.0-msvc-x86_64-{version}.exe", "windows"),
Artifact(f"{PKG_BASE}/windows/{version}/msvc/gstreamer-1.0-msvc-arm64-{version}.exe", "windows"),
]
raise ValueError(f"Unknown platform: {platform!r}")


def resolve_platforms(value: str) -> list[str]:
"""Expand 'all' or a comma list into validated platform names."""
if not value or value == "all":
return list(PLATFORMS)
requested = [p.strip() for p in value.split(",") if p.strip()]
unknown = [p for p in requested if p not in PLATFORMS]
if unknown:
raise ValueError(f"Unknown platform(s): {', '.join(unknown)}; valid: {', '.join(PLATFORMS)}")
return requested


def _download(url: str, dest: Path) -> None:
with urllib.request.urlopen(url, timeout=120) as resp, dest.open("wb") as fh:
while chunk := resp.read(1 << 20):
fh.write(chunk)


def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as fh:
while chunk := fh.read(1 << 20):
digest.update(chunk)
return digest.hexdigest()


def _fetch_expected_sha(url: str) -> str:
with urllib.request.urlopen(f"{url}.sha256sum", timeout=60) as resp:
return resp.read().decode().split()[0].strip().lower()


def _s3_object_exists(bucket: str, key: str) -> bool:
result = run_captured(["aws", "s3api", "head-object", "--bucket", bucket, "--key", key])
return result.returncode == 0


def mirror_artifact(artifact: Artifact, *, bucket: str, work_dir: Path, dry_run: bool, force: bool) -> str:
"""Download, verify, and upload one artifact. Returns a status word for the summary."""
key = artifact.s3_key()
if not force and not dry_run and _s3_object_exists(bucket, key):
print(f"skip (exists): {key}")
return "skipped"

local = work_dir / artifact.filename
print(f"download: {artifact.url}")
_download(artifact.url, local)

expected = _fetch_expected_sha(artifact.url)
actual = _sha256(local)
if actual != expected:
raise RuntimeError(f"checksum mismatch for {artifact.filename}: expected {expected}, got {actual}")
print(f"verified sha256: {actual}")

if dry_run:
print(f"dry-run: would upload -> s3://{bucket}/{key}")
return "dry-run"

run_captured(
["aws", "s3", "cp", str(local), f"s3://{bucket}/{key}", "--acl", "public-read"],
check=True,
)
print(f"uploaded: s3://{bucket}/{key}")
local.unlink(missing_ok=True)
return "uploaded"


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True, help="GStreamer version to mirror (e.g. 1.28.3)")
parser.add_argument("--platforms", default="all", help="'all' or comma list: android,ios,macos,windows")
parser.add_argument("--bucket", default="qgroundcontrol", help="Target S3 bucket")
parser.add_argument("--work-dir", default=".", help="Scratch directory for downloads")
parser.add_argument("--dry-run", action="store_true", help="Download and verify but do not upload")
parser.add_argument("--force", action="store_true", help="Re-upload even if the object already exists")
return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
if args.bucket not in ALLOWED_BUCKETS:
print(f"::error::Bucket {args.bucket!r} not in allowlist: {sorted(ALLOWED_BUCKETS)}", file=sys.stderr)
return 1
try:
platforms = resolve_platforms(args.platforms)
artifacts = [a for p in platforms for a in artifacts_for(p, args.version)]
except ValueError as exc:
print(f"::error::{exc}", file=sys.stderr)
return 1

work_dir = Path(args.work_dir)
work_dir.mkdir(parents=True, exist_ok=True)

rows: list[str] = []
for artifact in artifacts:
try:
status = mirror_artifact(
artifact, bucket=args.bucket, work_dir=work_dir, dry_run=args.dry_run, force=args.force
)
except (OSError, RuntimeError) as exc:
print(f"::error::Failed to mirror {artifact.filename}: {exc}", file=sys.stderr)
return 1
rows.append(f"| `{artifact.s3_dir}` | `{artifact.filename}` | {status} |")

write_step_summary(
f"### GStreamer {args.version} mirror -> `s3://{args.bucket}/dependencies/gstreamer/`\n\n"
"| Platform | File | Status |\n| --- | --- | --- |\n" + "\n".join(rows) + "\n"
)
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading