mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-11 08:48:25 +03:00
@@ -85,25 +85,34 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- id: linux-amd64
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-24.04
|
||||
python-version: "3.13"
|
||||
python-architecture: x64
|
||||
pyside6-version: "6.8.3"
|
||||
linux-appimage-builder: linuxdeploy
|
||||
windows-compatibility: ""
|
||||
windows-architecture: ""
|
||||
- id: linux-arm64
|
||||
os: ubuntu-22.04-arm
|
||||
os: ubuntu-24.04-arm
|
||||
python-version: "3.11"
|
||||
python-architecture: arm64
|
||||
pyside6-version: "6.5.3"
|
||||
pyside6-version: "6.8.3"
|
||||
linux-appimage-builder: appimagetool
|
||||
windows-compatibility: ""
|
||||
windows-architecture: ""
|
||||
- id: windows-win7-amd64
|
||||
- id: windows7-amd64
|
||||
os: windows-2025
|
||||
python-version: "3.13"
|
||||
python-architecture: x64
|
||||
pyside6-version: "6.8.3"
|
||||
windows-compatibility: windows
|
||||
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: windows-arm64
|
||||
os: windows-11-vs2026-arm
|
||||
@@ -127,12 +136,7 @@ jobs:
|
||||
windows-compatibility: ""
|
||||
windows-architecture: ""
|
||||
env:
|
||||
XRAY_CORE_VERSION: "1.8.26.9"
|
||||
HYSTERIA_VERSION: "1.3.5.4"
|
||||
HYSTERIA2_VERSION: "2.12.1.1"
|
||||
TUN2SOCKS_VERSION: "2.7.0.1"
|
||||
GO_WIN7_VERSION: "1.26.7"
|
||||
NUITKA_VERSION: "4.1.3"
|
||||
WIX_VERSION: "6.0.2"
|
||||
WIX_INSTALLER_SHA256: "A8A5CC7443353CEF3AB900C60CD7A3A5EE601746319D104AC7B12AD0CED2345C"
|
||||
steps:
|
||||
@@ -151,7 +155,26 @@ jobs:
|
||||
- name: Install Linux dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y libxcb-cursor0 qt6-base-dev patchelf ccache zlib1g-dev
|
||||
|
||||
if [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
if apt-cache show libminizip1t64 >/dev/null 2>&1; then
|
||||
minizip_package='libminizip1t64'
|
||||
else
|
||||
minizip_package='libminizip1'
|
||||
fi
|
||||
|
||||
sudo apt install -y \
|
||||
libevent-2.1-7 \
|
||||
libwebp7 \
|
||||
"$minizip_package"
|
||||
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
|
||||
@@ -159,20 +182,33 @@ jobs:
|
||||
flatpak install -y flathub org.kde.Platform//6.8 org.kde.Sdk//6.8
|
||||
|
||||
if [ "$RUNNER_ARCH" == "X64" ]; then
|
||||
appimage_arch="x86_64"
|
||||
echo "QMAKE=$(command -v qmake6)" >> "$GITHUB_ENV"
|
||||
|
||||
linuxdeploy_arch="x86_64"
|
||||
linuxdeploy_tools=".linuxdeploy-bin"
|
||||
mkdir "$linuxdeploy_tools"
|
||||
echo "${PWD}/${linuxdeploy_tools}" >> "$GITHUB_PATH"
|
||||
|
||||
wget -P "$linuxdeploy_tools" \
|
||||
"https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-${linuxdeploy_arch}.AppImage"
|
||||
wget -P "$linuxdeploy_tools" \
|
||||
"https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/latest/download/linuxdeploy-plugin-qt-${linuxdeploy_arch}.AppImage"
|
||||
wget -P "$linuxdeploy_tools" \
|
||||
"https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-${linuxdeploy_arch}.AppImage"
|
||||
chmod +x "$linuxdeploy_tools"/*
|
||||
elif [ "$RUNNER_ARCH" == "ARM64" ]; then
|
||||
appimage_arch="aarch64"
|
||||
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"/*
|
||||
else
|
||||
echo "Unknown runner architecture: $RUNNER_ARCH"
|
||||
exit 1
|
||||
fi
|
||||
echo "appimagetool architecture is $appimage_arch"
|
||||
|
||||
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
|
||||
@@ -221,14 +257,14 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
if: matrix.id == 'windows-win7-amd64'
|
||||
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 != 'windows-win7-amd64'
|
||||
if: matrix.id != 'windows7-amd64'
|
||||
- name: Verify runner and Python architecture
|
||||
env:
|
||||
EXPECTED_ARCHITECTURE: ${{ matrix.python-architecture }}
|
||||
@@ -268,7 +304,7 @@ jobs:
|
||||
uses: LorenEteval/setup-pyside6-win7@v1
|
||||
with:
|
||||
pyside6-version: ${{ matrix.pyside6-version }}
|
||||
if: matrix.id == 'windows-win7-amd64'
|
||||
if: matrix.id == 'windows7-amd64'
|
||||
- name: Install Python build and runtime dependencies
|
||||
run: |
|
||||
python -c "import sys; print(sys.version)"
|
||||
@@ -304,7 +340,7 @@ jobs:
|
||||
)
|
||||
PY
|
||||
|
||||
if [[ "${{ matrix.id }}" != "windows-win7-amd64" ]]; then
|
||||
if [[ "${{ matrix.id }}" != "windows7-amd64" ]]; then
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
pyside_binary_policy="--only-binary=PySide6-Essentials,PySide6-Addons"
|
||||
else
|
||||
@@ -322,8 +358,7 @@ jobs:
|
||||
fi
|
||||
|
||||
python -m pip install -r .binary-requirements.txt
|
||||
|
||||
python -m pip install "Nuitka==${NUITKA_VERSION}" imageio requests
|
||||
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: Install patched Go for Windows7
|
||||
@@ -339,87 +374,97 @@ jobs:
|
||||
echo "GOROOT=${go_root}" >> $GITHUB_ENV
|
||||
echo "CGO_ENABLED=1" >> $GITHUB_ENV
|
||||
echo "${go_root}\\bin" >> $GITHUB_PATH
|
||||
if: matrix.id == 'windows-win7-amd64'
|
||||
- name: Build Windows7 bindings from source with patched Go
|
||||
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: |
|
||||
command -v go
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
|
||||
Write-Output "Go executable: $((Get-Command go -ErrorAction Stop).Source)"
|
||||
go version
|
||||
go env GOROOT GOOS GOARCH CGO_ENABLED
|
||||
command -v gcc
|
||||
Write-Output "GCC executable: $((Get-Command gcc -ErrorAction Stop).Source)"
|
||||
gcc -dumpmachine
|
||||
|
||||
short_temp="$RUNNER_TEMP/w7"
|
||||
mkdir -p "$short_temp"
|
||||
short_temp="$(cygpath -w "$short_temp")"
|
||||
export TMPDIR="$short_temp"
|
||||
export TEMP="$short_temp"
|
||||
export TMP="$short_temp"
|
||||
$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
|
||||
}
|
||||
|
||||
python - <<'PY'
|
||||
import os
|
||||
import tempfile
|
||||
Write-Output "Configured temporary directory: $temporaryDirectory"
|
||||
python -c "import tempfile; print(f'Python temporary directory: {tempfile.gettempdir()}')"
|
||||
|
||||
print(f'Configured temporary directory: {os.environ["TEMP"]}')
|
||||
print(f'Python temporary directory: {tempfile.gettempdir()}')
|
||||
PY
|
||||
|
||||
python -m pip install \
|
||||
"setuptools>=68" \
|
||||
wheel \
|
||||
"cmake>=3.15" \
|
||||
python -m pip install `
|
||||
"setuptools>=68" `
|
||||
wheel `
|
||||
"cmake>=3.15" `
|
||||
"pybind11>=3.0.1,<3.1"
|
||||
|
||||
rm -rf win7-wheels
|
||||
mkdir win7-wheels
|
||||
bindings=(
|
||||
"Xray-core==${XRAY_CORE_VERSION}"
|
||||
"hysteria2==${HYSTERIA2_VERSION}"
|
||||
"tun2socks==${TUN2SOCKS_VERSION}"
|
||||
$wheelDirectory = Join-Path $temporaryDirectory 'wheels'
|
||||
New-Item -ItemType Directory -Force -Path $wheelDirectory | Out-Null
|
||||
|
||||
$bindings = @(
|
||||
'Xray-core'
|
||||
'hysteria2'
|
||||
'tun2socks'
|
||||
)
|
||||
|
||||
for binding in "${bindings[@]}"
|
||||
do
|
||||
python -m pip wheel \
|
||||
--no-cache-dir \
|
||||
--no-build-isolation \
|
||||
--no-deps \
|
||||
--no-binary=:all: \
|
||||
--wheel-dir win7-wheels \
|
||||
"${binding}"
|
||||
done
|
||||
foreach ($binding in $bindings) {
|
||||
python -m pip wheel `
|
||||
--no-cache-dir `
|
||||
--no-build-isolation `
|
||||
--no-deps `
|
||||
--no-binary=:all: `
|
||||
--wheel-dir $wheelDirectory `
|
||||
$binding
|
||||
}
|
||||
|
||||
python - <<'PY'
|
||||
from pathlib import Path
|
||||
$wheels = @(
|
||||
Get-ChildItem -LiteralPath $wheelDirectory -Filter '*.whl' -File |
|
||||
Sort-Object Name
|
||||
)
|
||||
Write-Output 'Locally built wheels:'
|
||||
|
||||
wheels = sorted(Path('win7-wheels').glob('*.whl'))
|
||||
print('Locally built wheels:')
|
||||
foreach ($wheel in $wheels) {
|
||||
Write-Output " $($wheel.Name)"
|
||||
}
|
||||
|
||||
for wheel in wheels:
|
||||
print(f' {wheel.name}')
|
||||
$unexpectedWheels = @($wheels | Where-Object Name -NotLike '*win_amd64.whl')
|
||||
|
||||
if len(wheels) != 3 or any('win_amd64.whl' not in wheel.name for wheel in wheels):
|
||||
raise SystemExit('expected exactly three locally built win_amd64 wheels')
|
||||
PY
|
||||
if ($wheels.Count -ne 3 -or $unexpectedWheels.Count -ne 0) {
|
||||
throw 'Expected exactly three locally built win_amd64 wheels'
|
||||
}
|
||||
|
||||
python -m pip install \
|
||||
--no-index \
|
||||
--force-reinstall \
|
||||
--no-deps \
|
||||
win7-wheels/*.whl
|
||||
if: matrix.id == 'windows-win7-amd64'
|
||||
- name: Install Windows7-compatible Hysteria1 wheel
|
||||
$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==${HYSTERIA_VERSION}"
|
||||
if: matrix.id == 'windows-win7-amd64'
|
||||
hysteria
|
||||
if: matrix.id == 'windows7-amd64' || matrix.id == 'windows10-amd64'
|
||||
- name: Install supported binding wheels
|
||||
run: |
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
@@ -432,12 +477,12 @@ jobs:
|
||||
${binary_policy} \
|
||||
--no-cache-dir \
|
||||
--no-deps \
|
||||
"Xray-core==${XRAY_CORE_VERSION}" \
|
||||
"hysteria==${HYSTERIA_VERSION}" \
|
||||
"hysteria2==${HYSTERIA2_VERSION}" \
|
||||
"tun2socks==${TUN2SOCKS_VERSION}"
|
||||
if: matrix.id != 'windows-win7-amd64'
|
||||
- name: Verify Windows binding imports
|
||||
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
|
||||
@@ -448,22 +493,113 @@ jobs:
|
||||
|
||||
# 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 Qt WebEngine imports
|
||||
if: runner.os != 'Windows'
|
||||
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"
|
||||
LINUX_APPIMAGE_BUILDER: ${{ matrix.linux-appimage-builder }}
|
||||
run: |
|
||||
if [[ -n "${{ matrix.windows-compatibility }}" ]]; then
|
||||
export WIN_VER_COMPATIBLE="${{ matrix.windows-compatibility }}"
|
||||
fi
|
||||
python Deploy.py
|
||||
- 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
|
||||
@@ -477,10 +613,19 @@ jobs:
|
||||
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 []),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -568,6 +713,6 @@ jobs:
|
||||
with:
|
||||
identifier: LorenEteval.Furious
|
||||
release-tag: ${{ github.ref_name }}
|
||||
installers-regex: '^Furious-.*-(?:windows-amd64|windows11-arm64)\.zip$'
|
||||
installers-regex: '^Furious-.*-(?:windows10-amd64|windows11-arm64)\.zip$'
|
||||
max-versions-to-keep: 5
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
||||
@@ -52,6 +52,7 @@ USER_HOME = pathlib.Path.home()
|
||||
DEPLOY_DIR_NAME = f'{APPLICATION_NAME}-Deploy'
|
||||
|
||||
HYSTERIA_DATA_DIR = DATA_DIR / 'hysteria'
|
||||
WINDOWS_7_COMPATIBILITY_DLL_NAME = 'api-ms-win-core-path-l1-1-0.dll'
|
||||
|
||||
NUITKA_BINARY_VERSION_OPTION = (
|
||||
f'--company-name={APPLICATION_NAME} '
|
||||
@@ -189,11 +190,34 @@ elif PLATFORM == 'Linux':
|
||||
)
|
||||
|
||||
LINUX_APPIMAGE_FILENAME = f'{ARTIFACT_NAME}.AppImage'
|
||||
LINUX_CREATE_APPIMAGE_CMD = [
|
||||
f'appimagetool-{PLATFORM_MACHINE}.AppImage',
|
||||
LINUX_APP_DIR,
|
||||
ROOT_DIR / LINUX_APPIMAGE_FILENAME,
|
||||
]
|
||||
LINUX_APPIMAGE_BUILDER = os.environ.get(
|
||||
'LINUX_APPIMAGE_BUILDER', 'appimagetool'
|
||||
).casefold()
|
||||
|
||||
if LINUX_APPIMAGE_BUILDER == 'linuxdeploy':
|
||||
LINUX_CREATE_APPIMAGE_CMD = [
|
||||
f'linuxdeploy-{PLATFORM_MACHINE}.AppImage',
|
||||
'--appdir',
|
||||
LINUX_APP_DIR,
|
||||
'-d',
|
||||
LINUX_APP_DIR / LINUX_DESKTOP_FILE,
|
||||
'-i',
|
||||
LINUX_APP_DIR / LINUX_ICON_FILE,
|
||||
'--plugin',
|
||||
'qt',
|
||||
'--output',
|
||||
'appimage',
|
||||
]
|
||||
elif LINUX_APPIMAGE_BUILDER == 'appimagetool':
|
||||
LINUX_CREATE_APPIMAGE_CMD = [
|
||||
f'appimagetool-{PLATFORM_MACHINE}.AppImage',
|
||||
LINUX_APP_DIR,
|
||||
ROOT_DIR / LINUX_APPIMAGE_FILENAME,
|
||||
]
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f'unsupported Linux AppImage builder: {LINUX_APPIMAGE_BUILDER!r}'
|
||||
)
|
||||
|
||||
LINUX_DEBIAN_DIR = ROOT_DIR / 'debian'
|
||||
LINUX_DEB_FILENAME = f'{ARTIFACT_NAME}.deb'
|
||||
@@ -470,6 +494,22 @@ def main():
|
||||
printStandardStream(result.stdout, result.stderr)
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
distributionPath = ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist'
|
||||
|
||||
if winVerCompatible.casefold() == 'windows7':
|
||||
compatibilityDll = pathlib.Path(sys.executable).with_name(
|
||||
WINDOWS_7_COMPATIBILITY_DLL_NAME
|
||||
)
|
||||
|
||||
if not compatibilityDll.is_file():
|
||||
raise FileNotFoundError(
|
||||
f'Windows 7 compatibility DLL not found: {compatibilityDll}'
|
||||
)
|
||||
|
||||
shutil.copy2(compatibilityDll, distributionPath)
|
||||
|
||||
logger.info(f'included Windows 7 compatibility DLL: {compatibilityDll}')
|
||||
|
||||
try:
|
||||
shutil.rmtree(ROOT_DIR / DEPLOY_DIR_NAME / WIN_UNZIPPED)
|
||||
except FileNotFoundError:
|
||||
@@ -482,14 +522,14 @@ def main():
|
||||
# Copy LICENSE to distribution folder
|
||||
shutil.copy(
|
||||
ROOT_DIR / 'LICENSE',
|
||||
ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist',
|
||||
distributionPath,
|
||||
)
|
||||
shutil.copy(
|
||||
ROOT_DIR / 'LICENSE.rtf',
|
||||
ROOT_DIR / DEPLOY_DIR_NAME,
|
||||
)
|
||||
shutil.copytree(
|
||||
ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist',
|
||||
distributionPath,
|
||||
ROOT_DIR / DEPLOY_DIR_NAME / WIN_UNZIPPED,
|
||||
)
|
||||
shutil.make_archive(
|
||||
@@ -645,10 +685,32 @@ def main():
|
||||
|
||||
raise
|
||||
|
||||
shutil.copytree(
|
||||
ROOT_DIR / DEPLOY_DIR_NAME / 'Furious-GUI.app',
|
||||
MAC_APP_DIR / 'Furious-GUI.app',
|
||||
)
|
||||
sourceApp = ROOT_DIR / DEPLOY_DIR_NAME / 'Furious-GUI.app'
|
||||
stagedApp = MAC_APP_DIR / 'Furious-GUI.app'
|
||||
|
||||
try:
|
||||
result = runExternalCommand(
|
||||
(
|
||||
'ditto',
|
||||
'--rsrc',
|
||||
'--extattr',
|
||||
str(sourceApp),
|
||||
str(stagedApp),
|
||||
),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as err:
|
||||
logger.error(f'stage macOS app failed with returncode {err.returncode}')
|
||||
|
||||
printStandardStream(err.stdout, err.stderr)
|
||||
|
||||
sys.exit(EXIT_FAILURE)
|
||||
else:
|
||||
logger.info('stage macOS app success')
|
||||
|
||||
printStandardStream(result.stdout, result.stderr)
|
||||
|
||||
logger.info('generating dmg')
|
||||
|
||||
@@ -734,12 +796,16 @@ def main():
|
||||
|
||||
logger.info('generating AppImage')
|
||||
|
||||
# Nuitka's standalone directory already owns its complete Qt runtime.
|
||||
# appimagetool packages it as-is instead of harvesting Qt a second time.
|
||||
os.environ['ARCH'] = PLATFORM_MACHINE
|
||||
if LINUX_APPIMAGE_BUILDER == 'linuxdeploy':
|
||||
os.environ['LDAI_OUTPUT'] = LINUX_APPIMAGE_FILENAME
|
||||
os.environ['LINUXDEPLOY_OUTPUT_VERSION'] = APPLICATION_VERSION
|
||||
else:
|
||||
# Nuitka's standalone directory already owns its complete Qt runtime.
|
||||
# appimagetool packages it as-is instead of harvesting Qt a second time.
|
||||
os.environ['ARCH'] = PLATFORM_MACHINE
|
||||
|
||||
try:
|
||||
runExternalCommand(
|
||||
result = runExternalCommand(
|
||||
LINUX_CREATE_APPIMAGE_CMD,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
|
||||
@@ -26,13 +26,6 @@ from Furious.Service import EndpointInfo, EndpointInfoState
|
||||
from Furious.Widget.WaitingSpinner import WaitingSpinner
|
||||
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtWebChannel import QWebChannel
|
||||
from PySide6.QtWebEngineCore import (
|
||||
QWebEnginePage,
|
||||
QWebEngineProfile,
|
||||
QWebEngineSettings,
|
||||
)
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import json
|
||||
@@ -42,8 +35,33 @@ __all__ = ['EndpointInfoWidget']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from PySide6.QtWebChannel import QWebChannel
|
||||
from PySide6.QtWebEngineCore import (
|
||||
QWebEnginePage,
|
||||
QWebEngineProfile,
|
||||
QWebEngineSettings,
|
||||
)
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
except ImportError:
|
||||
(
|
||||
QWebChannel,
|
||||
QWebEnginePage,
|
||||
QWebEngineProfile,
|
||||
QWebEngineSettings,
|
||||
QWebEngineView,
|
||||
) = (
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
class _EndpointWebView(QWebEngineView):
|
||||
_EndpointWebViewBase = QWebEngineView if QWebEngineView is not None else QWidget
|
||||
|
||||
|
||||
class _EndpointWebView(_EndpointWebViewBase):
|
||||
"""Keep map wheel gestures from bubbling into the Metrics scroll area."""
|
||||
|
||||
def wheelEvent(self, event):
|
||||
@@ -204,10 +222,7 @@ class _EndpointMapWidget(QWidget):
|
||||
layout.addWidget(self.webView, 0, 0)
|
||||
layout.addWidget(self.statusOverlay, 0, 0)
|
||||
|
||||
if (
|
||||
PLATFORM == 'Windows'
|
||||
and QApplication.platformName().casefold() != 'offscreen'
|
||||
):
|
||||
if QApplication.platformName().casefold() != 'offscreen':
|
||||
# Qt WebEngine embeds a native surface. When that surface first
|
||||
# appears inside the Metrics QScrollArea, Qt otherwise promotes
|
||||
# the visible parent chain to native windows, remapping and
|
||||
@@ -234,6 +249,7 @@ class _EndpointMapWidget(QWidget):
|
||||
"""Replace the displayed coordinate and status message."""
|
||||
refreshCompleted = self._retainMapDuringRefresh and not loading
|
||||
previousLocation = self._location
|
||||
|
||||
previousCoordinate = (
|
||||
(
|
||||
previousLocation.latitude,
|
||||
@@ -367,7 +383,6 @@ class _EndpointMapWidget(QWidget):
|
||||
self._mapError = message or self._unavailableText
|
||||
self._updateOverlay()
|
||||
|
||||
@QtCore.Slot(QWebEnginePage.RenderProcessTerminationStatus, int)
|
||||
def _renderProcessTerminated(self, status, exitCode):
|
||||
"""Convert an unexpected Chromium exit into the normal map fallback."""
|
||||
if self._rendererFailureHandled:
|
||||
@@ -402,9 +417,12 @@ class _EndpointMapWidget(QWidget):
|
||||
return
|
||||
|
||||
hasLocation = self._hasLocation()
|
||||
location = self._location if hasLocation else None
|
||||
accentColor = self.palette().color(QtGui.QPalette.ColorRole.Highlight).name()
|
||||
font = QtGui.QFontInfo(self.font())
|
||||
|
||||
location, accentColor, font = (
|
||||
self._location if hasLocation else None,
|
||||
self.palette().color(QtGui.QPalette.ColorRole.Highlight).name(),
|
||||
QtGui.QFontInfo(self.font()),
|
||||
)
|
||||
|
||||
state = {
|
||||
'markerVisible': hasLocation,
|
||||
@@ -478,6 +496,103 @@ class _EndpointMapWidget(QWidget):
|
||||
self.loadingSpinner.stop()
|
||||
|
||||
|
||||
class _UnavailableEndpointMapWidget(QWidget):
|
||||
"""Present endpoint-map state when Qt WebEngine is unavailable."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Create one persistent, lightweight fallback owned by the map card."""
|
||||
super().__init__(parent)
|
||||
|
||||
self._loading = False
|
||||
self._active = False
|
||||
self._message = ''
|
||||
self._loadingText = ''
|
||||
self._unavailableText = ''
|
||||
|
||||
self.setObjectName('EndpointMapWidget')
|
||||
self.setMinimumHeight(260)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
self.statusLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.statusLabel.setObjectName('EndpointMapPlaceholder')
|
||||
self.statusLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
|
||||
self.statusLabel.setWordWrap(False)
|
||||
|
||||
self.loadingSpinner = WaitingSpinner(
|
||||
self,
|
||||
center_on_parent=False,
|
||||
line_length=5,
|
||||
line_width=2,
|
||||
radius=4,
|
||||
lines=12,
|
||||
)
|
||||
self.loadingSpinner.setFixedSize(22, 22)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(12, 12, 12, 12)
|
||||
layout.setSpacing(8)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self.loadingSpinner)
|
||||
layout.addWidget(self.statusLabel)
|
||||
layout.addStretch(1)
|
||||
|
||||
def setLocation(self, _location, message='', *, loading=False):
|
||||
"""Present the shared endpoint status without attempting map rendering."""
|
||||
self._message = str(message or '')
|
||||
self._loading = bool(loading)
|
||||
self._updatePresentation()
|
||||
|
||||
def beginRefresh(self):
|
||||
"""Show refresh progress until the endpoint service publishes a result."""
|
||||
self._loading = True
|
||||
self._message = self._loadingText
|
||||
self._updatePresentation()
|
||||
|
||||
def setActive(self, active):
|
||||
"""Animate the fallback only while its containing page is visible."""
|
||||
self._active = bool(active)
|
||||
self._updatePresentation()
|
||||
|
||||
def updateTheme(self, _theme=None):
|
||||
"""Refresh palette-derived spinner color after an application theme change."""
|
||||
self._updatePresentation()
|
||||
|
||||
def setUnavailableText(self, text):
|
||||
"""Set the translated explanation shown in place of the map."""
|
||||
self._unavailableText = str(text or '')
|
||||
self._updatePresentation()
|
||||
|
||||
def setLoadingText(self, text):
|
||||
"""Set the translated refresh message."""
|
||||
self._loadingText = str(text or '')
|
||||
self._updatePresentation()
|
||||
|
||||
def _updatePresentation(self):
|
||||
"""Synchronize the persistent label and visibility-gated spinner."""
|
||||
if self._loading:
|
||||
message = self._loadingText or self._message
|
||||
else:
|
||||
message = self._message or self._unavailableText
|
||||
|
||||
self.statusLabel.setText(message)
|
||||
self.loadingSpinner.color = self.palette().color(QtGui.QPalette.ColorRole.Text)
|
||||
|
||||
if self._active and self._loading:
|
||||
self.loadingSpinner.start()
|
||||
else:
|
||||
self.loadingSpinner.stop()
|
||||
|
||||
|
||||
def _createEndpointMapWidget(parent):
|
||||
"""Create the rich renderer when available, otherwise its safe fallback."""
|
||||
if QWebEngineView is not None:
|
||||
return _EndpointMapWidget(parent)
|
||||
else:
|
||||
logger.warning('Qt WebEngine is unavailable; using the endpoint map fallback')
|
||||
|
||||
return _UnavailableEndpointMapWidget(parent)
|
||||
|
||||
|
||||
class _ValueRow(QtCore.QObject):
|
||||
"""Align one endpoint label, selectable value, and optional copy action."""
|
||||
|
||||
@@ -683,7 +798,7 @@ class EndpointInfoWidget(Mixins.ThemeAware, Mixins.QTranslatable, QFrame):
|
||||
self.mapTitleLabel = AppQLabel(_('Approximate Location'), parent=self.mapCard)
|
||||
self.mapTitleLabel.setObjectName('MetricCardTitle')
|
||||
|
||||
self.mapWidget = _EndpointMapWidget(self.mapCard)
|
||||
self.mapWidget = _createEndpointMapWidget(self.mapCard)
|
||||
|
||||
mapLayout = QVBoxLayout(self.mapCard)
|
||||
mapLayout.setContentsMargins(14, 12, 14, 12)
|
||||
|
||||
@@ -65,6 +65,8 @@ class MainWindow(AppQMainWindow):
|
||||
QtCore.QSize(1800, 960) if PLATFORM != 'Darwin' else DEFAULT_WINDOW_SIZE_DARWIN
|
||||
)
|
||||
|
||||
QT_FALLBACK_WINDOW_SIZE = QtCore.QSize(640, 480)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Create and register the built-in application pages."""
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -251,7 +253,15 @@ class MainWindow(AppQMainWindow):
|
||||
|
||||
restored = False
|
||||
|
||||
if restored:
|
||||
if (
|
||||
restored
|
||||
and PLATFORM == 'Darwin'
|
||||
and self.size() == self.QT_FALLBACK_WINDOW_SIZE
|
||||
):
|
||||
self._applyDefaultWindowSize(
|
||||
'saved macOS main-window geometry restored to the Qt fallback size'
|
||||
)
|
||||
elif restored:
|
||||
logger.info(
|
||||
f'restored main-window geometry: {self.geometry().getRect()}'
|
||||
)
|
||||
|
||||
@@ -82,6 +82,7 @@ class _GeometryWindow(AppQMainWindow):
|
||||
|
||||
DEFAULT_WINDOW_SIZE = QtCore.QSize(700, 500)
|
||||
|
||||
QT_FALLBACK_WINDOW_SIZE = MainWindow.QT_FALLBACK_WINDOW_SIZE
|
||||
_applyDefaultWindowSize = MainWindow._applyDefaultWindowSize
|
||||
_restoreLegacyWindowSize = MainWindow._restoreLegacyWindowSize
|
||||
_restoreMainWindowState = MainWindow._restoreMainWindowState
|
||||
@@ -359,8 +360,8 @@ class MainWindowGeometryTest(unittest.TestCase):
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
|
||||
def testValidIntentional640By480GeometryIsPreserved(self):
|
||||
"""Never reinterpret a successful small user geometry as Qt fallback."""
|
||||
def testDarwinRestoredQtFallbackSizeUsesCanonicalDefault(self):
|
||||
"""Replace macOS Qt fallback dimensions with the product default."""
|
||||
with isolatedSettings():
|
||||
expected = QtCore.QRect(40, 50, 640, 480)
|
||||
AppSettings.set(
|
||||
@@ -370,7 +371,31 @@ class MainWindowGeometryTest(unittest.TestCase):
|
||||
|
||||
window = _GeometryWindow()
|
||||
|
||||
with patch('Furious.Qt.QtWidgets.moveToCenter') as moveToCenter:
|
||||
with patch('Furious.Window.MainWindow.PLATFORM', 'Darwin'), patch(
|
||||
'Furious.Qt.QtWidgets.moveToCenter'
|
||||
) as moveToCenter:
|
||||
window.show()
|
||||
|
||||
self.assertEqual(window.size(), window.DEFAULT_WINDOW_SIZE)
|
||||
moveToCenter.assert_not_called()
|
||||
|
||||
window.close()
|
||||
window.deleteLater()
|
||||
|
||||
def testNonDarwinRestored640By480GeometryIsPreserved(self):
|
||||
"""Preserve a successful 640 by 480 restoration outside macOS."""
|
||||
with isolatedSettings():
|
||||
expected = QtCore.QRect(40, 50, 640, 480)
|
||||
AppSettings.set(
|
||||
'AppMainWindowGeometry',
|
||||
self._saveGeometry(expected),
|
||||
)
|
||||
|
||||
window = _GeometryWindow()
|
||||
|
||||
with patch('Furious.Window.MainWindow.PLATFORM', 'Windows'), patch(
|
||||
'Furious.Qt.QtWidgets.moveToCenter'
|
||||
) as moveToCenter:
|
||||
window.show()
|
||||
|
||||
self.assertEqual(window.size(), expected.size())
|
||||
|
||||
Reference in New Issue
Block a user