Update workflow

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-23 16:41:07 +08:00
parent f8f70bf7ab
commit 38375bcce0
3 changed files with 265 additions and 105 deletions
+235 -79
View File
@@ -78,33 +78,63 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
deploy-binaries:
name: Deploy binaries on ${{ matrix.os }} Python ${{ matrix.python-version }}
name: Deploy ${{ matrix.id }} on ${{ matrix.os }} Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
fail-fast: false
matrix:
os: [ ubuntu-22.04, ubuntu-22.04-arm, windows-2025, macos-15-intel, macos-14 ]
python-version: [ "3.11", "3.13" ]
exclude:
- os: ubuntu-22.04
python-version: "3.11"
- os: windows-2025
python-version: "3.11"
- os: macos-15-intel
python-version: "3.11"
- os: macos-14
python-version: "3.11"
- os: ubuntu-22.04-arm
python-version: "3.13"
include:
- os: windows-2025
id: win7
- 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: windows-win7-amd64
os: windows-2025
python-version: "3.13"
python-architecture: x64
pyside6-version: "6.8.3"
windows-compatibility: windows
windows-architecture: amd64
- id: windows-arm64
os: windows-11-vs2026-arm
python-version: "3.13"
python-architecture: arm64
pyside6-version: "6.11.2"
windows-compatibility: ""
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:
# Last PySide6 version that supports macOS 10.9 & Python 3.11
PYSIDE6_LEGACY_VERSION: "6.4.3"
PYSIDE6_TARGET_VERSION: "6.8.3"
# PySide6 has Windows ARM64 build since 6.9.0
PYSIDE6_LATEST_VERSION: "6.9.1"
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"
steps:
- uses: actions/checkout@v7
- name: Install macOS dependencies
@@ -149,97 +179,223 @@ jobs:
if: runner.os == 'Linux'
- name: Install Windows dependencies
run: |
if [ "$RUNNER_ARCH" == "X64" ]; then
toolset_arch="x64"
elif [ "$RUNNER_ARCH" == "ARM64" ]; then
toolset_arch="arm64"
else
echo "Unknown runner architecture: $RUNNER_ARCH"
exit 1
fi
winget install --id WiXToolset.WiXCLI --version 6.0.2.0 -e --source winget --accept-package-agreements --accept-source-agreements
echo "C:/Program Files/WiX Toolset v6.0/bin" >> $GITHUB_PATH
echo "C:/Program Files/WiX Toolset v6.0/bin/${toolset_arch}" >> $GITHUB_PATH
dotnet tool install --global wix --version "${WIX_VERSION}"
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
if: runner.os == 'Windows'
- name: Check Windows dependencies
run: |
wix --version
wix extension add WixToolset.UI.wixext/6.0.2
wix extension add "WixToolset.UI.wixext/${WIX_VERSION}"
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 == 'win7'
if: matrix.id == 'windows-win7-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 != 'win7'
if: matrix.id != 'windows-win7-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: ${{ env.PYSIDE6_TARGET_VERSION }}
if: matrix.id == 'win7'
- name: Download Python dependencies & latest asset files
pyside6-version: ${{ matrix.pyside6-version }}
if: matrix.id == 'windows-win7-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'
from pathlib import Path
import re
excluded = {
'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 }}" != "windows-win7-amd64" ]]; then
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} \
"PySide6-Essentials==${{ matrix.pyside6-version }}" \
"PySide6-Addons==${{ matrix.pyside6-version }}"
fi
if [ "$RUNNER_OS" == "Linux" ] && [ "$RUNNER_ARCH" == "ARM64" ]; then
python -m pip install "numpy<2"
python -m pip install PySide6-Essentials==6.5.3
fi
python -m pip install -r .binary-requirements.txt
python -m pip install "Nuitka==${NUITKA_VERSION}" imageio requests
python -c "from PySide6 import QtCore; print(f'PySide6/Qt {QtCore.__version__}')"
- 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 == 'windows-win7-amd64'
- name: Build Windows7 bindings from source with patched Go
run: |
command -v go
go version
go env GOROOT GOOS GOARCH CGO_ENABLED
command -v gcc
gcc -dumpmachine
rm -rf win7-wheels
mkdir win7-wheels
python -m pip wheel \
--no-cache-dir \
--no-deps \
--no-binary=Xray-core,hysteria2,tun2socks \
--wheel-dir win7-wheels \
"Xray-core==${XRAY_CORE_VERSION}" \
"hysteria2==${HYSTERIA2_VERSION}" \
"tun2socks==${TUN2SOCKS_VERSION}"
python - <<'PY'
from pathlib import Path
wheels = sorted(Path('win7-wheels').glob('*.whl'))
print('Locally built wheels:')
for wheel in wheels:
print(f' {wheel.name}')
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
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
run: |
python -m pip install \
--only-binary=hysteria \
--no-cache-dir \
--no-deps \
"hysteria==${HYSTERIA_VERSION}"
if: matrix.id == 'windows-win7-amd64'
- name: Install supported binding wheels
run: |
if [[ "$RUNNER_OS" == "Windows" ]]; then
binary_policy="--only-binary=Xray-core,hysteria,hysteria2,tun2socks"
else
python -m pip install PySide6-Essentials==${PYSIDE6_TARGET_VERSION}
binary_policy=""
fi
python -m pip install -r requirements.txt
python -m pip install nuitka imageio requests
python -m pip install \
${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 architecture and imports
run: |
python VerifyWindowsArchitecture.py \
--expected "${{ matrix.windows-architecture }}" \
--bindings
if: runner.os == 'Windows'
- name: Download latest asset files
run: |
python Deploy.py --download
- name: Set up go
uses: actions/setup-go@v7
with:
go-version: "1.26"
check-latest: true
if: matrix.id != 'win7'
- name: Install go dependencies
run: |
if [[ "${{ matrix.id }}" == "win7" ]]; then
go_name="go-for-win7-windows-amd64"
go_file="${go_name}.zip"
curl -L -o ${go_file} https://github.com/XTLS/go-win7/releases/download/patched-1.26.7/${go_file}
7z x ${go_file} -o${go_name} -y
export GOROOT="$PWD/${go_name}"
export CGO_ENABLED=1
export PATH="${GOROOT}/bin:$PATH"
fi
go version
python -m pip install "Xray-core >= 1.8.8"
python -m pip install "hysteria2 >= 2.0.4"
python -m pip install "tun2socks > 2.5.2"
- name: Set up go legacy
uses: actions/setup-go@v7
with:
go-version: "1.20"
check-latest: true
- name: Install go legacy dependencies
run: |
go version
python -m pip install "hysteria > 1.3.5"
- name: Run deploy script
run: |
if [[ "${{ matrix.id }}" == "win7" ]]; then
export WIN_VER_COMPATIBLE="windows"
if [[ -n "${{ matrix.windows-compatibility }}" ]]; then
export WIN_VER_COMPATIBLE="${{ matrix.windows-compatibility }}"
fi
python Deploy.py
- name: Verify packaged Windows binaries
run: |
python VerifyWindowsArchitecture.py \
--expected "${{ matrix.windows-architecture }}" \
--tree "Furious-Deploy/Furious.dist"
if: runner.os == 'Windows'
- name: Store the distribution packages
uses: actions/upload-artifact@v7
with:
name: binary-distributions-${{ matrix.os }}-Python${{ matrix.python-version }}
name: binary-distributions-${{ matrix.id }}-Python${{ matrix.python-version }}
path: |
Furious-*.zip
Furious-*.msi
@@ -294,6 +450,6 @@ jobs:
with:
identifier: LorenEteval.Furious
release-tag: ${{ github.ref_name }}
installers-regex: '^Furious-.*\.zip$'
installers-regex: '^Furious-.*-(?:windows-amd64|windows11-arm64)\.zip$'
max-versions-to-keep: 5
token: ${{ secrets.WINGET_TOKEN }}
+5
View File
@@ -68,6 +68,11 @@
- For backend/process/platform work, verify error cleanup and bounded shutdown. For Qt ownership work, follow
`Furious/AGENTS.md` and `Furious/Qt/AGENTS.md`, use the `manage-qt-pyside6-lifetimes` skill, and run the relevant
lifetime tests.
- Windows 7 release bindings that require newer Go runtimes must use locally built wheels produced with the patched
`go-win7` toolchain; an ordinary modern PyPI wheel does not establish Windows 7 compatibility. A binding built with an
official Go release that still supports Windows 7 may use a verified prebuilt wheel.
- Architecture-specific Windows release jobs must verify the active Python, installed native extensions, and packaged
executable architecture before publishing uniquely named artifacts.
- Review for duplicated state authorities, mutation of persisted configuration during runtime preparation, broad
swallowed errors, unbounded waits/caches, generated-file edits without regeneration, and protocol branches that belong
in a capability.
+25 -26
View File
@@ -62,6 +62,21 @@ NUITKA_BINARY_VERSION_OPTION = (
PLATFORM_MACHINE_LOWER = PLATFORM_MACHINE.casefold()
if PLATFORM == 'Windows':
windowsArchitectures = {
'amd64': ('amd64', 'x64'),
'x86_64': ('amd64', 'x64'),
'arm64': ('arm64', 'arm64'),
'aarch64': ('arm64', 'arm64'),
}
try:
WINDOWS_ARTIFACT_ARCHITECTURE, WINDOWS_WIX_ARCHITECTURE = windowsArchitectures[
PLATFORM_MACHINE_LOWER
]
except KeyError:
raise RuntimeError(f'unsupported Windows architecture: {PLATFORM_MACHINE}')
if PLATFORM == 'Windows':
NUITKA_BINARY_VERSION_OPTION += f'--file-description=\"{APPLICATION_DESCRIPTION}\" '
@@ -126,10 +141,10 @@ if PLATFORM == 'Windows':
ARTIFACT_NAME = (
f'{APPLICATION_NAME}-{APPLICATION_VERSION}-'
f'{winVerCompatible}-{PLATFORM_MACHINE_LOWER}'
f'{winVerCompatible}-{WINDOWS_ARTIFACT_ARCHITECTURE}'
)
WIN_UNZIPPED = f'{APPLICATION_NAME}-{APPLICATION_VERSION}-{winVerCompatible}'
WIN_UNZIPPED = ARTIFACT_NAME
elif PLATFORM == 'Darwin':
value = versionToValue(PYSIDE6_VERSION)
@@ -493,28 +508,6 @@ def main():
os.makedirs(msifolder, exist_ok=True)
try:
# Requires "heat" in PATH
result = runExternalCommand(
'heat dir'.split()
+ [ROOT_DIR / DEPLOY_DIR_NAME / 'SourceDir']
+ '-cg FuriousComponents -dr INSTALLFOLDER -srd -scom -sreg -gg -out'.split()
+ [msifolder / 'FuriousFiles.wxs'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
except subprocess.CalledProcessError as err:
logger.error(f'run heat failed with returncode {err.returncode}')
printStandardStream(err.stdout, err.stderr)
sys.exit(EXIT_FAILURE)
else:
logger.info(f'run heat success')
printStandardStream(result.stdout, result.stderr)
# Create Product.wxs file
try:
with open(
@@ -547,10 +540,15 @@ def main():
f' <WixVariable Id=\"WixUILicenseRtf\" Value=\"LICENSE.rtf\" />\n'
f'\n'
f' <!-- Installation directory -->\n'
f' <StandardDirectory Id=\"ProgramFiles64Folder\">\n'
f' <StandardDirectory Id=\"ProgramFiles6432Folder\">\n'
f' <Directory Id=\"INSTALLFOLDER\" Name=\"{WIN_UNZIPPED}\" />\n'
f' </StandardDirectory>\n'
f'\n'
f' <!-- Files harvested by WiX for the selected architecture -->\n'
f' <ComponentGroup Id="FuriousComponents" Directory="INSTALLFOLDER">\n'
f' <Files Include="{xml_escape(str(ROOT_DIR / DEPLOY_DIR_NAME / "SourceDir" / "**"))}" />\n'
f' </ComponentGroup>\n'
f'\n'
f' <!-- Features -->\n'
f' <Feature Id=\"MainFeature\" Title=\"{APPLICATION_NAME}\" Level=\"1\">\n'
f' <ComponentGroupRef Id=\"FuriousComponents\" />\n'
@@ -606,8 +604,9 @@ def main():
[
'wix',
'build',
msifolder / 'FuriousFiles.wxs',
msifolder / 'Product.wxs',
'-arch',
WINDOWS_WIX_ARCHITECTURE,
'-o',
ROOT_DIR / f'{ARTIFACT_NAME}.msi',
'-ext',