diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index 5cd22b6..db5987f 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -1,6 +1,6 @@ # Build the desktop app on every push, and attach it to every release. # -# On a push, the Windows installer and the Linux .deb are built +# On a push, the Windows installer and the Linux .deb and AppImage are built # and kept as workflow artifacts, so a change that breaks them shows up on the # commit that made it, and anyone can try that commit's build. # @@ -244,9 +244,12 @@ jobs: working-directory: desktop run: npm ci - - name: Build the .deb + - name: Build the .deb and the AppImage working-directory: desktop env: + # linuxdeploy is itself an AppImage, and a runner has no FUSE. + APPIMAGE_EXTRACT_AND_RUN: '1' + NO_STRIP: 'true' MPFTP_EMU_RUNTIME: ${{ github.workspace }}/.workbench/assets/pydevices # The cargo cache restores target/, packages from earlier versions included. run: | @@ -271,6 +274,14 @@ jobs: ls -l "$deb" sha256sum "$deb" echo "deb=$deb" >> "$GITHUB_OUTPUT" + appimages=("$bundle"/appimage/*.AppImage) + [[ ${#appimages[@]} -eq 1 ]] || { echo "::error::expected one AppImage, found ${#appimages[@]}"; exit 1; } + ls -l "${appimages[0]}" + sha256sum "${appimages[0]}" + echo "appimage=${appimages[0]}" >> "$GITHUB_OUTPUT" + # One folder, so the artifact holds both files at its top level. + mkdir linux-out + cp "$deb" "${appimages[0]}" linux-out/ # Run what was built, as someone would: install the .deb, start the app # on a virtual display, and check that it started its Python host and @@ -280,10 +291,15 @@ jobs: DEB: ${{ steps.built.outputs.deb }} run: bash desktop/scripts/smoke_linux.sh deb "$DEB" + - name: Run the AppImage + env: + APPIMAGE: ${{ steps.built.outputs.appimage }} + run: bash desktop/scripts/smoke_linux.sh appimage "$APPIMAGE" + - uses: actions/upload-artifact@v7 with: name: mpftp-desktop-linux-x64 - path: ${{ steps.built.outputs.deb }} + path: linux-out/ if-no-files-found: error compression-level: 0 @@ -322,8 +338,8 @@ jobs: run: | set -euo pipefail shopt -s nullglob - files=(dist/*-setup.exe dist/*.deb) - [[ ${#files[@]} -eq 2 ]] || { echo "::error::expected two installers, found ${#files[@]}"; ls -l dist; exit 1; } + files=(dist/*-setup.exe dist/*.deb dist/*.AppImage) + [[ ${#files[@]} -eq 3 ]] || { echo "::error::expected three installers, found ${#files[@]}"; ls -l dist; exit 1; } # Installers are named with the semver form: 0.1.5rc2 is 0.1.5-rc.2. semver="$(sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)(a|b|rc)([0-9]+)$/\1-\2.\3/; s/^([0-9]+\.[0-9]+\.[0-9]+)\.dev([0-9]+)$/\1-dev.\2/' <<< "$version")" for f in "${files[@]}"; do diff --git a/ROADMAP.md b/ROADMAP.md index 190e614..e62bc68 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,8 +9,6 @@ if something is broken or missing for you, open an - **One-click installers.** mpftp with its own Python inside. [Desktop](desktop/README.md) is a preview for Windows and Linux, attached to every release; next come signed installers and macOS. -- **Desktop as an AppImage.** One Linux download that carries its own - libraries, for distributions the `.deb` doesn't cover. - **Desktop updates itself.** Today it links to the latest release; an updater would fetch and install a new version for you. - **Where a search result comes from, in the row.** Package search in the diff --git a/desktop/README.md b/desktop/README.md index bd4ac4b..937be00 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -25,6 +25,10 @@ installers next to the wheel and the VS Code extension: `sudo apt install ./mpftp__amd64.deb` and start `mpftp`. With arguments, such as `mpftp --help` or `mpftp ls -d /dev/ttyACM0`, the same command is the CLI, run with the Python the package carries. +- **Other Linux distributions:** `mpftp__amd64.AppImage`, one file + that carries its own libraries. Make it executable (`chmod +x`) and run it; + with arguments it is the CLI, as with the `.deb`. If your system has no FUSE, + run it with `--appimage-extract-and-run`. To serial ports on Linux, your user needs to be in the `dialout` group (or `uucp` on some distributions). @@ -106,10 +110,10 @@ Then, from `desktop/`: ```bash npm ci npm run stage:linux # a python-build-standalone Python with mpftp, in python-linux/ -npm run build:linux # the .deb +npm run build:linux # the .deb and the AppImage ``` -It lands in `src-tauri/target/release/bundle/deb/`. +They land in `src-tauri/target/release/bundle/deb/` and `.../appimage/`. `tauri.linux.conf.json` swaps the Windows Python for the Linux one, and [`scripts/smoke_linux.sh`](scripts/smoke_linux.sh) starts a built package and checks that its Python host comes up. diff --git a/desktop/scripts/smoke_linux.sh b/desktop/scripts/smoke_linux.sh index b100539..a1ee4fd 100755 --- a/desktop/scripts/smoke_linux.sh +++ b/desktop/scripts/smoke_linux.sh @@ -73,7 +73,9 @@ for name in host sidecar; do || { echo "::error::the $name runs $exe, not the bundled $python"; fail=1; } done -app_pid="$(pgrep -f -x "$app" || pgrep -n -f 'squashfs-root/usr/bin/mpftp|/usr/bin/mpftp' || true)" +# The app is the host's parent. An AppImage's AppRun starts it as a bare +# `mpftp`, so its command line doesn't name the path. +app_pid="$(ps -o ppid= -p "${host%%$'\n'*}" 2>/dev/null | tr -d ' ' || true)" echo "app: ${app_pid:-not found}" [[ -n "$app_pid" ]] && kill ${app_pid} for _ in $(seq 15); do diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 001db50..fa924f5 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -453,6 +453,9 @@ fn spawn_host(python: &Path, log: &Path) -> std::io::Result<(Child, ChildStdin, cmd.args(["-m", "mpftp.desktop_host"]) // The host starts its sidecar with this same Python. .env("MPFTP_PYTHON", python) + // An AppImage's AppRun points these at its own usr/, which the bundled Python must not see. + .env_remove("PYTHONHOME") + .env_remove("PYTHONPATH") .env("PYTHONNOUSERSITE", "1") .env("PYTHONUNBUFFERED", "1") .env("PYTHONUTF8", "1") @@ -615,6 +618,9 @@ fn run_cli_if_asked() { .args(&args) // The CLI starts its sidecar with this same Python. .env("MPFTP_PYTHON", &python) + // An AppImage's AppRun points these at its own usr/, which the bundled Python must not see. + .env_remove("PYTHONHOME") + .env_remove("PYTHONPATH") .env("PYTHONNOUSERSITE", "1") .exec(); eprintln!("mpftp: could not run {}: {err}", python.display()); diff --git a/desktop/src-tauri/tauri.linux.conf.json b/desktop/src-tauri/tauri.linux.conf.json index edce3ba..6c76041 100644 --- a/desktop/src-tauri/tauri.linux.conf.json +++ b/desktop/src-tauri/tauri.linux.conf.json @@ -1,6 +1,6 @@ { "bundle": { - "targets": ["deb"], + "targets": ["deb", "appimage"], "resources": { "../python-win/": null, "../python-linux/": "python/"