From 01f4ff7fd370882d8038428ed4a94b1f4d93252f Mon Sep 17 00:00:00 2001 From: Loren Eteval Date: Mon, 2 Jun 2025 15:19:16 +0800 Subject: [PATCH] Add linux AppImage release workflow Signed-off-by: Loren Eteval --- .github/workflows/deploy-pypi.yml | 68 +++++++++++--- Deploy.py | 141 +++++++++++++++++++++++++++++- 2 files changed, 195 insertions(+), 14 deletions(-) diff --git a/.github/workflows/deploy-pypi.yml b/.github/workflows/deploy-pypi.yml index 69f8f9e..c08f453 100644 --- a/.github/workflows/deploy-pypi.yml +++ b/.github/workflows/deploy-pypi.yml @@ -66,29 +66,54 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 deploy-binaries: - name: Deploy binaries on ${{ matrix.os }} + name: Deploy binaries on ${{ matrix.os }} Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }} continue-on-error: true strategy: matrix: - os: [ windows-2022, macos-13, macos-14 ] - python-version: [ "3.13" ] + os: [ ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-13, macos-14 ] + python-version: [ "3.12", "3.13" ] + exclude: + - os: ubuntu-22.04 + python-version: "3.12" + - os: windows-2022 + python-version: "3.12" + - os: macos-13 + python-version: "3.12" + - os: macos-14 + python-version: "3.12" + - os: ubuntu-22.04-arm + python-version: "3.13" env: # Last PySide6 version that supports macOS 10.9 & Python 3.11 PYSIDE6_LEGACY_VERSION: "6.4.3" PYSIDE6_TARGET_VERSION: "6.8.3" - # Set by command line - # PYSIDE6_Windows_X64_TARGET_VERSION - # PYSIDE6_macOS_X64_TARGET_VERSION - # PYSIDE6_macOS_ARM64_TARGET_VERSION # PySide6 has Windows ARM64 build since 6.9.0 - PYSIDE6_Windows_ARM64_TARGET_VERSION: "6.9.0" + PYSIDE6_LATEST_VERSION: "6.9.0" steps: - name: Set platform environment variables version run: | echo "PYSIDE6_Windows_X64_TARGET_VERSION=$PYSIDE6_TARGET_VERSION" >> $GITHUB_ENV + echo "PYSIDE6_Windows_ARM64_TARGET_VERSION=$PYSIDE6_LATEST_VERSION" >> $GITHUB_ENV echo "PYSIDE6_macOS_X64_TARGET_VERSION=$PYSIDE6_TARGET_VERSION" >> $GITHUB_ENV echo "PYSIDE6_macOS_ARM64_TARGET_VERSION=$PYSIDE6_TARGET_VERSION" >> $GITHUB_ENV + echo "PYSIDE6_Linux_X64_TARGET_VERSION=$PYSIDE6_TARGET_VERSION" >> $GITHUB_ENV + echo "PYSIDE6_Linux_ARM64_TARGET_VERSION=6.6.3.1" >> $GITHUB_ENV + case "${{ runner.arch }}" in + X64) + echo "MAPPED_RUNNER_ARCH=x86_64" >> $GITHUB_ENV + ;; + ARM64) + echo "MAPPED_RUNNER_ARCH=aarch64" >> $GITHUB_ENV + ;; + ARM) + echo "MAPPED_RUNNER_ARCH=armv7" >> $GITHUB_ENV + ;; + *) + echo "Unknown arch: ${{ runner.arch }}" + exit 1 + ;; + esac - uses: actions/checkout@v4 - name: Install macOS dependencies run: | @@ -101,6 +126,21 @@ jobs: 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 + sudo apt install -y libxcb-cursor0 + sudo apt install -y qt6-base-dev + sudo apt install -y patchelf ccache + echo "QMAKE=$(which qmake6)" >> $GITHUB_ENV + echo "Mapped runner architecture is $MAPPED_RUNNER_ARCH" + mkdir .linuxdeploy-bin + echo "$PWD/.linuxdeploy-bin" >> $GITHUB_PATH + wget -P .linuxdeploy-bin https://github.com/linuxdeploy/linuxdeploy/releases/latest/download/linuxdeploy-$MAPPED_RUNNER_ARCH.AppImage + wget -P .linuxdeploy-bin https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/latest/download/linuxdeploy-plugin-qt-$MAPPED_RUNNER_ARCH.AppImage + wget -P .linuxdeploy-bin https://github.com/AppImage/appimagetool/releases/latest/download/appimagetool-$MAPPED_RUNNER_ARCH.AppImage + chmod +x .linuxdeploy-bin/* + if: runner.os == 'Linux' - name: Set up Python uses: actions/setup-python@v4 with: @@ -110,7 +150,7 @@ jobs: check-latest: true - name: Set up Python venv run: | - if [ "$RUNNER_OS" == "macOS" ]; then + if [ "$RUNNER_OS" == "macOS" ] || [ "$RUNNER_OS" == "Linux" ]; then python3 -m venv .venv source .venv/bin/activate elif [ "$RUNNER_OS" == "Windows" ]; then @@ -122,13 +162,14 @@ jobs: fi python -c "import sys; print(sys.version)" python -m pip install --upgrade pip - - name: Install pre-built numpy wheel on Windows ARM64 - run: | - python -m pip install "https://github.com/LorenEteval/numpy-2.2.4-cp313-win_arm64/releases/download/2.2.4/numpy-2.2.4-cp313-cp313-win_arm64.whl" - if: runner.os == 'Windows' && runner.arch == 'ARM64' - name: Download Python dependencies & latest asset files run: | python -m pip install setuptools wheel + if [ "$RUNNER_OS" == "Linux" ] && [ "$RUNNER_ARCH" == "ARM64" ]; then + python -m pip install "numpy<2" + elif [ "$RUNNER_OS" == "Windows" ] && [ "$RUNNER_ARCH" == "ARM64" ]; then + python -m pip install "https://github.com/LorenEteval/numpy-2.2.4-cp313-win_arm64/releases/download/2.2.4/numpy-2.2.4-cp313-cp313-win_arm64.whl" + fi python -m pip install PySide6-Essentials==$PYSIDE6_${{ runner.os }}_${{ runner.arch }}_TARGET_VERSION python -m pip install -r requirements.txt python -m pip install nuitka imageio @@ -180,6 +221,7 @@ jobs: path: | *.zip *.dmg + *.AppImage github-release: name: >- diff --git a/Deploy.py b/Deploy.py index 49876fa..a75b9a3 100644 --- a/Deploy.py +++ b/Deploy.py @@ -40,6 +40,21 @@ EXIT_FAILURE = 1 DEPLOY_DIR_NAME = f'{APPLICATION_NAME}-Deploy' +NUITKA_BINARY_VERSION_OPTION = ( + f'--company-name={APPLICATION_NAME} ' + f'--product-name={APPLICATION_NAME} ' + f'--file-version={APPLICATION_VERSION} ' + f'--product-version={APPLICATION_VERSION} ' +) + +if PLATFORM == 'Windows': + NUITKA_BINARY_VERSION_OPTION += f'--file-description=\"A GUI proxy client based on PySide6. Support Xray-core & hysteria\" ' + +if PLATFORM == 'Windows' or PLATFORM == 'Darwin': + NUITKA_BINARY_VERSION_OPTION += ( + f'--copyright=\"Copyright (C) 2024 Loren Eteval \" ' + ) + if PLATFORM == 'Windows': NUITKA_BUILD = ( f'python -m nuitka ' @@ -47,6 +62,7 @@ if PLATFORM == 'Windows': f'--disable-console ' f'--assume-yes-for-downloads ' f'--include-package-data=Furious ' + f'{NUITKA_BINARY_VERSION_OPTION}' f'--windows-icon-from-ico=\"Icons/png/rocket-takeoff-window.png\" ' f'--force-stdout-spec=^%TEMP^%/_Furious_Enable_Stdout ' f'--force-stderr-spec=^%TEMP^%/_Furious_Enable_Stderr ' @@ -63,12 +79,24 @@ elif PLATFORM == 'Darwin': f'--disable-console ' f'--assume-yes-for-downloads ' f'--include-package-data=Furious ' + f'{NUITKA_BINARY_VERSION_OPTION}' f'--macos-create-app-bundle ' f'--macos-app-icon=\"Icons/png/rocket-takeoff-window.png\" ' f'--macos-app-name=\"Furious\" ' f'Furious-GUI.py ' f'--output-dir=\"{ROOT_DIR / DEPLOY_DIR_NAME}\"' ) +elif PLATFORM == 'Linux': + NUITKA_BUILD = ( + f'python -m nuitka ' + f'--standalone --plugin-enable=pyside6 ' + f'--disable-console ' + f'--assume-yes-for-downloads ' + f'--include-package-data=Furious ' + f'{NUITKA_BINARY_VERSION_OPTION}' + f'Furious ' + f'--output-dir=\"{ROOT_DIR / DEPLOY_DIR_NAME}\"' + ) else: # Deploy: Not implemented NUITKA_BUILD = '' @@ -117,6 +145,24 @@ elif PLATFORM == 'Darwin': f'\"{ROOT_DIR / MAC_DMG_FILENAME}\" ' f'\"{MAC_APP_DIR}\"' ) +elif PLATFORM == 'Linux': + LINUX_APP_DIR = ROOT_DIR / 'AppDir' + LINUX_ICON_FILE = 'rocket-takeoff-window-512x512.png' + LINUX_DESKTOP_FILE = f'{APPLICATION_NAME}.desktop' + + ARTIFACT_NAME = ( + f'{APPLICATION_NAME}-{APPLICATION_VERSION}-linux-{PLATFORM_MACHINE.lower()}' + ) + + LINUX_APPIMAGE_FILENAME = f'{ARTIFACT_NAME}.AppImage' + LINUX_CREATE_APPIMAGE_CMD = ( + f'linuxdeploy-{PLATFORM_MACHINE}.AppImage ' + f'--appdir AppDir ' + f'-d \"{LINUX_APP_DIR / LINUX_DESKTOP_FILE}\" ' + f'-i \"{LINUX_APP_DIR / LINUX_ICON_FILE}\" ' + f'--plugin qt ' + f'--output appimage' + ) else: # Deploy: Not implemented ARTIFACT_NAME = '' @@ -190,7 +236,7 @@ def cleanup(): # More cleanup on Darwin try: # Remove artifact - os.remove(ROOT_DIR / f'{ARTIFACT_NAME}.dmg') + os.remove(ROOT_DIR / MAC_DMG_FILENAME) except Exception as ex: # Any non-exit exceptions @@ -204,6 +250,27 @@ def cleanup(): except Exception as ex: # Any non-exit exceptions + logger.error(f'remove app dir failed: {ex}') + else: + logger.info(f'remove app dir success') + elif PLATFORM == 'Linux': + # More cleanup on Linux + try: + # Remove artifact + os.remove(ROOT_DIR / LINUX_APPIMAGE_FILENAME) + except Exception as ex: + # Any non-exit exceptions + + logger.error(f'remove artifact failed: {ex}') + else: + logger.info(f'remove artifact success') + + try: + # Remove app folder + shutil.rmtree(LINUX_APP_DIR) + except Exception as ex: + # Any non-exit exceptions + logger.error(f'remove app dir failed: {ex}') else: logger.info(f'remove app dir success') @@ -352,6 +419,78 @@ def main(): else: logger.info(f'generate dmg success: {MAC_DMG_FILENAME}') + printStandardStream(result.stdout, result.stderr) + elif PLATFORM == 'Linux': + try: + shutil.rmtree(LINUX_APP_DIR) + except FileNotFoundError: + pass + except Exception: + # Any non-exit exceptions + + raise + + try: + os.makedirs(LINUX_APP_DIR / 'usr' / 'bin', exist_ok=True) + except Exception: + # Any non-exit exceptions + + raise + + iconbase = 'rocket-takeoff-window-512x512' + + shutil.copytree( + ROOT_DIR / DEPLOY_DIR_NAME / f'{APPLICATION_NAME}.dist', + LINUX_APP_DIR / 'usr' / 'bin', + dirs_exist_ok=True, + ) + shutil.copy( + ROOT_DIR / 'Icons' / 'png' / f'{iconbase}.png', + LINUX_APP_DIR, + ) + + try: + with open( + LINUX_APP_DIR / LINUX_DESKTOP_FILE, + 'w', + encoding='utf-8', + ) as file: + file.write( + f'[Desktop Entry]\n' + f'Type=Application\n' + f'Name={APPLICATION_NAME}\n' + f'Exec={APPLICATION_NAME}.bin\n' + f'Icon={iconbase}\n' + f'Categories=Utility\n' + ) + except Exception: + # Any non-exit exceptions + + raise + + logger.info('generating AppImage') + + # https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/blob/master/README.md + os.environ['LDAI_OUTPUT'] = LINUX_APPIMAGE_FILENAME + os.environ['LINUXDEPLOY_OUTPUT_VERSION'] = APPLICATION_VERSION + + try: + result = runExternalCommand( + LINUX_CREATE_APPIMAGE_CMD, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + check=True, + ) + except subprocess.CalledProcessError as err: + logger.error(f'generate AppImage failed with returncode {err.returncode}') + + printStandardStream(err.stdout, err.stderr) + + sys.exit(EXIT_FAILURE) + else: + logger.info(f'generate AppImage success: {LINUX_APPIMAGE_FILENAME}') + printStandardStream(result.stdout, result.stderr) else: # Deploy: Not implemented