Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e2033a7
Add native Windows Arm64 library pipeline
yeelam-gordon Aug 28, 2026
28b6726
Test Arm64 architecture validator
yeelam-gordon Aug 28, 2026
5f2d3dc
Address Copilot review findings
yeelam-gordon Aug 28, 2026
2293ad6
Address second Copilot review
yeelam-gordon Aug 28, 2026
1562708
Address third Copilot review
yeelam-gordon Aug 28, 2026
1ec214a
Clarify Arm64 oracle assertions
yeelam-gordon Aug 28, 2026
7dc7cc3
Address fifth Copilot review
yeelam-gordon Aug 28, 2026
79730e4
Address current Copilot review
yeelam-gordon Aug 28, 2026
e03e204
Improve Arm64 oracle error diagnostics
yeelam-gordon Aug 28, 2026
4ef9476
Fail closed on unverified package hashes
yeelam-gordon Aug 28, 2026
5035d8d
Harden Arm64 package inputs
yeelam-gordon Aug 28, 2026
a26673e
Test against the matching audio PR branch
yeelam-gordon Aug 28, 2026
0fd940a
Capture Arm64 Ninja parse context
yeelam-gordon Aug 28, 2026
27c8396
Fix per-test coverage targets for Ninja
yeelam-gordon Aug 28, 2026
ab3365d
Support FFmpeg 8 and 9 codec capability queries
yeelam-gordon Aug 28, 2026
042f77b
Fix Windows Arm64 dependency linkage
yeelam-gordon Aug 28, 2026
283d613
Add explicit OpenCV 5 module includes
yeelam-gordon Aug 28, 2026
0c85a95
Support OpenCV 5 geometry headers
yeelam-gordon Aug 28, 2026
a9d05c8
Use lean Windows headers in Arm64 oracle test
yeelam-gordon Aug 28, 2026
cc21ce1
Preserve the test media path separator
yeelam-gordon Aug 28, 2026
1db53a1
Use a portable ObjectMask test path
yeelam-gordon Aug 28, 2026
25eddad
Harden OpenCV selection and test temp paths
yeelam-gordon Aug 28, 2026
29e8bed
Use bounded FFmpeg capability arrays
yeelam-gordon Aug 28, 2026
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
74 changes: 74 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,77 @@ jobs:
if: ${{ steps.coverage.outputs.value }}
with:
file: build/coverage.info

# Supplemental native Arm64 presubmit. Its output is never a release
# artifact; GitLab remains the production artifact chain.
build-arm64-presubmit:
runs-on: windows-11-arm
continue-on-error: true
steps:
- name: Preserve repository line endings
run: git config --global core.autocrlf input

- uses: actions/checkout@v4

- name: Checkout OpenShotAudio
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.owner.login || github.repository_owner }}/libopenshot-audio
ref: ${{ github.event.pull_request.head.ref || github.ref_name }}
path: audio

- name: Checkout Catch2
uses: actions/checkout@v4
with:
repository: catchorg/Catch2
ref: v3.8.1
path: Catch2

- uses: msys2/setup-msys2@v2
with:
msystem: CLANGARM64
update: true

- name: Install exact CLANGARM64 package versions
shell: msys2 {0}
run: |
mapfile -t packages < <(sed -n '/^[^#[:space:]][^=]*=/s/,[^,]*$//p' ci/windows-arm64-packages.lock)
pacman --noconfirm -S --needed -- "${packages[@]}"

- name: Build (CLANGARM64, presubmit only)
shell: msys2 {0}
run: |
cmake -B Catch2/build -S Catch2 -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$PWD/catch2-install" \
-DCATCH_BUILD_TESTING=OFF \
-DCATCH_INSTALL_DOCS=OFF
cmake --build Catch2/build
cmake --install Catch2/build
cmake -B audio/build -S audio -G Ninja \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_INSTALL_PREFIX="$PWD/audio/install-arm64" \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_AUDIO_DOCS=OFF
cmake --build audio/build
cmake --install audio/build
cmake -B build -S . -G Ninja \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DUSE_QT6=ON \
-DOpenShotAudio_ROOT="$PWD/audio/install-arm64" \
-DCatch2_DIR="$PWD/catch2-install/lib/cmake/Catch2" \
-DCMAKE_INSTALL_PREFIX="$PWD/install-arm64" \
-DCMAKE_BUILD_TYPE=Release \
-DENABLE_LIB_DOCS=OFF
cmake --build build
ctest --test-dir build --output-on-failure -VV
cmake --install build
python -m unittest discover -s ci -p "test_*.py" -v
python ci/validate_arm64_architecture.py \
--require-native-arm64 \
--package-lock ci/windows-arm64-packages.lock \
--payload-root install-arm64 \
--require-payload \
--json-report build/arm64-presubmit-report.json
36 changes: 36 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,42 @@ windows-builder-x64:
tags:
- windows

windows-builder-arm64:
stage: build-libopenshot
artifacts:
expire_in: 6 months
paths:
- build\install-arm64\*
- build\arm64-architecture-report.json
script:
- try { Invoke-WebRequest -Uri "https://gitlab.openshot.org/OpenShot/libopenshot-audio/-/jobs/artifacts/$CI_COMMIT_REF_NAME/download?job=windows-builder-arm64" -Headers @{"PRIVATE-TOKEN"="$ACCESS_TOKEN"} -OutFile "artifacts.zip" } catch { $_.Exception.Response.StatusCode.Value__ }
- if (-not (Test-Path "artifacts.zip")) { Invoke-WebRequest -Uri "https://gitlab.openshot.org/OpenShot/libopenshot-audio/-/jobs/artifacts/develop/download?job=windows-builder-arm64" -Headers @{"PRIVATE-TOKEN"="$ACCESS_TOKEN"} -OutFile "artifacts.zip" }
- Expand-Archive -Path artifacts.zip -DestinationPath .
- $env:MSYSTEM = "CLANGARM64"
- $env:Path = "C:\msys64\clangarm64\bin;C:\msys64\usr\bin;" + $env:Path;
- cmake -B build -S . -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -D"CMAKE_C_COMPILER=clang" -D"CMAKE_CXX_COMPILER=clang++" -D"CMAKE_INSTALL_PREFIX:PATH=$CI_PROJECT_DIR\build\install-arm64" -D"OpenShotAudio_ROOT=$CI_PROJECT_DIR\build\install-arm64" -D"PYTHON_MODULE_PATH=python" -D"USE_QT6=ON" -D"OPENSHOT_QT_API=pyqt6" -G Ninja -D"CMAKE_BUILD_TYPE:STRING=Release"
- cmake --build build --parallel $([Environment]::ProcessorCount)
- ctest --test-dir build --output-on-failure -VV
- cmake --install build
- python -m unittest discover -s ci -p "test_*.py" -v
- python ci\validate_arm64_architecture.py --require-native-arm64 --package-lock ci\windows-arm64-packages.lock --payload-root build\install-arm64 --require-payload --json-report build\arm64-architecture-report.json
- $PROJECT_VERSION = (Select-String -Path "CMakeLists.txt" -Pattern '^set\(PROJECT_VERSION_FULL "(.*)\"' | %{$_.Matches.Groups[1].value})
- $PROJECT_SO = (Select-String -Path "CMakeLists.txt" -Pattern '^set\(PROJECT_SO_VERSION (.*)\)' | %{$_.Matches.Groups[1].value})
- New-Item -path "build/install-arm64/share/" -Name "$CI_PROJECT_NAME.env" -Value "CI_PROJECT_NAME:$CI_PROJECT_NAME`nCI_COMMIT_REF_NAME:$CI_COMMIT_REF_NAME`nCI_COMMIT_SHA:$CI_COMMIT_SHA`nCI_JOB_ID:$CI_JOB_ID`nCI_PIPELINE_ID:$CI_PIPELINE_ID`nVERSION:$PROJECT_VERSION`nSO:$PROJECT_SO`nTARGET_TRIPLET:aarch64-w64-mingw32`nPE_MACHINE:0xAA64" -ItemType file -force
- $PREV_GIT_LABEL=(git describe --tags --abbrev=0 '@^')
- git log "$PREV_GIT_LABEL..@" --oneline --pretty=format:"- %C(auto,yellow)%h%C(auto,magenta)% %C(auto,blue)%>(12,trunc)%ad %C(auto,green)%<(25,trunc)%aN%C(auto,reset)%s%C(auto,red)% gD% D" --date=short > "build/install-arm64/share/$CI_PROJECT_NAME.log"
when: always
rules:
- if: '$ENABLE_WINDOWS_ARM64 == "1" && $CI_COMMIT_TAG == null'
- when: never
tags:
- windows-arm64
# Requires PR A's published windows-builder-arm64 artifact/digest and a
# native/virtual Windows Arm64 GitLab runner (design-spec.md
# release-infrastructure surface). Does not weaken or replace the
# existing windows-builder-x64/x86 jobs above.
allow_failure: true

windows-builder-x86:
stage: build-libopenshot
artifacts:
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ if(DEFINED UNIT_TEST_TARGETS AND NOT TARGET coverage)
DEPENDS openshot openshot-${_t}-test
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Running unit tests for ${_t} class (coverage disabled)"
VERBATIM
)
endforeach()
endif()
Expand Down
150 changes: 150 additions & 0 deletions ci/test_validate_arm64_architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# SPDX-FileCopyrightText: 2026 OpenShot Studios, LLC
# SPDX-License-Identifier: LGPL-3.0-or-later

import contextlib
import importlib.util
import io
import os
import struct
import sys
import tempfile
import unittest
from unittest import mock

VALIDATOR_PATH = os.path.join(os.path.dirname(__file__), "validate_arm64_architecture.py")
SPEC = importlib.util.spec_from_file_location("validate_arm64_architecture", VALIDATOR_PATH)
if SPEC is None or SPEC.loader is None:
raise RuntimeError("Unable to load validator from %s" % VALIDATOR_PATH)
validator = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(validator)


def write_pe(path, machine):
data = bytearray(0x80)
data[:2] = b"MZ"
struct.pack_into("<I", data, 0x3C, 0x40)
data[0x40:0x44] = b"PE\0\0"
struct.pack_into("<H", data, 0x44, machine)
with open(path, "wb") as stream:
stream.write(data)


class Arm64ArchitectureValidatorTests(unittest.TestCase):
def test_payload_scan_accepts_arm64_and_rejects_amd64(self):
with tempfile.TemporaryDirectory() as root:
write_pe(os.path.join(root, "native.dll"), validator.IMAGE_FILE_MACHINE_ARM64)
results, failures = validator.scan_payload_architecture(root)
self.assertEqual(len(results), 1)
self.assertEqual(failures, [])

write_pe(os.path.join(root, "foreign.pyd"), validator.IMAGE_FILE_MACHINE_AMD64)
results, failures = validator.scan_payload_architecture(root)
self.assertEqual(len(results), 2)
self.assertEqual(len(failures), 1)

def test_required_native_host_fails_closed(self):
oracle = {
"checked": True,
"process_machine": validator.IMAGE_FILE_MACHINE_UNKNOWN,
"native_machine": validator.IMAGE_FILE_MACHINE_AMD64,
"is_wow_or_emulated": False,
"native_arm64_ok": False,
"reason": None,
}
with mock.patch.object(validator, "read_native_process_oracle", return_value=oracle):
with mock.patch.object(sys, "argv", ["validator", "--require-native-arm64"]):
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(validator.main(), 1)

def test_package_lock_detects_version_drift(self):
with tempfile.NamedTemporaryFile("w", delete=False) as lock:
lock.write("example-package=1.2.3,UNVERIFIED-NO-SIGNED-SNAPSHOT\n")
lock_path = lock.name
try:
completed = mock.Mock(returncode=0, stdout="example-package 1.2.4\n")
with mock.patch.object(validator.subprocess, "run", return_value=completed):
verified, failures = validator.verify_package_lock(lock_path)
self.assertEqual(verified[0]["version"], "1.2.4")
self.assertEqual(len(failures), 1)
finally:
os.unlink(lock_path)

def test_package_lock_reports_missing_pacman(self):
with tempfile.NamedTemporaryFile("w", delete=False) as lock:
lock.write("example-package=1.2.3,UNVERIFIED-NO-SIGNED-SNAPSHOT\n")
lock_path = lock.name
try:
with mock.patch.object(
validator.subprocess, "run", side_effect=FileNotFoundError("pacman")
):
verified, failures = validator.verify_package_lock(lock_path)
self.assertEqual(verified, [])
self.assertIn("Unable to run pacman", failures[0])
finally:
os.unlink(lock_path)

def test_package_lock_reports_malformed_entry(self):
with tempfile.NamedTemporaryFile("w", delete=False) as lock:
lock.write("malformed-entry\n")
lock_path = lock.name
try:
verified, failures = validator.verify_package_lock(lock_path)
self.assertEqual(verified, [])
self.assertEqual(failures, ["Malformed package lock entry: malformed-entry"])
finally:
os.unlink(lock_path)

def test_package_lock_rejects_unsafe_package_name(self):
with tempfile.NamedTemporaryFile("w", delete=False) as lock:
lock.write("-unsafe=1.2.3,UNVERIFIED-NO-SIGNED-SNAPSHOT\n")
lock_path = lock.name
try:
verified, failures = validator.verify_package_lock(lock_path)
self.assertEqual(verified, [])
self.assertEqual(failures, ["Invalid package name in lock: '-unsafe'"])
finally:
os.unlink(lock_path)

def test_package_lock_rejects_unexpected_pacman_output(self):
with tempfile.NamedTemporaryFile("w", delete=False) as lock:
lock.write("example-package=1.2.3,UNVERIFIED-NO-SIGNED-SNAPSHOT\n")
lock_path = lock.name
try:
completed = mock.Mock(returncode=0, stdout="warning only\n")
with mock.patch.object(validator.subprocess, "run", return_value=completed):
verified, failures = validator.verify_package_lock(lock_path)
self.assertEqual(verified, [])
self.assertIn("Unexpected pacman output", failures[0])
finally:
os.unlink(lock_path)

def test_package_lock_rejects_unverified_real_hash(self):
with tempfile.NamedTemporaryFile("w", delete=False) as lock:
lock.write("example-package=1.2.3,abc123\n")
lock_path = lock.name
try:
completed = mock.Mock(returncode=0, stdout="example-package 1.2.3\n")
with mock.patch.object(validator.subprocess, "run", return_value=completed):
_verified, failures = validator.verify_package_lock(lock_path)
self.assertIn("hash verification is not implemented", failures[0])
finally:
os.unlink(lock_path)

def test_require_payload_rejects_missing_root(self):
with mock.patch.object(sys, "argv", ["validator", "--require-payload"]):
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(validator.main(), 1)

def test_require_payload_rejects_directory_without_valid_pe(self):
with tempfile.TemporaryDirectory() as root:
with open(os.path.join(root, "stub.exe"), "w", encoding="utf-8") as stream:
stream.write("not a PE")
with mock.patch.object(
sys, "argv", ["validator", "--payload-root", root, "--require-payload"]
):
with contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(validator.main(), 1)


if __name__ == "__main__":
unittest.main()
Loading