diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 3fafbb1c2c..dbded25528 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -14,6 +14,7 @@ on: - 'resources/**' - ".github/workflows/build_*.yml" - 'scripts/flatpak/**' + - 'snap/**' - 'scripts/msix/**' - 'tests/**' @@ -32,6 +33,7 @@ on: - 'build_release_vs2022.bat' - 'build_release_macos.sh' - 'scripts/flatpak/**' + - 'snap/**' - 'scripts/msix/**' - 'tests/**' @@ -56,7 +58,7 @@ jobs: strategy: fail-fast: false # Build both arches on every event (PRs included), through the same - # build_check_cache -> build_deps -> build_orca chain (the AppImage). + # build_check_cache -> build_deps -> build_orca chain (AppImage + snap). # aarch64 always uses the GitHub-hosted arm runner (there is no arm # self-hosted server). amd64's empty arch is load-bearing: it keeps the # historical 'linux-clang' deps cache key and the unsuffixed asset names. diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml index 941e6ed57c..f6b425613d 100644 --- a/.github/workflows/build_orca.yml +++ b/.github/workflows/build_orca.yml @@ -65,10 +65,14 @@ jobs: echo "ver_pure=$ver_pure" >> $GITHUB_ENV echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV - # Per-arch Linux AppImage naming: amd64 keeps the historical unsuffixed - # name (arch_suffix empty). Unused on macOS/Windows. + # Per-arch Linux naming, computed once: amd64 keeps the historical + # unsuffixed names (arch_suffix empty); snap_arch is snapcraft's token. + # Unused on macOS/Windows. if [ '${{ inputs.arch }}' = 'aarch64' ]; then echo "arch_suffix=_aarch64" >> $GITHUB_ENV + echo "snap_arch=arm64" >> $GITHUB_ENV + else + echo "snap_arch=amd64" >> $GITHUB_ENV fi shell: bash @@ -419,6 +423,37 @@ jobs: chmod +x "$appimage" if $tests; then tar -cvpf build_tests.tar build/tests; fi + # Build the snap right here, reusing the AppDir we just produced + # (build/package): the compiled binary + bundled libs + resources are + # repackaged as-is, for both amd64 and aarch64. Skipped on PRs (snapcraft + # adds several minutes per arch). The snap is Store-only; it is never + # attached to a GitHub release. + - name: Free disk space for snap build + if: runner.os == 'Linux' && !vars.SELF_HOSTED && github.event_name != 'pull_request' + run: sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /opt/hostedtoolcache || true + - name: Build snap + if: runner.os == 'Linux' && !vars.SELF_HOSTED && github.event_name != 'pull_request' + id: snapbuild + uses: snapcore/action-build@v1 + - name: Upload snap artifact + if: ${{ ! env.ACT && runner.os == 'Linux' && !vars.SELF_HOSTED && github.event_name != 'pull_request' }} + uses: actions/upload-artifact@v7 + with: + name: OrcaSlicer-Linux-snap_${{ env.ver }}_${{ env.snap_arch }}.snap + path: ${{ steps.snapbuild.outputs.snap }} + retention-days: 5 + if-no-files-found: error + # Nightly -> Snap Store 'edge'. NOTE: classic confinement means the Store + # holds uploads for manual review until classic is granted (snap/README.md). + - name: Publish snap to edge (nightly) + if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED + uses: snapcore/action-publish@v1 + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + with: + snap: ${{ steps.snapbuild.outputs.snap }} + release: edge + # Use tar because upload-artifacts won't always preserve directory structure # and doesn't preserve file permissions - name: Upload Test Artifact diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 98514d99db..cd7614f97e 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -79,6 +79,7 @@ jobs: -p 'OrcaSlicer_Mac_universal_*' \ -p 'OrcaSlicer_Linux_ubuntu_*' \ -p 'OrcaSlicer-Linux-flatpak_*' \ + -p 'OrcaSlicer-Linux-snap_*' \ -p 'PDB' echo "Downloaded artifact folders:" ls -1 artifacts @@ -101,6 +102,9 @@ jobs: find artifacts -type f -name '*.AppImage' -exec cp -v {} upload/ \; # Flatpak bundles (x86_64 + aarch64). find artifacts -type f -name '*.flatpak' -exec cp -v {} upload/ \; + # Snaps are intentionally NOT copied here: they go to the Snap Store + # only (see the "Publish snaps to the Snap Store" step), not to the + # GitHub release. # Windows debug symbols (PDB archive, for developers). find artifacts -type f -name 'Debug_PDB_*.7z' -exec cp -v {} upload/ \; @@ -126,3 +130,30 @@ jobs: gh release upload "$TAG" upload/* --repo "$GITHUB_REPOSITORY" --clobber echo "Uploaded to draft release: $TAG" gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' + + - name: Publish snaps to the Snap Store + # Snaps ship to the Store only (not as release assets). Channel comes from + # the tag suffix; nightly -> edge is handled in the build workflows. + # NOTE: classic confinement means the Store holds uploads for manual + # review until classic is granted (see snap/README.md). + env: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + run: | + set -euo pipefail + mapfile -t snaps < <(find artifacts -type f -name '*.snap') + if [ ${#snaps[@]} -eq 0 ]; then + echo "::warning::No .snap artifacts in run $RUN_ID; skipping Snap Store publish." + exit 0 + fi + case "$TAG" in + *-rc*) channel=candidate ;; + *-beta*|*-alpha*) channel=beta ;; + *) channel=stable ;; + esac + echo "Releasing $TAG to Snap Store channel: $channel" + sudo snap install snapcraft --classic + for s in "${snaps[@]}"; do + echo "::group::snapcraft upload $s --release=$channel" + snapcraft upload "$s" --release="$channel" + echo "::endgroup::" + done diff --git a/README.md b/README.md index 175426c818..1d2c0a5036 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,28 @@ flatpak run com.orcaslicer.OrcaSlicer It can also be installed through graphical software managers (KDE Discover, GNOME Software, etc.) when Flathub is enabled. Search for **OrcaSlicer** in your software center. +### Snap Store + +OrcaSlicer is available from the Snap Store: + +[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/orcaslicer) + +```shell +sudo snap install orcaslicer --classic +``` + +Use a different channel for pre-releases or bleeding-edge builds: + +```shell +sudo snap install orcaslicer --classic --candidate # release candidates (vX.Y.Z-rc) +sudo snap install orcaslicer --classic --beta # alpha / beta pre-releases +sudo snap install orcaslicer --classic --edge # nightly builds +``` + +The snap uses classic confinement, so it has full hardware and filesystem access just like the +AppImage — including USB/serial printers, removable media, network shares, and 3D mice +(3Dconnexion SpaceMouse via `spacenavd`). No extra setup is required after install. + ### AppImage AppImages are published for both **x86_64** and **aarch64** (ARM64). Pick the file matching your CPU — the ARM64 build has `aarch64` in its name (e.g. `OrcaSlicer_Linux_AppImage_Ubuntu2404_aarch64_*.AppImage`). diff --git a/snap/README.md b/snap/README.md new file mode 100644 index 0000000000..afe25eabbe --- /dev/null +++ b/snap/README.md @@ -0,0 +1,69 @@ +# Snap packaging + +OrcaSlicer ships a [snap](https://snapcraft.io/orcaslicer) built by **repackaging the AppImage +build output** (`build/package`) — the compiled binary, bundled private libraries and resources +are reused as-is. + +The snap uses **classic confinement**: like the AppImage, it runs in the host namespace and +resolves the desktop stack (GTK / WebKitGTK / GStreamer / GLU) from the host. Classic is required +for full hardware/filesystem access — notably the **3D mouse** (3Dconnexion SpaceMouse via the host +`spacenavd` socket at `/run/spnav.sock`), which no strict-confinement interface can reach. + +- `snapcraft.yaml` — the manifest (`plugin: dump` of `build/package`, classic confinement). +- `local/launcher` — the runtime wrapper (sets `LD_LIBRARY_PATH`, `LC_NUMERIC=C`, `SPNAV_SOCKET`). + +## CI flow + +| Trigger | Where | Snap action | +|---|---|---| +| push to `main` | `build_orca.yml` (amd64 + aarch64) | build both arches + publish to **edge** | +| PR | (none) | snap is not built on PRs (the AppImage build still runs) | +| release (manual) | `publish_release.yml` | push the build run's `.snap` artifacts to the channel below | + +The snap is **Store-only** — unlike the AppImage/Flatpak it is *not* attached to GitHub releases +(a downloaded `.snap` is useless without `snap install`). Both arches go through the same +`build_orca.yml` Linux build, reusing the AppDir (`build/package`) it just produced for the snap. +`build_all.yml`'s `build_linux` job matrixes over amd64 and aarch64 on every event; aarch64 always +uses a GitHub-hosted arm runner (amd64 honors the self-hosted runner when configured). + +## Channel mapping (tag suffix → Snap Store channel) + +| Tag | Channel | +|---|---| +| `vX.Y.Z` (release) | `stable` | +| `-rc` / `-rcN` | `candidate` | +| `-beta` / `-alpha` | `beta` | +| nightly (push to `main`) | `edge` | + +## One-time maintainer setup + +1. `snapcraft login` then `snapcraft register orcaslicer` (the name must be free; if not, change + `name:` in `snapcraft.yaml` and the asset names in the workflows). +2. **Request classic confinement** for `orcaslicer` on the [snapcraft forum](https://forum.snapcraft.io/) + (Store Requests category). Justification: a desktop slicer needs the host `spacenavd` socket + (`/run/spnav.sock`) for 3D mice plus arbitrary user/network filesystem paths that strict + interfaces cannot provide. **Until this is granted, uploads to every channel are held for manual + review**, so the automated publish below will not go live yet. +3. Export CI credentials: + `snapcraft export-login --snaps orcaslicer --channels stable,candidate,beta,edge --acls package_push,package_release exported.txt` +4. Add the file contents as the GitHub Actions secret **`SNAPCRAFT_STORE_CREDENTIALS`**. + +## Notes + +- Cross-distro library behavior matches the AppImage (relies on host libs): the host must provide + the GTK/WebKitGTK/GStreamer/GLU/OpenGL stack (the same packages the AppImage documents). +- The `classic`/`library` snapcraft linters are silenced in `snapcraft.yaml` because they assume a + self-contained snap and would flag every host-resolved library. Runtime smoke tests are the real + check. + +## Local build / test + +```shell +sudo snap install snapcraft --classic +sudo snap install lxd && sudo lxd init --auto +./build_linux.sh -dsir -l -L # produces build/package +snapcraft # -> orcaslicer__amd64.snap +sudo snap install --dangerous --classic ./orcaslicer_*.snap +snap run orcaslicer +# Smoke test: load an STL, slice, USB/serial printer, network share, and a SpaceMouse if available. +``` diff --git a/snap/local/launcher b/snap/local/launcher new file mode 100755 index 0000000000..8bee09362b --- /dev/null +++ b/snap/local/launcher @@ -0,0 +1,24 @@ +#!/bin/sh +# Snap launch wrapper for OrcaSlicer (classic confinement). +# +# Classic confinement runs in the host namespace and uses host libraries, just +# like the AppImage. This wrapper sets up only the load-bearing environment and +# execs the binary. It deliberately does NOT reuse the AppImage's orca-slicer-env +# (which probes the host for WebKitGTK and exit 1's if absent, and sets LC_ALL=C +# which would break translations) nor the Flatpak entrypoint (which assumes /app). + +# Keep the C numeric locale (decimal separator) without overriding the UI +# language, matching the Flatpak entrypoint. Otherwise some locales corrupt +# G-code coordinates. +export LC_NUMERIC=C + +# Resolve OrcaSlicer's bundled private libraries first (OpenSSL 1.1.x, CURL, +# etc.). The desktop stack (GTK / WebKitGTK / GStreamer / GLU) resolves from the +# host, exactly as in the AppImage. +export LD_LIBRARY_PATH="$SNAP/lib/orca-runtime:$SNAP/bin${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +# 3D mouse (3Dconnexion SpaceMouse via the host spacenavd daemon). Classic +# confinement can reach the host socket; mirrors the Flatpak's SPNAV_SOCKET env. +export SPNAV_SOCKET=/run/spnav.sock + +exec "$SNAP/bin/orca-slicer" "$@" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000000..47c034e3b8 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,89 @@ +name: orcaslicer +# Snap name must be lowercase with no dots; it has to be registered in the Snap +# Store before the first upload (`snapcraft register orcaslicer`). +base: core24 +adopt-info: metadata # version (from version.inc) + summary/description (from the metainfo) +grade: stable +# Classic confinement: like the AppImage, OrcaSlicer runs in the host namespace +# and uses host libraries. This is required for full hardware/filesystem access — +# notably the 3D mouse (SpaceMouse via the host spacenavd socket at +# /run/spnav.sock), which no strict-confinement interface can reach. +# NOTE: classic confinement must be granted once by the Snap Store (forum request) +# before uploads to any channel are accepted. See snap/README.md. +confinement: classic +license: AGPL-3.0-only +icon: resources/images/OrcaSlicer_192px.png +compression: lzo # faster first-launch decompression for a large (~1 GB) snap + +# As with the AppImage, the desktop stack (GTK / WebKitGTK / GStreamer / GLU) is +# resolved from the host, not bundled. The classic/library linters assume a +# self-contained snap and would flag every host-resolved library, so silence them; +# runtime smoke tests are the real check (see snap/README.md). +lint: + ignore: + - classic + - library + +platforms: + amd64: + arm64: + +apps: + orcaslicer: + command: bin/launcher # snap/local/launcher (NOT the AppImage orca-slicer-env) + common-id: com.orcaslicer.OrcaSlicer # ties the app to the AppStream component id + desktop: usr/share/applications/com.orcaslicer.OrcaSlicer.desktop + +parts: + # The compiled application, reused verbatim from the AppImage build output + # (build/package = bin/orca-slicer + lib/orca-runtime + resources + share). + # Dumped at the snap root so the binary finds its data at bin/../resources, + # exactly as it does inside the AppImage. The host supplies the desktop stack + # (GTK / WebKitGTK / GStreamer / GLU) just as it does for the AppImage. + orcaslicer: + plugin: dump + source: build/package + + # Desktop integration + Store metadata. Reuses the existing desktop file, + # metainfo and icon (no forks). Mirrors the Flatpak post-install step. + metadata: + plugin: nil + parse-info: [usr/share/metainfo/com.orcaslicer.OrcaSlicer.metainfo.xml] + build-packages: [desktop-file-utils] + override-build: | + set -e + # AppStream metainfo (drives the Store listing: summary, description, screenshots) + install -Dm644 "$CRAFT_PROJECT_DIR/scripts/flatpak/com.orcaslicer.OrcaSlicer.metainfo.xml" \ + "$CRAFT_PART_INSTALL/usr/share/metainfo/com.orcaslicer.OrcaSlicer.metainfo.xml" + + # Desktop entry: reuse the canonical one, point Exec/Icon at the snap. + install -Dm644 "$CRAFT_PROJECT_DIR/src/dev-utils/platform/unix/com.orcaslicer.OrcaSlicer.desktop" \ + "$CRAFT_PART_INSTALL/usr/share/applications/com.orcaslicer.OrcaSlicer.desktop" + desktop-file-edit \ + --set-key=Exec --set-value="orcaslicer %U" \ + --set-icon="orcaslicer" \ + "$CRAFT_PART_INSTALL/usr/share/applications/com.orcaslicer.OrcaSlicer.desktop" + + # Icons (name must match the desktop Icon= key and the snap name). + install -Dm644 "$CRAFT_PROJECT_DIR/resources/images/OrcaSlicer.svg" \ + "$CRAFT_PART_INSTALL/usr/share/icons/hicolor/scalable/apps/orcaslicer.svg" + install -Dm644 "$CRAFT_PROJECT_DIR/resources/images/OrcaSlicer_192px.png" \ + "$CRAFT_PART_INSTALL/usr/share/icons/hicolor/192x192/apps/orcaslicer.png" + + # Ship the bundled fonts in a fontconfig-scanned directory so Pango knows + # them before initialization (avoids the AddPrivateFont/ensure_faces crash + # the Flatpak guards against). + install -d "$CRAFT_PART_INSTALL/usr/share/fonts/OrcaSlicer" + install -m644 "$CRAFT_PROJECT_DIR"/resources/fonts/*.ttf \ + "$CRAFT_PART_INSTALL/usr/share/fonts/OrcaSlicer/" + + # Version straight from version.inc (single source of truth, same as AppImage/Flatpak). + ver=$(grep 'set(SoftFever_VERSION' "$CRAFT_PROJECT_DIR/version.inc" | cut -d '"' -f2) + craftctl set version="$ver" + + # Minimal snap launch wrapper (replaces the AppImage orca-slicer-env / Flatpak entrypoint). + launcher: + plugin: dump + source: snap/local + organize: + launcher: bin/launcher