# Multi-platform build + packaging workflow # - Builds a pyinstaller executable on Windows, Debian, Arch, Fedora. # - Packages using fpm into .deb, .rpm, and pacman (.pkg.tar.zst) files with explicit filenames. # - Creates a GitHub Release with the produced artifacts. # # Notes: # - Poetry is installed via snok/install-poetry@v1 in all jobs. # - fpm gem is pinned to 1.16.0 in the examples; change as needed. # - Fedora job uses tarball downloads for pyenv and python-build (non-interactive, CI-friendly). name: Build Multi-Platform Binaries on: workflow_dispatch: inputs: branch: description: "Branch to build from" required: false default: "main" type: string jobs: description: "Comma-separated jobs to run (e.g., build-windows,build-debian,build-arch,build-rhel)" required: true default: "build-windows,build-debian,build-arch,build-rhel" DO_RELEASE: description: "Set to 'true' to create a GitHub release after successful builds" required: false default: "false" UPLOAD_S3: description: "Set to 'true' to upload build artifacts to S3 after builds" required: false default: "false" pull_request: branches: [ main ] permissions: contents: write env: # change this if you prefer a different pinned fpm version FPM_VERSION: "1.16.0" # Personal Access Token for HTTPS git operations (populate in repository secrets) CI_CD_PAT: ${{ secrets.CI_CD_PAT }} CI_CD: true jobs: run-unit-tests-linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Display build information run: | echo "Event: ${{ github.event_name }}" echo "Branch: ${{ github.event.inputs.branch || github.head_ref || github.ref }}" echo "Jobs to run: ${{ github.event.inputs.jobs || 'build-windows,build-debian,build-arch,build-rhel' }}" echo "Create release: ${{ github.event.inputs.DO_RELEASE || 'false' }}" echo "Upload to S3: ${{ github.event.inputs.UPLOAD_S3 || 'false' }}" - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install Poetry uses: snok/install-poetry@v1 - name: Install dependencies run: | poetry install - name: Run tests run: | poetry run pytest tests/ -v run-unit-tests-windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-create: true virtualenvs-in-project: true - name: Ensure Poetry is on PATH (Windows) shell: pwsh run: | # Add Poetry user bin to PATH for subsequent steps in this job $poetryPath = Join-Path $env:USERPROFILE ".local\bin" Write-Output $poetryPath >> $Env:GITHUB_PATH - name: Install dependencies run: | poetry install - name: Run tests run: | poetry run pytest tests/ -v build-windows: needs: [run-unit-tests-linux, run-unit-tests-windows] if: ${{ github.event_name == 'pull_request' || contains(github.event.inputs.jobs, 'build-windows') }} runs-on: windows-latest steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-create: true virtualenvs-in-project: true - name: Ensure Poetry is on PATH (Windows) shell: pwsh run: | # Add Poetry user bin to PATH for subsequent steps in this job $poetryPath = Join-Path $env:USERPROFILE ".local\bin" Write-Output $poetryPath >> $Env:GITHUB_PATH - name: Install dependencies run: | poetry install - name: Build Windows executable run: | # Use the Windows spec file so packaging is consistent and reproducible poetry run pyinstaller scripts/spec_scripts/android-file-handler-windows.spec - name: Upload Windows artifact uses: actions/upload-artifact@v4 with: name: windows-binary path: | dist/**/android-file-handler*.exe dist/android-file-handler.exe build-debian: needs: [run-unit-tests-linux, run-unit-tests-windows] if: ${{ github.event_name == 'pull_request' || contains(github.event.inputs.jobs, 'build-debian') }} env: DISTRO_TYPE: debian runs-on: ubuntu-latest container: image: python:3.12-slim steps: - name: Install system dependencies & gem fpm (include Tcl/Tk) run: | set -euo pipefail apt-get update # Install Tcl/Tk runtimes, dev headers and common X libraries required by tkinter apt-get install -y --no-install-recommends \ curl git build-essential ruby ruby-dev gcc make zlib1g-dev ca-certificates python3-tk \ tcl8.6 tk8.6 tcl8.6-dev tk8.6-dev libx11-6 libxext6 libxrender1 libxcb1 # install pinned fpm to the system gem dir (will be available under gem env's EXECUTABLE DIRECTORY or /usr/local/bin) gem install --no-document -v "${FPM_VERSION}" fpm - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-create: true virtualenvs-in-project: true - name: Configure Poetry run: | echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV export PATH="$HOME/.local/bin:$PATH" poetry config virtualenvs.create true poetry config virtualenvs.in-project true - name: Install dependencies & build executable run: | export PATH="$HOME/.local/bin:$PATH" poetry env use python3.12 || true # Debug: show tkinter/_tkinter and Tcl library discovery in the Poetry venv poetry run python -c 'import tkinter, _tkinter, sys; print("tkinter=", getattr(tkinter, "__file__", None)); print("_tkinter=", getattr(_tkinter, "__file__", None)); import tkinter as tk; print("TCL_LIBRARY=", tk.Tcl().eval("info library"))' # Build distro-specific package layout using the build script via Poetry poetry run python scripts/build_package_linux.py - name: Package .deb (fpm) shell: bash run: | set -euo pipefail export PATH="$HOME/.local/bin:$PATH" VERSION="$(poetry version -s)" PKG_DIR="pkg_dist_debian" mkdir -p dist # Debug listing echo "Packaging from $PKG_DIR" ls -la "$PKG_DIR" || true ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" PKG_ITEMS=( "usr/local/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" ) if [ -f "$ICON_PATH" ]; then PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" ) else echo "Note: icon not present, packaging without icon" fi fpm -s dir -t deb -n android-file-handler -v "$VERSION" \ --architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ --after-install scripts/debian_postinst.sh \ -p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" "${PKG_ITEMS[@]}" - name: Upload Debian .deb uses: actions/upload-artifact@v4 with: name: debian-package path: | dist/android-file-handler_*.deb pkg_dist_debian/** build-arch: needs: [run-unit-tests-linux, run-unit-tests-windows] if: ${{ github.event_name == 'pull_request' || contains(github.event.inputs.jobs, 'build-arch') }} env: DISTRO_TYPE: arch runs-on: ubuntu-latest container: image: archlinux:latest steps: - name: Install system dependencies (Arch) and system Ruby run: | set -euo pipefail pacman -Syu --noconfirm pacman -S --noconfirm ruby base-devel curl git tar ca-certificates tk tcl libx11 libxext libxrender libxcb # Install fpm system-wide and pin version so fpm will be in /usr/in gem install --no-document erb gem install --no-document -v "${FPM_VERSION}" fpm --bindir /usr/bin # persist system bindir to subsequent steps (usually already on PATH) echo "/usr/local/bin" >> $GITHUB_PATH - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install Poetry uses: snok/install-poetry@v1 with: version: latest virtualenvs-create: true virtualenvs-in-project: true - name: Configure Poetry run: | echo 'export PATH="$HOME/bin:$PATH"' >> $GITHUB_ENV export PATH="$HOME/bin:$PATH" - name: Install dependencies & build executable run: | export PATH="$HOME/bin:/usr/bin:$PATH" # Use unified build script to produce pkg_dist_arch layout via Poetry poetry run python scripts/build_package_linux.py - name: Package pacman (fpm) shell: bash run: | set -euo pipefail export PATH="$HOME/.local/bin:/usr/bin:$PATH" VERSION="$(poetry version -s)" PKG_DIR="pkg_dist_arch" mkdir -p dist echo "Packaging from $PKG_DIR" ls -la "$PKG_DIR" || true ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" PKG_ITEMS=( "usr/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" ) if [ -f "$ICON_PATH" ]; then PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" ) else echo "Note: icon not present, packaging without icon" fi fpm -s dir -t pacman -n android-file-handler -v "$VERSION" \ --architecture x86_64 --prefix /usr/bin \ -p "dist/android-file-handler-${VERSION}-1-x86_64.pkg.tar.zst" -C "$PKG_DIR" "${PKG_ITEMS[@]}" - name: Upload Arch package uses: actions/upload-artifact@v4 with: name: arch-package path: | dist/*.pkg.tar.* pkg_dist_arch/** build-rhel: needs: [run-unit-tests-linux, run-unit-tests-windows] if: ${{ github.event_name == 'pull_request' || contains(github.event.inputs.jobs, 'build-rhel') }} env: DISTRO_TYPE: rhel runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Update OS and install Docker run: | set -euo pipefail sudo apt-get update # Remove possible conflicting containerd packages that can block docker.io installation sudo apt-get remove -y --purge containerd containerd.io || true sudo apt-get autoremove -y || true # Try installing docker.io from distro packages if ! sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg lsb-release docker.io git build-essential; then echo "apt install docker.io failed; falling back to Docker convenience script" # Fallback: use Docker's official convenience script curl -fsSL https://get.docker.com | sudo sh fi sudo usermod -aG docker $USER || true - name: Pull Fedora container image run: | docker pull fedora:latest - name: Start Fedora container (background) and mount repo run: | # Run container with workspace mounted docker run -d --name rhel-build -v "$PWD":/workspace -w /workspace fedora:latest sleep infinity - name: Prepare container build environment (dnf, pyenv, python, poetry, fpm) run: | set -euo pipefail # Update the Fedora image and install build deps docker exec rhel-build bash -lc "dnf -y update && dnf -y install gcc make zlib-devel bzip2 bzip2-devel readline-devel sqlite-devel openssl-devel libffi-devel wget tar git curl ruby rubygems rpm-build redhat-rpm-config gcc-c++ patch which xz-devel" # Install pyenv into the container (if not present) docker exec rhel-build bash -lc "[ -d \"/root/.pyenv\" ] || git clone https://github.com/pyenv/pyenv.git /root/.pyenv && mkdir -p /root/.pyenv/plugins && [ -d \"/root/.pyenv/plugins/python-build\" ] || git clone https://github.com/pyenv/pyenv-build.git /root/.pyenv/plugins/python-build" # Install Python 3.12 via pyenv and set global using explicit pyenv binary path # Use single quotes so $PYENV_ROOT and $PATH are expanded inside the container's shell docker exec rhel-build bash -lc 'export PYENV_ROOT="/root/.pyenv"; export PATH="$PYENV_ROOT/bin:$PATH"; /root/.pyenv/bin/pyenv install -s 3.12.0; /root/.pyenv/bin/pyenv global 3.12.0; /root/.pyenv/bin/pyenv rehash' # Install Poetry inside the container using the official installer (install to /root/.local/bin) # Use single quotes so PATH is evaluated inside the container shell rather than the runner docker exec rhel-build bash -lc 'export PATH="/root/.pyenv/shims:/root/.pyenv/bin:$PATH"; curl -sSL https://install.python-poetry.org | python3 - --yes' # Verify Poetry is available via explicit path if not on PATH docker exec rhel-build bash -lc "/root/.local/bin/poetry --version || echo 'Poetry not found in /root/.local/bin'" # Install fpm (Ruby gem) and ensure rpm build tools exist docker exec rhel-build bash -lc "dnf -y install ruby rubygems make && gem install --no-document -v \"${FPM_VERSION}\" fpm" - name: Build inside container using Poetry run: | set -euo pipefail # Use poetry inside the container to build package layouts. Use explicit paths so the runner shell # doesn't expand $HOME; expansion should occur inside the container. docker exec rhel-build bash -lc 'export PATH="/root/.local/bin:/root/.pyenv/shims:/root/.pyenv/bin:$PATH"; export CI_CD=true; export DISTRO_TYPE=rhel; cd /workspace && /root/.local/bin/poetry install --no-interaction && /root/.local/bin/poetry run python scripts/build_package_linux.py' - name: Package RHEL (fpm) inside container shell: bash run: | # Run the entire packaging flow inside the Fedora container so Poetry, PATH and arrays are evaluated in-container. docker exec rhel-build bash -lc ' set -euo pipefail VERSION="$(/root/.local/bin/poetry version -s)" PKG_DIR="pkg_dist_rhel" mkdir -p dist || true echo "Packaging from $PKG_DIR (version=$VERSION)" ls -la "$PKG_DIR" || true ICON_PATH="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" PKG_ITEMS=( "usr/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" ) if [ -f "$ICON_PATH" ]; then PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" ) else echo "Note: icon not present, packaging without icon" fi # Run fpm inside the container to produce an RPM cd /workspace exec fpm -s dir -t rpm -n android-file-handler -v "$VERSION" --architecture x86_64 --prefix /usr/bin --after-install scripts/rhel_postinst.sh -p "dist/android-file-handler-${VERSION}.x86_64.rpm" -C "$PKG_DIR" "${PKG_ITEMS[@]}" ' - name: Upload RHEL artifacts uses: actions/upload-artifact@v4 with: name: rhel-package path: | dist/*.rpm pkg_dist_rhel/** create-release: needs: [run-unit-tests-linux, run-unit-tests-windows, build-windows, build-debian, build-arch, build-rhel] if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.DO_RELEASE == 'true' && needs.build-windows.result == 'success' && needs.build-debian.result == 'success' && needs.build-arch.result == 'success' && needs.build-rhel.result == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Install Poetry uses: snok/install-poetry@v1 - name: Get version id: version run: echo "version=$(poetry version -s)" >> $GITHUB_OUTPUT - name: Download all artifacts uses: actions/download-artifact@v4 with: merge-multiple: true path: ./binaries - name: Prepare release files run: | mkdir -p ./release-files find ./binaries -name "*.exe" -exec cp {} ./release-files/ \; || true find ./binaries -name "*.deb" -exec cp {} ./release-files/ \; || true find ./binaries -name "*.rpm" -exec cp {} ./release-files/ \; || true find ./binaries -name "*.pkg.tar.*" -exec cp {} ./release-files/ \; || true - name: Create Release uses: softprops/action-gh-release@v1 with: tag_name: v${{ steps.version.outputs.version }} name: Release v${{ steps.version.outputs.version }} files: ./release-files/* draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} upload-s3: needs: [run-unit-tests-linux, run-unit-tests-windows, build-windows, build-debian, build-arch, build-rhel] if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.UPLOAD_S3 == 'true' || contains(github.event.inputs.jobs, 'upload-s3')) }} runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.head_ref || github.ref }} - name: Install AWS CLI run: | python -m pip install --upgrade pip awscli - name: Download build artifacts uses: actions/download-artifact@v4 with: merge-multiple: true path: ./binaries - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v2 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} - name: Upload artifacts to S3 run: | set -euo pipefail if [ -z "${{ secrets.S3_BUCKET }}" ]; then echo "S3_BUCKET secret not set; skipping upload" exit 0 fi aws s3 sync ./binaries s3://${{ secrets.S3_BUCKET }}/builds/${{ github.run_id }}/ --acl private env: AWS_PAGER: "" sync-wiki: needs: create-release if: always() && needs.create-release.result == 'success' uses: ./.github/workflows/sync-wiki.yml permissions: contents: write