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',