Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

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 }}"