mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-22 23:08:08 +03:00
833 lines
30 KiB
YAML
833 lines
30 KiB
YAML
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||
#
|
||
# This file is part of Furious.
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU General Public License as published by
|
||
# the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU General Public License
|
||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
|
||
name: Daily matrix build
|
||
|
||
on:
|
||
schedule:
|
||
# Daily at 02:17 UTC (10:17 Asia/Shanghai).
|
||
- cron: '17 2 * * *'
|
||
workflow_dispatch:
|
||
workflow_call:
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
defaults:
|
||
run:
|
||
shell: bash
|
||
|
||
jobs:
|
||
deploy-binaries:
|
||
name: Deploy ${{ matrix.id }} on ${{ matrix.os }} Python ${{ matrix.python-version }}
|
||
runs-on: ${{ matrix.os }}
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- id: linux-amd64
|
||
os: ubuntu-22.04
|
||
python-version: "3.13"
|
||
python-architecture: x64
|
||
pyside6-version: "6.8.3"
|
||
windows-compatibility: ""
|
||
windows-architecture: ""
|
||
- id: linux-arm64
|
||
os: ubuntu-22.04-arm
|
||
python-version: "3.11"
|
||
python-architecture: arm64
|
||
pyside6-version: "6.5.3"
|
||
windows-compatibility: ""
|
||
windows-architecture: ""
|
||
- id: windows7-amd64
|
||
os: windows-2025
|
||
python-version: "3.13"
|
||
python-architecture: x64
|
||
pyside6-version: "6.8.3"
|
||
windows-compatibility: windows7
|
||
windows-architecture: amd64
|
||
- id: windows10-amd64
|
||
os: windows-2025
|
||
python-version: "3.13"
|
||
python-architecture: x64
|
||
pyside6-version: "6.8.3"
|
||
windows-compatibility: windows10
|
||
windows-architecture: amd64
|
||
- id: windows10-arm64
|
||
os: windows-11-vs2026-arm
|
||
python-version: "3.13"
|
||
python-architecture: arm64
|
||
pyside6-version: "6.11.2"
|
||
windows-compatibility: windows10
|
||
windows-architecture: arm64
|
||
- id: macos-intel
|
||
os: macos-15-intel
|
||
python-version: "3.13"
|
||
python-architecture: x64
|
||
pyside6-version: "6.8.3"
|
||
windows-compatibility: ""
|
||
windows-architecture: ""
|
||
- id: macos-arm64
|
||
os: macos-14
|
||
python-version: "3.13"
|
||
python-architecture: arm64
|
||
pyside6-version: "6.8.3"
|
||
windows-compatibility: ""
|
||
windows-architecture: ""
|
||
env:
|
||
GO_WIN7_VERSION: "1.26.7"
|
||
WIX_VERSION: "6.0.2"
|
||
WIX_INSTALLER_SHA256: "A8A5CC7443353CEF3AB900C60CD7A3A5EE601746319D104AC7B12AD0CED2345C"
|
||
steps:
|
||
- uses: actions/checkout@v7
|
||
- name: Install macOS dependencies
|
||
run: |
|
||
brew install create-dmg
|
||
if: runner.os == 'macOS'
|
||
# Remove problematic brew libs if Intel Mac
|
||
# Credits: https://github.com/RimSort/RimSort
|
||
- name: Remove problematic brew libs on Intel Mac
|
||
run: |
|
||
brew remove --force --ignore-dependencies openssl@3
|
||
brew cleanup openssl@3
|
||
if: runner.os == 'macOS' && runner.arch == 'X64'
|
||
- name: Install Linux dependencies
|
||
run: |
|
||
sudo apt update
|
||
|
||
# if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||
# sudo apt install -y \
|
||
# libevent-2.1-7 \
|
||
# libwebp7 \
|
||
# libminizip1
|
||
# fi
|
||
|
||
sudo apt install -y \
|
||
libxcb-cursor0 \
|
||
qt6-base-dev \
|
||
patchelf \
|
||
ccache \
|
||
zlib1g-dev
|
||
sudo apt install -y flatpak flatpak-builder elfutils patchelf
|
||
sudo apt install -y rpm
|
||
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
|
||
flatpak update
|
||
flatpak install -y flathub org.kde.Platform//6.8 org.kde.Sdk//6.8
|
||
|
||
if [ "$RUNNER_ARCH" == "X64" ]; then
|
||
appimage_arch="x86_64"
|
||
elif [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||
appimage_arch="aarch64"
|
||
else
|
||
echo "Unknown runner architecture: $RUNNER_ARCH"
|
||
exit 1
|
||
fi
|
||
|
||
appimage_tools=".appimage-tools"
|
||
mkdir "$appimage_tools"
|
||
echo "${PWD}/${appimage_tools}" >> "$GITHUB_PATH"
|
||
|
||
wget -P "$appimage_tools" \
|
||
"https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-${appimage_arch}.AppImage"
|
||
chmod +x "$appimage_tools"/*
|
||
if: runner.os == 'Linux'
|
||
- name: Install Windows dependencies
|
||
shell: pwsh
|
||
run: |
|
||
$toolsetArchitecture = switch ($env:RUNNER_ARCH) {
|
||
'X64' { 'x64' }
|
||
'ARM64' { 'arm64' }
|
||
default { throw "Unknown runner architecture: $env:RUNNER_ARCH" }
|
||
}
|
||
|
||
$installer = Join-Path $env:RUNNER_TEMP 'wix-cli-x64.msi'
|
||
$installerUrl = "https://github.com/wixtoolset/wix/releases/download/v$env:WIX_VERSION/wix-cli-x64.msi"
|
||
Invoke-WebRequest -Uri $installerUrl -OutFile $installer
|
||
|
||
$installerHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $installer).Hash
|
||
|
||
if ($installerHash -ne $env:WIX_INSTALLER_SHA256) {
|
||
throw "Unexpected WiX installer SHA-256: $installerHash"
|
||
}
|
||
|
||
$msi = Start-Process msiexec.exe -ArgumentList @(
|
||
'/i',
|
||
$installer,
|
||
'/quiet',
|
||
'/norestart',
|
||
'/log',
|
||
(Join-Path $env:RUNNER_TEMP 'wix-install.log')
|
||
) -WindowStyle Hidden -Wait -PassThru
|
||
|
||
if ($msi.ExitCode -notin @(0, 3010)) {
|
||
throw "WiX installation failed with exit code $($msi.ExitCode)"
|
||
}
|
||
|
||
$toolsetRoot = 'C:\Program Files\WiX Toolset v6.0\bin'
|
||
$toolsetRoot | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||
(Join-Path $toolsetRoot $toolsetArchitecture) | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||
if: runner.os == 'Windows'
|
||
- name: Check Windows dependencies
|
||
run: |
|
||
wix --version
|
||
wix extension add -g "WixToolset.UI.wixext/${WIX_VERSION}"
|
||
wix extension list -g
|
||
if: runner.os == 'Windows'
|
||
- name: Set up Python for Windows7
|
||
uses: LorenEteval/setup-python-win7@v1
|
||
with:
|
||
python-version: ${{ matrix.python-version }}
|
||
check-latest: true
|
||
if: matrix.id == 'windows7-amd64'
|
||
- name: Set up Python
|
||
uses: actions/setup-python@v7
|
||
with:
|
||
python-version: ${{ matrix.python-version }}
|
||
architecture: ${{ matrix.python-architecture }}
|
||
check-latest: true
|
||
if: matrix.id != 'windows7-amd64'
|
||
- name: Verify runner and Python architecture
|
||
env:
|
||
EXPECTED_ARCHITECTURE: ${{ matrix.python-architecture }}
|
||
run: |
|
||
python - <<'PY'
|
||
import os
|
||
import platform
|
||
import struct
|
||
import sys
|
||
|
||
aliases = {
|
||
'x64': {'amd64', 'x86_64'},
|
||
'arm64': {'arm64', 'aarch64'},
|
||
}
|
||
expected = os.environ['EXPECTED_ARCHITECTURE']
|
||
runner_architecture = os.environ.get('RUNNER_ARCH', '').casefold()
|
||
machine = platform.machine().casefold()
|
||
pointer_width = struct.calcsize('P') * 8
|
||
|
||
print(f'RUNNER_OS={os.environ.get("RUNNER_OS")}')
|
||
print(f'RUNNER_ARCH={os.environ.get("RUNNER_ARCH")}')
|
||
print(f'Python={sys.version}')
|
||
print(f'platform.machine()={platform.machine()}')
|
||
print(f'pointer width={pointer_width}')
|
||
|
||
if runner_architecture != expected:
|
||
raise SystemExit(
|
||
f'expected {expected} runner, got {runner_architecture or "unknown"}'
|
||
)
|
||
|
||
if machine not in aliases[expected] or pointer_width != 64:
|
||
raise SystemExit(
|
||
f'expected native {expected} Python, got {machine}/{pointer_width}-bit'
|
||
)
|
||
PY
|
||
- name: Set up PySide6 for Windows7
|
||
uses: LorenEteval/setup-pyside6-win7@v1
|
||
with:
|
||
pyside6-version: ${{ matrix.pyside6-version }}
|
||
if: matrix.id == 'windows7-amd64'
|
||
- name: Install Python build and runtime dependencies
|
||
run: |
|
||
python -c "import sys; print(sys.version)"
|
||
python -m pip install --upgrade pip
|
||
python -m pip install setuptools wheel
|
||
|
||
python - <<'PY'
|
||
import re
|
||
from pathlib import Path
|
||
|
||
excluded = {
|
||
'pyside6',
|
||
'pyside6-addons',
|
||
'pyside6-essentials',
|
||
'xray-core',
|
||
'hysteria',
|
||
'hysteria2',
|
||
'tun2socks',
|
||
}
|
||
|
||
requirements = []
|
||
|
||
for line in Path('requirements.txt').read_text(encoding='utf-8').splitlines():
|
||
match = re.match(r'\s*([A-Za-z0-9_.-]+)', line)
|
||
|
||
if match and match.group(1).casefold().replace('_', '-') in excluded:
|
||
continue
|
||
|
||
requirements.append(line)
|
||
|
||
Path('.binary-requirements.txt').write_text(
|
||
'\n'.join(requirements) + '\n',
|
||
encoding='utf-8',
|
||
)
|
||
PY
|
||
|
||
if [[ "${{ matrix.id }}" != "windows7-amd64" ]]; then
|
||
pyside_packages=(
|
||
"PySide6-Essentials==${{ matrix.pyside6-version }}"
|
||
)
|
||
|
||
if [[ "$RUNNER_OS" != "Linux" ]]; then
|
||
pyside_packages+=(
|
||
"PySide6-Addons==${{ matrix.pyside6-version }}"
|
||
)
|
||
fi
|
||
|
||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||
pyside_binary_policy=(
|
||
"--only-binary=PySide6-Essentials,PySide6-Addons"
|
||
)
|
||
else
|
||
pyside_binary_policy=()
|
||
fi
|
||
|
||
python -m pip install \
|
||
"${pyside_binary_policy[@]}" \
|
||
"${pyside_packages[@]}"
|
||
fi
|
||
|
||
if [ "$RUNNER_OS" == "Linux" ] && [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||
python -m pip install "numpy<2"
|
||
fi
|
||
|
||
python -m pip install -r .binary-requirements.txt
|
||
python -m pip install nuitka imageio requests
|
||
python -c "import importlib.metadata; print('zxing-cpp', importlib.metadata.version('zxing-cpp'))"
|
||
python -c "from PySide6 import QtCore; print(f'PySide6/Qt {QtCore.__version__}')"
|
||
- name: Verify Linux Essentials-only Qt environment
|
||
if: runner.os == 'Linux'
|
||
env:
|
||
EXPECTED_PYSIDE6_VERSION: ${{ matrix.pyside6-version }}
|
||
run: |
|
||
python - <<'PY'
|
||
import importlib.metadata
|
||
import importlib.util
|
||
import os
|
||
import platform
|
||
import sys
|
||
|
||
from PySide6 import QtCore
|
||
|
||
expected = os.environ['EXPECTED_PYSIDE6_VERSION']
|
||
essentials = importlib.metadata.version('PySide6-Essentials')
|
||
|
||
try:
|
||
addons = importlib.metadata.version('PySide6-Addons')
|
||
except importlib.metadata.PackageNotFoundError:
|
||
addons = None
|
||
|
||
print(f'Python={sys.version}')
|
||
print(f'platform.machine()={platform.machine()}')
|
||
print(f'PySide6-Essentials={essentials}')
|
||
print(f'PySide6-Addons={addons or "absent"}')
|
||
print(f'Qt={QtCore.qVersion()}')
|
||
|
||
if essentials != expected:
|
||
raise SystemExit(
|
||
f'expected PySide6-Essentials {expected}, got {essentials}'
|
||
)
|
||
|
||
if QtCore.__version__ != expected:
|
||
raise SystemExit(
|
||
f'expected PySide6/Qt {expected}, got {QtCore.__version__}'
|
||
)
|
||
|
||
if addons is not None:
|
||
raise SystemExit(f'PySide6-Addons must be absent, got {addons}')
|
||
|
||
for name in (
|
||
'PySide6.QtWebChannel',
|
||
'PySide6.QtWebEngineCore',
|
||
'PySide6.QtWebEngineWidgets',
|
||
):
|
||
if importlib.util.find_spec(name) is not None:
|
||
raise SystemExit(f'{name} must be absent from the Linux environment')
|
||
PY
|
||
- name: Install patched Go for Windows7
|
||
run: |
|
||
go_name="go-for-win7-windows-amd64"
|
||
go_file="${go_name}.zip"
|
||
|
||
curl --fail --location --output "${go_file}" \
|
||
"https://github.com/XTLS/go-win7/releases/download/patched-${GO_WIN7_VERSION}/${go_file}"
|
||
7z x "${go_file}" "-o${go_name}" -y
|
||
|
||
go_root="$(cygpath -w "$PWD/${go_name}")"
|
||
echo "GOROOT=${go_root}" >> $GITHUB_ENV
|
||
echo "CGO_ENABLED=1" >> $GITHUB_ENV
|
||
echo "${go_root}\\bin" >> $GITHUB_PATH
|
||
if: matrix.id == 'windows7-amd64'
|
||
- name: Set up Go for Windows10
|
||
uses: actions/setup-go@v7
|
||
with:
|
||
go-version: "1.26"
|
||
check-latest: true
|
||
cache: false
|
||
if: matrix.id == 'windows10-amd64'
|
||
- name: Build Windows AMD64 bindings from source
|
||
shell: pwsh
|
||
env:
|
||
CC: gcc
|
||
CXX: g++
|
||
CGO_ENABLED: "1"
|
||
CMAKE_BUILD_PARALLEL_LEVEL: "2"
|
||
GOPROXY: https://proxy.golang.org,direct
|
||
run: |
|
||
$ErrorActionPreference = 'Stop'
|
||
$PSNativeCommandUseErrorActionPreference = $true
|
||
|
||
Write-Output "Go executable: $((Get-Command go -ErrorAction Stop).Source)"
|
||
go version
|
||
go env GOROOT GOOS GOARCH CGO_ENABLED
|
||
Write-Output "GCC executable: $((Get-Command gcc -ErrorAction Stop).Source)"
|
||
gcc -dumpmachine
|
||
|
||
$temporaryDirectory = Join-Path $env:RUNNER_TEMP 'bindings'
|
||
New-Item -ItemType Directory -Force -Path $temporaryDirectory | Out-Null
|
||
foreach ($name in 'TMPDIR', 'TEMP', 'TMP') {
|
||
Set-Item -Path "Env:$name" -Value $temporaryDirectory
|
||
}
|
||
|
||
Write-Output "Configured temporary directory: $temporaryDirectory"
|
||
python -c "import tempfile; print(f'Python temporary directory: {tempfile.gettempdir()}')"
|
||
|
||
python -m pip install `
|
||
"setuptools>=68" `
|
||
wheel `
|
||
"cmake>=3.15" `
|
||
"pybind11>=3.0.1,<3.1"
|
||
|
||
$wheelDirectory = Join-Path $temporaryDirectory 'wheels'
|
||
New-Item -ItemType Directory -Force -Path $wheelDirectory | Out-Null
|
||
|
||
$bindings = @(
|
||
'Xray-core'
|
||
'hysteria2'
|
||
'tun2socks'
|
||
)
|
||
|
||
foreach ($binding in $bindings) {
|
||
python -m pip wheel `
|
||
--no-cache-dir `
|
||
--no-build-isolation `
|
||
--no-deps `
|
||
--no-binary=:all: `
|
||
--wheel-dir $wheelDirectory `
|
||
$binding
|
||
}
|
||
|
||
$wheels = @(
|
||
Get-ChildItem -LiteralPath $wheelDirectory -Filter '*.whl' -File |
|
||
Sort-Object Name
|
||
)
|
||
Write-Output 'Locally built wheels:'
|
||
|
||
foreach ($wheel in $wheels) {
|
||
Write-Output " $($wheel.Name)"
|
||
}
|
||
|
||
$unexpectedWheels = @($wheels | Where-Object Name -NotLike '*win_amd64.whl')
|
||
|
||
if ($wheels.Count -ne 3 -or $unexpectedWheels.Count -ne 0) {
|
||
throw 'Expected exactly three locally built win_amd64 wheels'
|
||
}
|
||
|
||
$wheelPaths = @($wheels.FullName)
|
||
python -m pip install `
|
||
--no-index `
|
||
--force-reinstall `
|
||
--no-deps `
|
||
$wheelPaths
|
||
if: matrix.id == 'windows7-amd64' || matrix.id == 'windows10-amd64'
|
||
- name: Install pre-built Hysteria1 wheel
|
||
run: |
|
||
python -m pip install \
|
||
--only-binary=hysteria \
|
||
--no-cache-dir \
|
||
--no-deps \
|
||
hysteria
|
||
if: matrix.id == 'windows7-amd64' || matrix.id == 'windows10-amd64'
|
||
- name: Install supported binding wheels
|
||
run: |
|
||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||
binary_policy="--only-binary=Xray-core,hysteria,hysteria2,tun2socks"
|
||
else
|
||
binary_policy=""
|
||
fi
|
||
|
||
python -m pip install \
|
||
${binary_policy} \
|
||
--no-cache-dir \
|
||
--no-deps \
|
||
Xray-core \
|
||
hysteria \
|
||
hysteria2 \
|
||
tun2socks
|
||
if: matrix.id != 'windows7-amd64' && matrix.id != 'windows10-amd64'
|
||
- name: Verify Windows native and UI imports
|
||
run: |
|
||
python - <<'PY'
|
||
import importlib
|
||
|
||
for name in ('xray', 'hysteria', 'hysteria2', 'tun2socks'):
|
||
module = importlib.import_module(name)
|
||
print(f'{name}: {module.__file__}')
|
||
|
||
# Native extension imports fail here if a wheel has the wrong machine type.
|
||
print('All native bindings imported successfully')
|
||
|
||
# Some Windows PySide6 distributions intentionally omit Qt WebEngine.
|
||
# The application must still import and use its non-WebEngine map fallback.
|
||
from Furious.Widget.EndpointInfoWidget import EndpointInfoWidget
|
||
|
||
print(f'Endpoint UI imported successfully: {EndpointInfoWidget.__name__}')
|
||
PY
|
||
if: runner.os == 'Windows'
|
||
- name: Verify Linux application imports without WebEngine
|
||
if: runner.os == 'Linux'
|
||
env:
|
||
QT_QPA_PLATFORM: offscreen
|
||
run: |
|
||
python - <<'PY'
|
||
import importlib
|
||
|
||
import Furious
|
||
import Furious.Application
|
||
import Furious.Window
|
||
from PySide6 import QtCore
|
||
from PySide6.QtWidgets import QApplication
|
||
|
||
endpointModule = importlib.import_module(
|
||
'Furious.Widget.EndpointInfoWidget'
|
||
)
|
||
|
||
if endpointModule.QWebEngineView is not None:
|
||
raise SystemExit('Qt WebEngine must be unavailable in Linux releases')
|
||
|
||
application = QApplication.instance() or QApplication([])
|
||
mapWidget = endpointModule._createEndpointMapWidget(None)
|
||
|
||
if not isinstance(
|
||
mapWidget,
|
||
endpointModule._UnavailableEndpointMapWidget,
|
||
):
|
||
raise SystemExit('the endpoint map did not use its no-WebEngine fallback')
|
||
|
||
mapWidget.close()
|
||
mapWidget.deleteLater()
|
||
application.sendPostedEvents(
|
||
None,
|
||
QtCore.QEvent.Type.DeferredDelete,
|
||
)
|
||
application.processEvents()
|
||
|
||
print(f'Furious imported successfully: {Furious.__name__}')
|
||
print('Application and window modules imported successfully')
|
||
print('Endpoint UI created its no-WebEngine fallback successfully')
|
||
PY
|
||
- name: Verify Qt WebEngine imports
|
||
if: runner.os == 'macOS'
|
||
run: |
|
||
python - <<'PY'
|
||
from PySide6 import QtWebChannel, QtWebEngineCore, QtWebEngineWidgets
|
||
|
||
print(f'QtWebChannel imported successfully: {QtWebChannel.__name__}')
|
||
print(f'QtWebEngineCore imported successfully: {QtWebEngineCore.__name__}')
|
||
print(f'QtWebEngineWidgets imported successfully: {QtWebEngineWidgets.__name__}')
|
||
PY
|
||
- name: Download latest asset files
|
||
run: |
|
||
python Deploy.py --download
|
||
- name: Run deploy script
|
||
env:
|
||
APPIMAGE_EXTRACT_AND_RUN: "1"
|
||
run: |
|
||
if [[ -n "${{ matrix.windows-compatibility }}" ]]; then
|
||
export WIN_VER_COMPATIBLE="${{ matrix.windows-compatibility }}"
|
||
fi
|
||
python Deploy.py
|
||
- name: Verify Linux distribution excludes WebEngine
|
||
if: runner.os == 'Linux'
|
||
run: |
|
||
python - <<'PY'
|
||
from pathlib import Path
|
||
|
||
root = Path('Furious-Deploy/Furious.dist')
|
||
|
||
if not root.is_dir():
|
||
raise SystemExit(f'Nuitka distribution is missing: {root}')
|
||
|
||
forbidden = ('qtwebchannel', 'qtwebengine')
|
||
unexpected = []
|
||
|
||
for path in root.rglob('*'):
|
||
relative = path.relative_to(root).as_posix().casefold()
|
||
|
||
if any(name in relative for name in forbidden):
|
||
unexpected.append(relative)
|
||
|
||
if unexpected:
|
||
raise SystemExit(
|
||
'WebEngine/Addons artifacts found in the Linux distribution:\n'
|
||
+ '\n'.join(f' {path}' for path in unexpected)
|
||
)
|
||
|
||
print(f'No Qt WebEngine or Qt WebChannel artifacts found below {root}')
|
||
PY
|
||
- name: Verify Flatpak native dependencies
|
||
if: runner.os == 'Linux'
|
||
run: |
|
||
bundle="$(find . -maxdepth 1 -name 'Furious-*.flatpak' -print -quit)"
|
||
|
||
if [[ -z "$bundle" ]]; then
|
||
echo 'Flatpak bundle was not generated'
|
||
exit 1
|
||
fi
|
||
|
||
flatpak install --user --noninteractive --reinstall -y "$bundle"
|
||
|
||
cleanup() {
|
||
flatpak uninstall \
|
||
--user \
|
||
--noninteractive \
|
||
--delete-data \
|
||
-y \
|
||
com.Furious.Furious || true
|
||
}
|
||
|
||
trap cleanup EXIT
|
||
|
||
flatpak run --user --command=bash com.Furious.Furious -c '
|
||
set -uo pipefail
|
||
|
||
application_root=/app/lib/Furious
|
||
dependency_report=/tmp/furious-flatpak-missing-dependencies
|
||
failures=0
|
||
unsupported_qt_plugins=(
|
||
"$application_root/PySide6/qt-plugins/imageformats/libqpdf.so"
|
||
"$application_root/PySide6/qt-plugins/imageformats/libqtiff.so"
|
||
"$application_root/PySide6/qt-plugins/imageformats/libqwebp.so"
|
||
"$application_root/PySide6/qt-plugins/platforms/libqeglfs.so"
|
||
"$application_root/PySide6/qt-plugins/egldeviceintegrations"
|
||
)
|
||
|
||
: > "$dependency_report"
|
||
|
||
if [[ ! -x "$application_root/Furious.bin" ]]; then
|
||
printf "Packaged application is missing or not executable: %s\n" \
|
||
"$application_root/Furious.bin" >> "$dependency_report"
|
||
failures=1
|
||
fi
|
||
|
||
double_conversion="$(
|
||
find /app/lib -type f -name "libdouble-conversion.so.*" \
|
||
-print -quit 2>> "$dependency_report"
|
||
)"
|
||
|
||
if [[ -z "$double_conversion" ]]; then
|
||
printf "Bundled libdouble-conversion.so.3 was not found below /app/lib\n" \
|
||
>> "$dependency_report"
|
||
failures=1
|
||
else
|
||
printf "Bundled double-conversion library: %s\n" "$double_conversion"
|
||
fi
|
||
|
||
for plugin in "${unsupported_qt_plugins[@]}"; do
|
||
if [[ -e "$plugin" ]]; then
|
||
printf "Unsupported Qt plugin was packaged: %s\n" "$plugin" \
|
||
>> "$dependency_report"
|
||
failures=1
|
||
fi
|
||
done
|
||
|
||
if ! command -v ldd >/dev/null; then
|
||
printf "The Flatpak runtime does not provide ldd\n" \
|
||
>> "$dependency_report"
|
||
failures=1
|
||
else
|
||
while IFS= read -r binary; do
|
||
if ! dependencies="$(ldd "$binary" 2>&1)"; then
|
||
printf "%s\n%s\n\n" \
|
||
"Failed to inspect $binary:" \
|
||
"$dependencies" >> "$dependency_report"
|
||
failures=1
|
||
elif printf "%s\n" "$dependencies" | grep -Fq "not found"; then
|
||
printf "%s\n%s\n\n" \
|
||
"Missing dependencies for $binary:" \
|
||
"$dependencies" >> "$dependency_report"
|
||
failures=1
|
||
fi
|
||
done < <(
|
||
find "$application_root" \
|
||
-type f \
|
||
\( -name "*.bin" -o -name "*.so" -o -name "*.so.*" \) \
|
||
-print
|
||
)
|
||
fi
|
||
|
||
if ((failures)); then
|
||
cat "$dependency_report"
|
||
exit 1
|
||
fi
|
||
|
||
echo "Flatpak native dependency check passed"
|
||
'
|
||
- name: Verify macOS application bundle
|
||
if: runner.os == 'macOS'
|
||
shell: bash
|
||
run: |
|
||
app='app/Furious-GUI.app'
|
||
source_app='Furious-Deploy/Furious-GUI.app'
|
||
macos_root="${app}/Contents/MacOS"
|
||
qt_root="${macos_root}/PySide6/Qt"
|
||
webengine_framework="${qt_root}/lib/QtWebEngineCore.framework"
|
||
relocated_qt_root="${app}/Contents/Frameworks/PySide6/Qt"
|
||
relocated_webengine_framework="${relocated_qt_root}/lib/QtWebEngineCore.framework"
|
||
webengine_helper="${relocated_webengine_framework}/Helpers/QtWebEngineProcess.app/Contents/MacOS/QtWebEngineProcess"
|
||
failures=0
|
||
|
||
check() {
|
||
local description="$1"
|
||
local status
|
||
shift
|
||
|
||
echo "::group::${description}"
|
||
|
||
if "$@"; then
|
||
status=0
|
||
echo "PASS: ${description} (exit code ${status})"
|
||
else
|
||
status=$?
|
||
echo "::error title=macOS bundle verification::FAIL: ${description} (exit code ${status})"
|
||
failures=$((failures + 1))
|
||
fi
|
||
|
||
echo '::endgroup::'
|
||
}
|
||
|
||
echo "Application bundle: ${app}"
|
||
echo "Qt runtime root: ${qt_root}"
|
||
echo "Relocated Qt root: ${relocated_qt_root}"
|
||
echo "WebEngine framework link: ${webengine_framework}"
|
||
echo "Relocated WebEngine framework: ${relocated_webengine_framework}"
|
||
echo "WebEngine helper: ${webengine_helper}"
|
||
|
||
check 'Application bundle exists' test -d "${app}"
|
||
check 'Nuitka source application bundle exists' test -d "${source_app}"
|
||
framework_paths=(
|
||
"${webengine_framework}"
|
||
"${relocated_webengine_framework}"
|
||
"${relocated_webengine_framework}/Versions/Current"
|
||
"${relocated_webengine_framework}/QtWebEngineCore"
|
||
)
|
||
check 'Relocated framework paths can be inspected' ls -ld "${framework_paths[@]}"
|
||
check 'MacOS WebEngine framework entry is a symbolic link' test -L "${webengine_framework}"
|
||
check 'Framework Current version is a symbolic link' test -L "${relocated_webengine_framework}/Versions/Current"
|
||
check 'Framework top-level binary is a symbolic link' test -L "${relocated_webengine_framework}/QtWebEngineCore"
|
||
check 'WebEngine process helper is executable' test -x "${webengine_helper}"
|
||
resource_paths=(
|
||
"${macos_root}/icudtl.dat"
|
||
"${macos_root}/qtwebengine_resources.pak"
|
||
)
|
||
check 'WebEngine resource metadata can be inspected' ls -l@ "${resource_paths[@]}"
|
||
check 'Chromium ICU data exists' test -f "${resource_paths[0]}"
|
||
check 'Chromium resource pack exists' test -f "${resource_paths[1]}"
|
||
check 'Nuitka source code signature is valid' codesign --verify --deep --strict --verbose=2 "${source_app}"
|
||
check 'Staged application code signature is valid' codesign --verify --deep --strict --verbose=2 "${app}"
|
||
size_paths=(
|
||
"${app}"
|
||
"${app}/Contents/Frameworks"
|
||
"${app}/Contents/MacOS"
|
||
)
|
||
check 'Application bundle sizes can be measured' du -sh "${size_paths[@]}"
|
||
|
||
if ((failures > 0)); then
|
||
echo "::error title=macOS bundle verification::${failures} check(s) failed"
|
||
exit 1
|
||
fi
|
||
- name: Verify packaged Windows binaries
|
||
env:
|
||
EXPECTED_WINDOWS_ARCHITECTURE: ${{ matrix.windows-architecture }}
|
||
WINDOWS_COMPATIBILITY: ${{ matrix.windows-compatibility }}
|
||
run: |
|
||
python - <<'PY'
|
||
import os
|
||
import struct
|
||
from pathlib import Path
|
||
|
||
machineCodes = {
|
||
'amd64': 0x8664,
|
||
'arm64': 0xAA64,
|
||
}
|
||
expected = os.environ['EXPECTED_WINDOWS_ARCHITECTURE']
|
||
expectedMachine = machineCodes[expected]
|
||
root = Path('Furious-Deploy/Furious.dist')
|
||
compatibilityDll = root / 'api-ms-win-core-path-l1-1-0.dll'
|
||
|
||
if (
|
||
os.environ['WINDOWS_COMPATIBILITY'] == 'windows7'
|
||
and not compatibilityDll.is_file()
|
||
):
|
||
raise SystemExit(f'Windows 7 compatibility DLL is missing: {compatibilityDll}')
|
||
|
||
binaries = sorted(
|
||
{
|
||
*root.rglob('*.exe'),
|
||
*root.rglob('*.pyd'),
|
||
*([compatibilityDll] if compatibilityDll.is_file() else []),
|
||
}
|
||
)
|
||
|
||
if not binaries:
|
||
raise SystemExit(f'no packaged Windows binaries found below {root}')
|
||
|
||
for binary in binaries:
|
||
with binary.open('rb') as stream:
|
||
if stream.read(2) != b'MZ':
|
||
raise SystemExit(f'{binary} is not a PE binary')
|
||
|
||
stream.seek(0x3C)
|
||
peOffset = struct.unpack('<I', stream.read(4))[0]
|
||
stream.seek(peOffset)
|
||
|
||
if stream.read(4) != b'PE\0\0':
|
||
raise SystemExit(f'{binary} has an invalid PE header')
|
||
|
||
machine = struct.unpack('<H', stream.read(2))[0]
|
||
|
||
if machine != expectedMachine:
|
||
raise SystemExit(
|
||
f'{binary} has PE machine 0x{machine:04x}, expected '
|
||
f'{expected} (0x{expectedMachine:04x})'
|
||
)
|
||
|
||
print(f'Verified {len(binaries)} packaged {expected} PE binaries')
|
||
PY
|
||
if: runner.os == 'Windows'
|
||
- name: Store the distribution packages
|
||
uses: actions/upload-artifact@v7
|
||
with:
|
||
name: binary-distributions-${{ matrix.id }}-Python${{ matrix.python-version }}
|
||
path: |
|
||
Furious-*.zip
|
||
Furious-*.msi
|
||
Furious-*.dmg
|
||
Furious-*.AppImage
|
||
Furious-*.deb
|
||
Furious-*.flatpak
|
||
Furious-*.rpm
|