diff --git a/.github/workflows/deploy-pypi.yml b/.github/workflows/deploy-pypi.yml
index 43c513a..febdbe1 100644
--- a/.github/workflows/deploy-pypi.yml
+++ b/.github/workflows/deploy-pypi.yml
@@ -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 }}
diff --git a/AGENTS.md b/AGENTS.md
index 8d356df..0eaf3c6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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.
diff --git a/Deploy.py b/Deploy.py
index f3e6f6c..022915d 100644
--- a/Deploy.py
+++ b/Deploy.py
@@ -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' \n'
f'\n'
f' \n'
- f' \n'
+ f' \n'
f' \n'
f' \n'
f'\n'
+ f' \n'
+ f' \n'
+ f' \n'
+ f' \n'
+ f'\n'
f' \n'
f' \n'
f' \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',