3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
284 lines
14 KiB
YAML
284 lines
14 KiB
YAML
name: AppImage
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- "v*.*.*"
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: "Optional release tag to build from (e.g. v0.5.8). Leave empty to build from the selected ref (usually main) -- useful for verifying the recipe before cutting a tag."
|
|
required: false
|
|
default: ""
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
jobs:
|
|
build:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
# workflow_dispatch input: explicit tag; tag push: ref already there.
|
|
ref: ${{ github.event.inputs.tag || github.ref }}
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Install build prerequisites
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
python -m pip install --upgrade build
|
|
|
|
- name: Pre-install appimagetool (extract-and-wrap, FUSE-free)
|
|
# GitHub Actions ubuntu-24.04 runners cannot mount AppImages
|
|
# via FUSE in the sandboxed environment. Download appimagetool
|
|
# ourselves, extract its squashfs payload, and put a thin
|
|
# wrapper on PATH at /usr/local/bin/appimagetool that exec's
|
|
# into the extracted AppRun.
|
|
run: |
|
|
cd /tmp
|
|
wget -q https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool.AppImage
|
|
chmod +x appimagetool.AppImage
|
|
./appimagetool.AppImage --appimage-extract >/dev/null
|
|
sudo mv squashfs-root /opt/appimagetool
|
|
sudo tee /usr/local/bin/appimagetool >/dev/null <<'WRAPPER'
|
|
#!/bin/sh
|
|
exec /opt/appimagetool/AppRun "$@"
|
|
WRAPPER
|
|
sudo chmod +x /usr/local/bin/appimagetool
|
|
appimagetool --version || true
|
|
|
|
- name: Build winpodx wheel
|
|
run: python -m build --wheel --outdir dist
|
|
|
|
- name: Stage AppDir with portable Python + winpodx
|
|
# Bypass python-appimage entirely -- it does its own
|
|
# appimagetool download that ignores our PATH wrapper and
|
|
# silently fails on CI. Instead: drop a python-build-standalone
|
|
# portable Python 3.11 into AppDir, install winpodx into its
|
|
# site-packages, write AppRun + desktop + icon, then hand off
|
|
# to bundle-system-bins.sh for the FreeRDP overlay.
|
|
#
|
|
# Thin AppImage (0.6.0 item A): we no longer pip-install
|
|
# podman-compose into the bundled interpreter, and the dnf step
|
|
# below no longer installs the podman stack. Rootless podman
|
|
# cannot be bundled (host systemd / subuid integration); the
|
|
# AppImage now requires the user to install podman / docker /
|
|
# libvirt from the host package manager. See #357 / #363 for the
|
|
# root cause this redesign removes.
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p AppDir/opt AppDir/usr/bin AppDir/usr/lib
|
|
# Portable Python 3.11 with linked OpenSSL, gnu-linux x86_64.
|
|
# Pinned to a known-good astral-sh python-build-standalone release.
|
|
# Bump the tag + version pair together when refreshing.
|
|
PYBUILD_URL="https://github.com/astral-sh/python-build-standalone/releases/download/20240909/cpython-3.11.10+20240909-x86_64-unknown-linux-gnu-install_only.tar.gz"
|
|
curl -fsSL "${PYBUILD_URL}" -o /tmp/python-standalone.tar.gz
|
|
# Supply-chain: verify against the upstream .sha256 sidecar
|
|
# (astral-sh publishes one per asset). The portable interpreter
|
|
# executes winpodx's code -- a tampered asset would run with the
|
|
# user's RDP password + agent token, so fail closed on mismatch.
|
|
curl -fsSL "${PYBUILD_URL}.sha256" -o /tmp/python-standalone.sha256
|
|
echo "$(cat /tmp/python-standalone.sha256) /tmp/python-standalone.tar.gz" | sha256sum -c -
|
|
tar -xzf /tmp/python-standalone.tar.gz -C AppDir/opt/
|
|
# Tarball extracts to AppDir/opt/python/ on the install_only build.
|
|
AppDir/opt/python/bin/python3 -m pip install --upgrade pip
|
|
WHEEL="$(ls -t dist/winpodx-*.whl | head -n1)"
|
|
AppDir/opt/python/bin/python3 -m pip install --no-cache-dir "${WHEEL}[gui,reverse-open]"
|
|
# AppRun
|
|
cat > AppDir/AppRun <<'APPRUN'
|
|
#!/bin/sh
|
|
APPDIR="$(dirname "$(readlink -f "$0")")"
|
|
export PATH="$APPDIR/usr/bin:$APPDIR/opt/python/bin:$PATH"
|
|
export LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/opt/python/lib:$LD_LIBRARY_PATH"
|
|
export PYTHONPATH="$APPDIR/opt/python/lib/python3.11/site-packages:$PYTHONPATH"
|
|
# Invoke via `python3 -m winpodx`, NOT the pip-generated
|
|
# bin/winpodx console script. pip bakes an absolute build-time
|
|
# shebang (#!/home/runner/work/.../AppDir/opt/python/bin/python3)
|
|
# into bin/winpodx that doesn't exist at AppImage-extract time
|
|
# -> "bad interpreter". The -m form runs the module through the
|
|
# bundled interpreter directly and ignores the broken shebang.
|
|
exec "$APPDIR/opt/python/bin/python3" -m winpodx "$@"
|
|
APPRUN
|
|
chmod +x AppDir/AppRun
|
|
# Desktop + icon -- appimagetool expects them in AppDir root.
|
|
cp packaging/appimage/recipe/winpodx.desktop AppDir/
|
|
cp packaging/appimage/recipe/winpodx.png AppDir/
|
|
# Symlink common icon location too (some launchers look here)
|
|
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
|
|
cp packaging/appimage/recipe/winpodx.png AppDir/usr/share/icons/hicolor/256x256/apps/winpodx.png
|
|
# License texts (LGPL/GPL/Apache/PSF redistribution obligation --
|
|
# the fat AppImage redistributes third-party binaries, so their
|
|
# license + NOTICE texts must travel with it). winpodx's own MIT
|
|
# LICENSE + the aggregate THIRD_PARTY_LICENSES.md go at the doc
|
|
# root; the Fedora-bundled binaries' texts are added by the
|
|
# docker step below under third-party/. PyPI deps (PySide6/Qt,
|
|
# Pillow, cairosvg, pyxdg) already carry their license in their
|
|
# site-packages *.dist-info inside AppDir/opt/python.
|
|
mkdir -p AppDir/usr/share/doc/winpodx/third-party
|
|
cp LICENSE THIRD_PARTY_LICENSES.md AppDir/usr/share/doc/winpodx/
|
|
# python-build-standalone ships its license files; surface them.
|
|
find AppDir/opt/python -maxdepth 3 -iname "LICENSE*" -path "*python*" \
|
|
-exec cp {} AppDir/usr/share/doc/winpodx/third-party/python-LICENSE.txt \; 2>/dev/null || true
|
|
# LGPL compliance (legal gate): the fat AppImage REDISTRIBUTES the
|
|
# PySide6/Qt6 binaries (LGPL-3.0), so their license text must travel
|
|
# with it. The PySide6 wheels do NOT ship the LGPL/GPL texts (verified:
|
|
# neither the dist-info nor the PySide6/ tree carries them), so we
|
|
# vendor the verbatim texts + a NOTICE in-repo under
|
|
# packaging/appimage/licenses/pyside6/ and copy those in. HARD-FAIL if
|
|
# the vendored texts are missing, so the compliance artifact can't be
|
|
# dropped silently.
|
|
mkdir -p AppDir/usr/share/doc/winpodx/third-party/pyside6
|
|
cp packaging/appimage/licenses/pyside6/*.txt \
|
|
AppDir/usr/share/doc/winpodx/third-party/pyside6/
|
|
if ! ls AppDir/usr/share/doc/winpodx/third-party/pyside6/ 2>/dev/null \
|
|
| grep -qiE "lgpl"; then
|
|
echo "::error::LGPL compliance: no PySide6/Qt LGPL text bundled in the AppImage; refusing to publish a non-compliant build." >&2
|
|
exit 1
|
|
fi
|
|
du -sh AppDir
|
|
|
|
- name: Slim PySide6 — drop unused Qt6 modules
|
|
# PySide6 wheels bundle the entire Qt6 stack (QtWebEngine ~100 MB+,
|
|
# QtQuick/QtQml, Qt3D, QtCharts, QtMultimedia + FFmpeg, QtPdf, …).
|
|
# winpodx links only QtCore/QtGui/QtWidgets/QtSvg/QtDBus, so the rest
|
|
# is dead weight — this was the real reason the "Thin" AppImage was
|
|
# still ~270 MB (dropping the podman stack only saved ~20 MB). The
|
|
# prune removes ~500 MB uncompressed (the bulk of the AppImage).
|
|
run: |
|
|
bash packaging/appimage/slim-pyside6.sh \
|
|
AppDir/opt/python/lib/python3.11/site-packages/PySide6
|
|
du -sh AppDir
|
|
|
|
- name: Bundle FreeRDP via Fedora 41
|
|
# Thin-bundle stage: pulls only the FreeRDP 3 client binaries +
|
|
# libwinpr out of a Fedora 41 container and traverses ldd to bring
|
|
# in transitive lib deps. Host-critical libs (glibc, libX11,
|
|
# libwayland, libGL, ...) stay on the host -- they cannot be
|
|
# bundled without breaking desktop integration or hard-crashing
|
|
# on glibc mismatch.
|
|
#
|
|
# The container stack (podman / podman-compose / conmon / crun /
|
|
# netavark / aardvark-dns / slirp4netns / passt / pasta) is
|
|
# intentionally NOT installed here -- 0.6.0 item A drops it.
|
|
# Rootless podman needs host systemd / subuid integration that
|
|
# an AppImage can't carry; bundling it caused #357 (shadowed
|
|
# host podman-compose) and #363 (LD_LIBRARY_PATH-poisoned
|
|
# systemd-run / aardvark-dns). The AppImage now requires the
|
|
# user to install a container runtime from the host package
|
|
# manager (same model as install.sh).
|
|
run: |
|
|
docker run --rm \
|
|
-v "$PWD/AppDir:/AppDir" \
|
|
-v "$PWD/packaging/appimage:/scripts" \
|
|
fedora:41 bash -c '
|
|
set -euo pipefail
|
|
dnf install -y --setopt=install_weak_deps=False \
|
|
freerdp \
|
|
freerdp-libs \
|
|
libwinpr \
|
|
file
|
|
echo "[provenance] freerdp binaries provided:"
|
|
rpm -ql freerdp | grep -E "^(/usr/bin|/usr/libexec)/.*freerdp.*" || true
|
|
bash /scripts/bundle-system-bins.sh /AppDir
|
|
# License + NOTICE texts for the bundled FreeRDP stack
|
|
# (Apache-2.0). VENDORED in-repo at
|
|
# packaging/appimage/licenses/ (mounted at /scripts/licenses),
|
|
# harvested verbatim from the Fedora rpm payloads -- see
|
|
# that dir`s README for provenance + refresh steps. We do
|
|
# NOT read them off the installed filesystem: the fedora
|
|
# image installs with `tsflags=nodocs`, which strips
|
|
# /usr/share/licenses, and every attempt to defeat that on
|
|
# the CI runner silently produced nothing.
|
|
#
|
|
# Thin AppImage: only the FreeRDP stack`s license texts
|
|
# ride along now; the container-stack license dirs (podman
|
|
# / podman-compose / conmon / crun / netavark / passt /
|
|
# slirp4netns) stay vendored in-repo for provenance but are
|
|
# no longer copied into the AppImage because their binaries
|
|
# are no longer bundled.
|
|
dst=/AppDir/usr/share/doc/winpodx/third-party
|
|
mkdir -p "$dst"
|
|
src=/scripts/licenses
|
|
# REQUIRED FreeRDP-stack license texts -- fail the build if
|
|
# missing. Apache-2.0 redistribution obligation: freerdp-libs
|
|
# + libwinpr ship the combined FreeRDP license.
|
|
for pkg in freerdp-libs libwinpr; do
|
|
if [ ! -d "$src/$pkg" ]; then
|
|
echo "FATAL: vendored license dir $src/$pkg missing -- cannot ship a" >&2
|
|
echo "license-compliant AppImage. Re-run packaging/appimage/licenses refresh." >&2
|
|
exit 1
|
|
fi
|
|
cp -rL "$src/$pkg" "$dst/$pkg"
|
|
done
|
|
echo "[provenance] bundled license dirs:"
|
|
ls -1 "$dst" || true
|
|
'
|
|
|
|
- name: Repack as thin AppImage
|
|
env:
|
|
ARCH: x86_64
|
|
run: |
|
|
OUT="winpodx-x86_64.AppImage"
|
|
appimagetool AppDir "${OUT}"
|
|
chmod +x "${OUT}"
|
|
ls -lh "${OUT}"
|
|
|
|
- name: Locate built AppImage
|
|
id: locate
|
|
run: |
|
|
APPIMAGE="$(ls -t winpodx-x86_64.AppImage | head -n1)"
|
|
if [ -z "${APPIMAGE}" ]; then
|
|
echo "No AppImage produced." >&2
|
|
exit 1
|
|
fi
|
|
echo "appimage_path=${APPIMAGE}" >> "$GITHUB_OUTPUT"
|
|
ls -lh "${APPIMAGE}"
|
|
|
|
- name: Upload as workflow artefact (always)
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: winpodx-appimage-x86_64
|
|
path: ${{ steps.locate.outputs.appimage_path }}
|
|
if-no-files-found: error
|
|
|
|
# Single-writer rule: release.yml (REL- marker tag) is the SOLE creator
|
|
# of the GitHub Release and the only writer of its title/notes. This
|
|
# workflow only attaches the AppImage to an already-created release —
|
|
# it never creates or edits the notes. (softprops/action-gh-release
|
|
# would create-or-update the release even with only `files:` set, which
|
|
# contributed to the "edited N times" churn; `gh release upload` to an
|
|
# existing release does not.) Wait up to 12 min for release.yml — which
|
|
# runs the full test suite first — to create the release; skip
|
|
# gracefully if it never appears (the AppImage stays a build artifact).
|
|
- name: Wait for release (release.yml owns creation)
|
|
if: github.event_name == 'push'
|
|
id: wait
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
tag="${{ github.ref_name }}"
|
|
for _ in $(seq 1 48); do
|
|
if gh release view "$tag" --repo "${{ github.repository }}" >/dev/null 2>&1; then
|
|
echo "Release $tag exists — attaching AppImage."
|
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
echo "Release $tag not created yet (release.yml owns creation) — waiting…"
|
|
sleep 15
|
|
done
|
|
echo "::warning::Release $tag never appeared (REL-$tag marker tag not pushed?). Skipping AppImage upload; it remains as a build artifact."
|
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Attach AppImage to GitHub Release (tag push only)
|
|
if: github.event_name == 'push' && steps.wait.outputs.exists == 'true'
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
gh release upload "${{ github.ref_name }}" \
|
|
"${{ steps.locate.outputs.appimage_path }}" --clobber \
|
|
--repo "${{ github.repository }}"
|